scream-code 0.10.7 → 0.10.9

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.
@@ -7,7 +7,7 @@ import { i as __require, o as __toESM, r as __exportAll, t as __commonJSMin } fr
7
7
  import "./suppress-sqlite-warning-C2VB0doZ.mjs";
8
8
  import { C as join$1, D as resolve$1, E as relative$1, S as isAbsolute$1, T as parse$7, a as isSupportedFile, b as basename$1, i as ingestFile, r as ingestDirectory, t as multiSearch, w as normalize, x as dirname$2, y as KnowledgeStore } from "./src-BH9W5k24.mjs";
9
9
  import { t as require_base64_js } from "./base64-js-DzVmk6Nb.mjs";
10
- import { a as setLocale, i as getLocale, n as assertScreamHostIdentity, o as t, r as createScreamDefaultHeaders, t as TextInputDialogComponent } from "./text-input-dialog-BWp39mq8.mjs";
10
+ import { a as setLocale, i as getLocale, n as assertScreamHostIdentity, o as t, r as createScreamDefaultHeaders, t as TextInputDialogComponent } from "./text-input-dialog-DRUaPOEQ.mjs";
11
11
  import { createRequire } from "node:module";
12
12
  import { createHash, randomBytes, randomInt, randomUUID } from "node:crypto";
13
13
  import * as fs$1 from "node:fs/promises";
@@ -28,6 +28,8 @@ import Vt, { appendFileSync, chmodSync, closeSync, constants, createReadStream,
28
28
  import * as path$8 from "node:path";
29
29
  import path, { basename, dirname as dirname$1, extname, isAbsolute, join, posix, relative, resolve, sep, win32 } from "node:path";
30
30
  import { z } from "zod";
31
+ import { exec, execFile, execSync, spawn, spawnSync } from "node:child_process";
32
+ import { promisify } from "node:util";
31
33
  import { DatabaseSync } from "node:sqlite";
32
34
  import * as nodeOs from "node:os";
33
35
  import { homedir, tmpdir } from "node:os";
@@ -35,7 +37,6 @@ import { EventEmitter as EventEmitter$1 } from "node:events";
35
37
  import { StringDecoder } from "node:string_decoder";
36
38
  import Pi from "assert";
37
39
  import ro from "node:assert";
38
- import { exec, execFile, execSync, spawn, spawnSync } from "node:child_process";
39
40
  import * as posixPath from "node:path/posix";
40
41
  import * as win32Path from "node:path/win32";
41
42
  import { createServer } from "node:http";
@@ -51,10 +52,9 @@ import { AsyncLocalStorage } from "node:async_hooks";
51
52
  import { Command, Option } from "commander";
52
53
  import { createInterface } from "node:readline/promises";
53
54
  import chalk, { chalkStderr } from "chalk";
54
- import { CombinedAutocompleteProvider, Container, Editor, Image, Input, Key, Markdown, ProcessTerminal, Spacer, TUI, Text, decodeKittyPrintable, deleteAllKittyImages, fuzzyFilter, fuzzyMatch, getCapabilities, getImageDimensions, isKeyRelease, matchesKey, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@liutod-scream/pi-tui";
55
+ import { CombinedAutocompleteProvider, Container, Editor, Image, Input, Key, Markdown, ProcessTerminal, Spacer, TUI, Text, decodeKittyPrintable, deleteAllKittyImages, fuzzyFilter, fuzzyMatch, getCapabilities, getImageDimensions, isKeyRelease, matchesKey, setTightMode, sliceByColumn, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@liutod-scream/pi-tui";
55
56
  import { highlight, supportsLanguage } from "cli-highlight";
56
57
  import { diffWords } from "diff";
57
- import { promisify } from "node:util";
58
58
  import { gt, valid } from "semver";
59
59
  import { createInterface as createInterface$1 } from "node:readline";
60
60
  //#region ../../packages/agent-core/src/errors/codes.ts
@@ -10336,7 +10336,7 @@ var require_gaxios = /* @__PURE__ */ __commonJSMin(((exports) => {
10336
10336
  }
10337
10337
  static async #getFetch() {
10338
10338
  const hasWindow = typeof window !== "undefined" && !!window;
10339
- this.#fetch ||= hasWindow ? window.fetch : (await import("./src-BMbOMRuY.mjs")).default;
10339
+ this.#fetch ||= hasWindow ? window.fetch : (await import("./src-C8lOjRbK.mjs")).default;
10340
10340
  return this.#fetch;
10341
10341
  }
10342
10342
  /**
@@ -49870,6 +49870,7 @@ async function generate(provider, systemPrompt, tools, history, callbacks, optio
49870
49870
  const hasText = message.content.some((p) => p.type === "text" && p.text.trim().length > 0);
49871
49871
  const hasToolCalls = message.toolCalls.length > 0;
49872
49872
  if (hasThink && !hasText && !hasToolCalls) throw new APIEmptyResponseError(`The API returned a response containing only thinking content without any text or tool calls. This usually indicates the stream was interrupted or the output token budget was exhausted during reasoning. Provider: ${provider.name}, model: ${provider.modelName}. If this persists, reduce reasoning effort or switch to a model with a larger output budget.`);
49873
+ if (!hasText && !hasToolCalls && message.content.some((p) => p.type === "text")) throw new APIEmptyResponseError(`The API returned a response containing only empty or whitespace text. This usually indicates a truncated or malformed stream. Provider: ${provider.name}, model: ${provider.modelName}.`);
49873
49874
  if (callbacks?.onToolCall !== void 0) for (const toolCall of message.toolCalls) {
49874
49875
  await throwIfAborted$1(options?.signal, stream);
49875
49876
  await callbacks.onToolCall(toolCall);
@@ -50801,6 +50802,14 @@ function resolveGlobalLogPath(homeDir) {
50801
50802
  //#endregion
50802
50803
  //#region ../../packages/agent-core/src/utils/tokens.ts
50803
50804
  /**
50805
+ * WeakMap cache for per-message token estimates. Messages are immutable once
50806
+ * settled (streaming `partial` messages never enter compaction paths), so the
50807
+ * cache is safe. Spread copies created by micro-compaction ({ ...msg, content })
50808
+ * are new objects → natural cache miss → re-estimated. Original objects retain
50809
+ * their cached count, avoiding O(n²) re-scans during compaction detection.
50810
+ */
50811
+ const messageTokenCache = /* @__PURE__ */ new WeakMap();
50812
+ /**
50804
50813
  * Estimate token count from text using a character-based heuristic.
50805
50814
  * - ASCII (~4 chars per token)
50806
50815
  * - CJK and other non-ASCII (~1 char per token)
@@ -50830,12 +50839,15 @@ function estimateTokensForTools(tools) {
50830
50839
  return total;
50831
50840
  }
50832
50841
  function estimateTokensForMessage(message) {
50842
+ const cached = messageTokenCache.get(message);
50843
+ if (cached !== void 0) return cached;
50833
50844
  let total = estimateTokens$1(message.role);
50834
50845
  for (const part of message.content) total += estimateTokensForContentPart(part);
50835
50846
  if (message.toolCalls !== void 0) for (const call of message.toolCalls) {
50836
50847
  total += estimateTokens$1(call.name);
50837
50848
  total += estimateTokens$1(JSON.stringify(call.arguments));
50838
50849
  }
50850
+ messageTokenCache.set(message, total);
50839
50851
  return total;
50840
50852
  }
50841
50853
  function estimateTokensForContentPart(part) {
@@ -54165,7 +54177,7 @@ function matchRuleSubjects(ruleArgs, subjects, matchesPositivePattern) {
54165
54177
  }
54166
54178
  //#endregion
54167
54179
  //#region ../../packages/agent-core/src/tools/background/task-list.md
54168
- var task_list_default = "List background tasks and their current status.\n\nUse this tool to discover which background tasks exist and where each one\nstands. It is the entry point for inspecting background work: it returns a\ntask ID, status, command, description, and PID for every task it reports,\nplus the exit code and stop reason for tasks that have already finished.\n\nGuidelines:\n\n- After a context compaction, or whenever you are unsure which background\n tasks are running or what their task IDs are, call this tool to\n re-enumerate them instead of guessing a task ID.\n- Prefer the default `active_only=true`, which lists only non-terminal tasks.\n Pass `active_only=false` only when you specifically need to see tasks that\n have already finished. With `active_only=false` the result may also include\n `lost` tasks — tasks left over from a previous process that can no longer be\n inspected or controlled; treat them as already terminated.\n- `limit` caps how many tasks are returned. It accepts a value between 1 and\n 100 and defaults to 20 when omitted.\n- This tool only lists tasks; it does not return their output. Use it first\n to locate the task ID you need, then call `TaskOutput` with that ID to read\n the task's output and details.\n- This tool is read-only and does not change any state, so it is always safe\n to call, including in plan mode.\n";
54180
+ var task_list_default = "List background tasks and their current status.\n\nUse this tool to discover which background tasks exist and where each one\nstands. It is the entry point for inspecting background work: it returns a\ntask ID, status, command, description, and PID for every task it reports,\nplus the exit code and stop reason for tasks that have already finished.\n\nGuidelines:\n\n- After a context compaction, or whenever you are unsure which background\n tasks are running or what their task IDs are, call this tool to\n re-enumerate them instead of guessing a task ID.\n- Pass `active_only=false` only when you specifically need to see tasks that\n have already finished. With `active_only=false` the result may also include\n `lost` tasks — tasks left over from a previous process that can no longer be\n inspected or controlled; treat them as already terminated.\n- This tool only lists tasks; it does not return their output. Use it first\n to locate the task ID you need, then call `TaskOutput` with that ID to read\n the task's output and details.\n- This tool is read-only and does not change any state, so it is always safe\n to call, including in plan mode.\n";
54169
54181
  //#endregion
54170
54182
  //#region ../../packages/agent-core/src/tools/background/task-list.ts
54171
54183
  /**
@@ -54739,7 +54751,7 @@ function oneShotJitteredNextCronRunMs(task, idealMs, config = DEFAULT_CRON_JITTE
54739
54751
  }
54740
54752
  //#endregion
54741
54753
  //#region ../../packages/agent-core/src/tools/cron/cron-create.md
54742
- var cron_create_default = "Schedule a prompt to be enqueued at a future time. Use for both recurring schedules and one-shot reminders.\n\nUses standard 5-field cron in the user's local timezone: minute hour day-of-month month day-of-week. `0 9 * * *` means 9am local — no timezone conversion needed.\n\n## One-shot tasks (recurring: false)\n\nFor \"remind me at X\" or \"at <time>, do Y\" requests — fire once then auto-delete.\nPin minute/hour/day-of-month/month to specific values:\n \"remind me at 2:30pm today to check the deploy\" → cron: \"30 14 <today_dom> <today_month> *\", recurring: false\n \"tomorrow morning, run the smoke test\" → cron: \"57 8 <tomorrow_dom> <tomorrow_month> *\", recurring: false\n\n## Recurring jobs (recurring: true, the default)\n\nFor \"every N minutes\" / \"every hour\" / \"weekdays at 9am\" requests:\n \"*/5 * * * *\" (every 5 min), \"0 * * * *\" (hourly), \"0 9 * * 1-5\" (weekdays at 9am local)\n\n## Avoid the :00 and :30 minute marks when the task allows it\n\nEvery user who asks for \"9am\" gets `0 9`, and every user who asks for \"hourly\" gets `0 *` — which means requests from across the planet land on the API at the same instant. When the user's request is approximate, pick a minute that is NOT 0 or 30:\n \"every morning around 9\" → \"57 8 * * *\" or \"3 9 * * *\" (not \"0 9 * * *\")\n \"hourly\" → \"7 * * * *\" (not \"0 * * * *\")\n \"in an hour or so, remind me to...\" → pick whatever minute you land on, don't round\n\nOnly use minute 0 or 30 when the user names that exact time and clearly means it (\"at 9:00 sharp\", \"at half past\", coordinating with a meeting). When in doubt, nudge a few minutes early or late — the user will not notice, and the fleet will.\n\n## Coalesce semantics\n\nIf the scheduler slept past multiple ideal fire times (laptop closed, long-running turn, etc.), only **one** fire is delivered when it wakes up. The origin carries `coalescedCount` showing how many ideal fires were collapsed into this single delivery. You should treat `coalescedCount > 1` as \"I missed some checks; only the latest state matters\" rather than running the prompt that many times.\n\n## Cron-fire envelope\n\nWhen a cron task fires, the prompt you scheduled is re-injected wrapped in an XML envelope that exposes the fire context:\n\n```\n<cron-fire jobId=\"...\" cron=\"...\" recurring=\"true|false\" coalescedCount=\"N\" stale=\"true|false\">\n<prompt>\nyour original prompt text, verbatim\n</prompt>\n</cron-fire>\n```\n\nThe envelope is parseable. Use `coalescedCount > 1` to know multiple ideal fires were collapsed into a single delivery (treat as \"only the latest state matters\"), and `stale=\"true\"` as a cue that the task is past its 7-day threshold.\n\n## 7-day stale behavior\n\nRecurring tasks that have been alive for more than 7 days fire one\nfinal time with `stale: true` on the envelope, and the system then\nauto-deletes the task. The flag is the model's notice that this is\nthe last delivery. If the schedule is still wanted, call `CronCreate`\nagain with the same `cron` and `prompt` — that resets `createdAt` and\nstarts a fresh 7-day window. One-shot tasks are never marked stale.\n\nBench / acceptance runs can set `SCREAM_CRON_NO_STALE=1` to disable the\njudgment entirely.\n\n## Jitter behavior\n\nAnti-herd jitter is applied deterministically per task id:\n - Recurring: ideal fire time is shifted **forward** by an offset ≤ min(10% of the cron period, 15 minutes). A `*/5 * * * *` task can drift up to 30s; a `0 9 * * *` task can drift up to 15 minutes.\n - One-shot: only when the ideal fire lands on `:00` or `:30` of the hour, the fire is pulled **earlier** by ≤ 90 seconds. Other minutes pass through unchanged.\n\nBench / acceptance tests can set `SCREAM_CRON_NO_JITTER=1` to disable jitter entirely.\n\n## One-shot vs recurring — when to pick which\n\nUse `recurring: false` for \"remind me at X\" style requests, single deadlines, \"in N minutes do Y\", and any task that should not repeat. Use `recurring: true` for periodic polling (CI status, build watchers, scheduled reports), workday rituals, and anything the user explicitly described as recurring.\n\n## Session lifetime\n\nCron tasks live in the current scream CLI session. When you exit, they\nare persisted under the session homedir; the next `scream resume` of the\nsame session reloads them and the scheduler resumes from each task's\n`createdAt`. Fire times that fell during the offline window are\ncollapsed into a single delivery via `coalescedCount` (and recurring\ntasks past their 7-day window arrive with `stale: true` as their final\ndelivery).\n\nTasks do **not** carry over into a brand-new session — they are scoped\nto the resumed session id, not to the working directory.\n\n## Returned fields\n\n`id` (8-hex), `humanSchedule` (English summary), `recurring`,\n`nextFireAt` (ISO timestamp or null). `id` is needed by `CronDelete`.\n";
54754
+ var cron_create_default = "Schedule a prompt to be enqueued at a future time. Use for both recurring schedules and one-shot reminders.\n\n`0 9 * * *` means 9am local — no timezone conversion needed.\n\n## One-shot tasks (recurring: false)\n\nFor \"remind me at X\" or \"at <time>, do Y\" requests — fire once then auto-delete.\nPin minute/hour/day-of-month/month to specific values:\n \"remind me at 2:30pm today to check the deploy\" → cron: \"30 14 <today_dom> <today_month> *\", recurring: false\n \"tomorrow morning, run the smoke test\" → cron: \"57 8 <tomorrow_dom> <tomorrow_month> *\", recurring: false\n\n## Recurring jobs\n\nFor \"every N minutes\" / \"every hour\" / \"weekdays at 9am\" requests:\n \"*/5 * * * *\" (every 5 min), \"0 * * * *\" (hourly), \"0 9 * * 1-5\" (weekdays at 9am local)\n\n## Avoid the :00 and :30 minute marks when the task allows it\n\nEvery user who asks for \"9am\" gets `0 9`, and every user who asks for \"hourly\" gets `0 *` — which means requests from across the planet land on the API at the same instant. When the user's request is approximate, pick a minute that is NOT 0 or 30:\n \"every morning around 9\" → \"57 8 * * *\" or \"3 9 * * *\" (not \"0 9 * * *\")\n \"hourly\" → \"7 * * * *\" (not \"0 * * * *\")\n \"in an hour or so, remind me to...\" → pick whatever minute you land on, don't round\n\nOnly use minute 0 or 30 when the user names that exact time and clearly means it (\"at 9:00 sharp\", \"at half past\", coordinating with a meeting). When in doubt, nudge a few minutes early or late — the user will not notice, and the fleet will.\n\n## Coalesce semantics\n\nIf the scheduler slept past multiple ideal fire times (laptop closed, long-running turn, etc.), only **one** fire is delivered when it wakes up. The origin carries `coalescedCount` showing how many ideal fires were collapsed into this single delivery. You should treat `coalescedCount > 1` as \"I missed some checks; only the latest state matters\" rather than running the prompt that many times.\n\n## Cron-fire envelope\n\nWhen a cron task fires, the prompt you scheduled is re-injected wrapped in an XML envelope that exposes the fire context:\n\n```\n<cron-fire jobId=\"...\" cron=\"...\" recurring=\"true|false\" coalescedCount=\"N\" stale=\"true|false\">\n<prompt>\nyour original prompt text, verbatim\n</prompt>\n</cron-fire>\n```\n\nThe envelope is parseable. Use `coalescedCount > 1` to know multiple ideal fires were collapsed into a single delivery (treat as \"only the latest state matters\"), and `stale=\"true\"` as a cue that the task is past its 7-day threshold.\n\n## 7-day stale behavior\n\nRecurring tasks that have been alive for more than 7 days fire one\nfinal time with `stale: true` on the envelope, and the system then\nauto-deletes the task. The flag is the model's notice that this is\nthe last delivery. If the schedule is still wanted, call `CronCreate`\nagain with the same `cron` and `prompt` — that resets `createdAt` and\nstarts a fresh 7-day window. One-shot tasks are never marked stale.\n\nBench / acceptance runs can set `SCREAM_CRON_NO_STALE=1` to disable the\njudgment entirely.\n\n## Jitter behavior\n\nAnti-herd jitter is applied deterministically per task id:\n - Recurring: ideal fire time is shifted **forward** by an offset ≤ min(10% of the cron period, 15 minutes). A `*/5 * * * *` task can drift up to 30s; a `0 9 * * *` task can drift up to 15 minutes.\n - One-shot: only when the ideal fire lands on `:00` or `:30` of the hour, the fire is pulled **earlier** by ≤ 90 seconds. Other minutes pass through unchanged.\n\nBench / acceptance tests can set `SCREAM_CRON_NO_JITTER=1` to disable jitter entirely.\n\n## One-shot vs recurring — when to pick which\n\nUse one-shot for \"remind me at X\" style requests, single deadlines, and any task that should not repeat. Use recurring for periodic polling (CI status, build watchers, scheduled reports), workday rituals, and anything the user explicitly described as recurring.\n\n## Session lifetime\n\nCron tasks live in the current scream CLI session. When you exit, they\nare persisted under the session homedir; the next `scream resume` of the\nsame session reloads them and the scheduler resumes from each task's\n`createdAt`. Fire times that fell during the offline window are\ncollapsed into a single delivery via `coalescedCount` (and recurring\ntasks past their 7-day window arrive with `stale: true` as their final\ndelivery).\n\nTasks do **not** carry over into a brand-new session — they are scoped\nto the resumed session id, not to the working directory.\n\n## Returned fields\n\n`id` (8-hex), `humanSchedule` (English summary), `recurring`,\n`nextFireAt` (ISO timestamp or null). `id` is needed by `CronDelete`.\n";
54743
54755
  /**
54744
54756
  * Hard ceiling on `prompt` byte length (UTF-8). The zod `.max(...)`
54745
54757
  * upstream is in code units, which underflows multi-byte input
@@ -54860,7 +54872,7 @@ function formatOutput(o) {
54860
54872
  }
54861
54873
  //#endregion
54862
54874
  //#region ../../packages/agent-core/src/tools/cron/cron-delete.md
54863
- var cron_delete_default = "Cancel a scheduled cron job by id.\n\nUse this tool to remove a cron task previously scheduled with\n`CronCreate`. The `id` is the 8-hex value returned by `CronCreate`, or\nshown in the `id:` column of `CronList` — quote it verbatim, no\nprefix.\n\nBehaviour by task kind:\n\n- **Recurring task** (`recurring: true`): stops all future fires\n immediately. The scheduler picks up the deletion on its next tick.\n- **One-shot task** (`recurring: false`): cancels the pending fire if\n it has not happened yet. One-shots that have already fired\n auto-delete themselves, so calling `CronDelete` on a fired one-shot\n returns \"no cron job with id ...\".\n\nNot-found is reported as an error (not a silent no-op) so you can\ncorrect yourself — typically by calling `CronList` to see which ids\nare actually live, rather than re-trying with the same stale id.\n\nRefresh pattern (use when you want a stale recurring schedule to\ncontinue):\n\nStale recurring tasks are auto-deleted by the system after their final\nfire — there is nothing for `CronDelete` to remove at that point. To\nkeep the schedule running, just call `CronCreate` with the same `cron`\nand `prompt`. Use `CronList`'s `prompt` field to recall the original\ntext after a context compaction.\n\n`CronDelete` remains the right call when you want to cancel a task\nthat is still live (recurring not yet stale, or a one-shot still\npending).\n\nGuidelines:\n\n- Cron deletion is irreversible — there is no undo. If you delete the\n wrong task, you must re-create it with `CronCreate`.\n- If the model is unsure which id is current (e.g. after a context\n compaction), call `CronList` first rather than guessing.\n";
54875
+ var cron_delete_default = "Cancel a scheduled cron job by id.\n\nUse this tool to remove a cron task previously scheduled with\n`CronCreate`. Quote the id verbatim, no prefix.\n\nBehaviour by task kind:\n\n- **Recurring task** (`recurring: true`): stops all future fires\n immediately. The scheduler picks up the deletion on its next tick.\n- **One-shot task** (`recurring: false`): cancels the pending fire if\n it has not happened yet. One-shots that have already fired\n auto-delete themselves, so calling `CronDelete` on a fired one-shot\n returns \"no cron job with id ...\".\n\nNot-found is reported as an error (not a silent no-op) so you can\ncorrect yourself — typically by calling `CronList` to see which ids\nare actually live, rather than re-trying with the same stale id.\n\nRefresh pattern (use when you want a stale recurring schedule to\ncontinue):\n\nStale recurring tasks are auto-deleted by the system after their final\nfire — there is nothing for `CronDelete` to remove at that point. To\nkeep the schedule running, just call `CronCreate` with the same `cron`\nand `prompt`. Use `CronList`'s `prompt` field to recall the original\ntext after a context compaction.\n\n`CronDelete` remains the right call when you want to cancel a task\nthat is still live (recurring not yet stale, or a one-shot still\npending).\n\nGuidelines:\n\n- Cron deletion is irreversible — there is no undo. If you delete the\n wrong task, you must re-create it with `CronCreate`.\n- If the model is unsure which id is current (e.g. after a context\n compaction), call `CronList` first rather than guessing.\n";
54864
54876
  //#endregion
54865
54877
  //#region ../../packages/agent-core/src/tools/cron/cron-delete.ts
54866
54878
  /**
@@ -55471,7 +55483,7 @@ function filterSubagentsBySpawns$1(subagents, allowedSpawns) {
55471
55483
  }
55472
55484
  //#endregion
55473
55485
  //#region ../../packages/agent-core/src/tools/builtin/collaboration/ask-user.md
55474
- var ask_user_default = "Use this tool when you need to ask the user questions with structured options during execution. This allows you to:\n1. Collect user preferences or requirements before proceeding\n2. Resolve ambiguous or underspecified instructions\n3. Let the user decide between implementation approaches as you work\n4. Present concrete options when multiple valid directions exist\n\n**When NOT to use:**\n- When you can infer the answer from context be decisive and proceed\n- Trivial decisions that don't materially affect the outcome\n\nOverusing this tool interrupts the user's flow. Only use it when the user's input genuinely changes your next action.\n\n**Usage notes:**\n- Users always have an \"Other\" option for custom input — don't create one yourself\n- Use multi_select to allow multiple answers to be selected for a question\n- Keep option labels concise (1-5 words), use descriptions for trade-offs and details\n- Each question should have 2-4 meaningful, distinct options\n- You can ask 1-4 questions at a time; group related questions to minimize interruptions\n- If you recommend a specific option, list it first and append \"(Recommended)\" to its label";
55486
+ var ask_user_default = "Use this tool when you need to ask the user questions with structured options during execution. This allows you to:\n1. Collect user preferences or requirements before proceeding\n2. Resolve ambiguous or underspecified instructions\n3. Let the user decide between implementation approaches as you work\n4. Present concrete options when multiple valid directions exist\n\n**When NOT to use:**\n- When you can infer the answer from context - be decisive and proceed\n- Trivial decisions that don't materially affect the outcome\n\nOverusing this tool interrupts the user's flow. Only use it when the user's input genuinely changes your next action.\n\n**Usage notes:**\n- Group related questions to minimize interruptions.\n- If you recommend a specific option, list it first.\n";
55475
55487
  //#endregion
55476
55488
  //#region ../../packages/agent-core/src/tools/builtin/collaboration/ask-user.ts
55477
55489
  /**
@@ -55719,7 +55731,7 @@ const SetGoalBudgetToolInputSchema = z.object({
55719
55731
  var SetGoalBudgetTool = class {
55720
55732
  agent;
55721
55733
  name = "SetGoalBudget";
55722
- description = "Set a hard budget limit for the current goal. Accepts one limit at a time (turns, tokens, or time). The goal will be blocked when the budget is reached.";
55734
+ description = "Set a hard budget limit for the current goal. The goal will be blocked when the budget is reached.";
55723
55735
  parameters = toInputJsonSchema(SetGoalBudgetToolInputSchema);
55724
55736
  constructor(agent) {
55725
55737
  this.agent = agent;
@@ -55791,14 +55803,32 @@ const MAX_GOAL_OBJECTIVE_LENGTH = 4e3;
55791
55803
  /** Consecutive markBlocked calls with the same reason required before blocking. */
55792
55804
  const BLOCKED_STREAK_THRESHOLD = 3;
55793
55805
  /** Maximum number of working notes kept per goal. */
55794
- const MAX_GOAL_NOTES = 30;
55806
+ const MAX_GOAL_NOTES = 60;
55795
55807
  /** Maximum characters per note. */
55796
- const MAX_NOTE_LENGTH = 200;
55808
+ const MAX_NOTE_LENGTH = 400;
55797
55809
  const GOAL_CANCELLED_REMINDER = [
55798
55810
  "The user cancelled the current goal.",
55799
55811
  "Ignore earlier active-goal reminders for that goal.",
55800
55812
  "Handle the next user request normally unless the user starts or resumes a goal."
55801
55813
  ].join(" ");
55814
+ /**
55815
+ * Returns true when any configured budget has less than `threshold` of its
55816
+ * allowance remaining (e.g. 0.2 = under 20% left). Budgets that are not
55817
+ * configured (null) are ignored. Used to steer the model toward convergence
55818
+ * before a hard over-budget block fires.
55819
+ */
55820
+ function isBudgetNearExhaustion(budget, threshold) {
55821
+ if (budget.turnBudget !== null && budget.remainingTurns !== null && budget.turnBudget > 0) {
55822
+ if (budget.remainingTurns / budget.turnBudget < threshold) return true;
55823
+ }
55824
+ if (budget.tokenBudget !== null && budget.remainingTokens !== null && budget.tokenBudget > 0) {
55825
+ if (budget.remainingTokens / budget.tokenBudget < threshold) return true;
55826
+ }
55827
+ if (budget.wallClockBudgetMs !== null && budget.remainingWallClockMs !== null && budget.wallClockBudgetMs > 0) {
55828
+ if (budget.remainingWallClockMs / budget.wallClockBudgetMs < threshold) return true;
55829
+ }
55830
+ return false;
55831
+ }
55802
55832
  const GOAL_COMPLETION_REMINDER_NAME = "goal_completion_summary";
55803
55833
  const GOAL_BLOCKED_REMINDER_NAME = "goal_blocked_reason";
55804
55834
  var GoalMode = class {
@@ -55858,6 +55888,7 @@ var GoalMode = class {
55858
55888
  state.wallClockResumedAt = void 0;
55859
55889
  }
55860
55890
  if (record.budgetLimits !== void 0) state.budgetLimits = record.budgetLimits;
55891
+ if (record.objective !== void 0) state.objective = record.objective;
55861
55892
  }
55862
55893
  restoreClear(_record) {
55863
55894
  this.state = void 0;
@@ -55958,6 +55989,23 @@ var GoalMode = class {
55958
55989
  this.appendGoalUpdate({ budgetLimits: state.budgetLimits });
55959
55990
  return this.toSnapshot(state);
55960
55991
  }
55992
+ async updateObjective(input, _actor = "user") {
55993
+ const state = this.requireState();
55994
+ const objective = input.objective.trim();
55995
+ if (objective.length === 0) throw new ScreamError(ErrorCodes.GOAL_OBJECTIVE_EMPTY, "Goal objective cannot be empty");
55996
+ if (state.status === "complete") throw new ScreamError(ErrorCodes.GOAL_STATUS_INVALID, "Cannot update a completed goal.");
55997
+ if (state.status === "blocked") throw new ScreamError(ErrorCodes.GOAL_STATUS_INVALID, "Cannot update a blocked goal. Resume it first.");
55998
+ state.objective = objective;
55999
+ const noteContent = `Goal objective updated by user: ${objective}`.slice(0, MAX_NOTE_LENGTH);
56000
+ state.notes.push({
56001
+ content: noteContent,
56002
+ time: Date.now()
56003
+ });
56004
+ if (state.notes.length > MAX_GOAL_NOTES) state.notes = state.notes.slice(-60);
56005
+ this.persistState(state);
56006
+ this.appendGoalUpdate({ objective });
56007
+ return this.toSnapshot(state);
56008
+ }
55961
56009
  async cancelGoal(actor = "user") {
55962
56010
  const state = this.requireState();
55963
56011
  const snapshot = this.toSnapshot(state);
@@ -56041,7 +56089,7 @@ var GoalMode = class {
56041
56089
  content: trimmed,
56042
56090
  time: Date.now()
56043
56091
  });
56044
- if (state.notes.length > MAX_GOAL_NOTES) state.notes = state.notes.slice(-30);
56092
+ if (state.notes.length > MAX_GOAL_NOTES) state.notes = state.notes.slice(-60);
56045
56093
  this.persistState(state, { silent: true });
56046
56094
  return this.toSnapshot(state);
56047
56095
  }
@@ -56187,6 +56235,85 @@ function formatTokens$2(tokens) {
56187
56235
  return `${(tokens / 1e6).toFixed(1)}M`;
56188
56236
  }
56189
56237
  //#endregion
56238
+ //#region ../../packages/agent-core/src/tools/builtin/goal/emission-guard.ts
56239
+ /**
56240
+ * Grader emission guard - deduplicates and filters grader (referee) feedback
56241
+ * before it is injected into the agent context.
56242
+ *
56243
+ * Without this guard, a grader that repeatedly returns the same FAIL reason
56244
+ * (e.g. "redo", "not right") causes the identical feedback to be appended to
56245
+ * context on every submission. This wastes context budget and can trap the
56246
+ * agent in a loop where it keeps retrying without new information.
56247
+ *
56248
+ * Inspired by oh-my-pi's `advisor/emission-guard.ts`, but adapted for
56249
+ * scream-code's grader model: the grader is called once per `complete`
56250
+ * submission (not continuously), so per-round limiting is unnecessary. The
56251
+ * guard focuses on normalization, content-free filtering, and exact-text
56252
+ * deduplication keyed by goal objective.
56253
+ */
56254
+ /** Short phrases that carry no actionable feedback. */
56255
+ const CONTENT_FREE_PHRASES = new Set([
56256
+ "stop",
56257
+ "done",
56258
+ "lgtm",
56259
+ "no issue",
56260
+ "redo",
56261
+ "try again",
56262
+ "not good enough",
56263
+ "fix it",
56264
+ "wrong",
56265
+ "incorrect",
56266
+ "bad",
56267
+ "重做",
56268
+ "不对",
56269
+ "不行",
56270
+ "继续",
56271
+ "再做",
56272
+ "错误",
56273
+ "不行",
56274
+ "不好",
56275
+ "改"
56276
+ ]);
56277
+ /** Maximum number of historically seen feedback texts retained per goal. */
56278
+ const MAX_SEEN_PER_GOAL = 16;
56279
+ /** Phrases at or below this length are checked against the content-free set. */
56280
+ function isContentFree(text) {
56281
+ const lower = text.toLowerCase().trim();
56282
+ return CONTENT_FREE_PHRASES.has(lower);
56283
+ }
56284
+ /**
56285
+ * Filters grader feedback before injection into the agent context.
56286
+ *
56287
+ * Returns the denoised feedback text if it should be injected, or `null` if
56288
+ * the feedback is empty, content-free, or an exact duplicate of a previously
56289
+ * seen feedback for the same goal. The caller should skip injection when
56290
+ * `null` is returned.
56291
+ *
56292
+ * @param reason The raw grader FAIL reason.
56293
+ * @param goalKey A stable identifier for the current goal (e.g. the objective
56294
+ * text). Different goals get independent deduplication buckets.
56295
+ */
56296
+ var GraderEmissionGuard = class {
56297
+ seenByGoal = /* @__PURE__ */ new Map();
56298
+ filter(reason, goalKey) {
56299
+ const normalized = reason.normalize("NFKC").trim();
56300
+ if (normalized.length === 0) return null;
56301
+ if (isContentFree(normalized)) return null;
56302
+ const key = normalized.toLowerCase();
56303
+ const bucket = this.seenByGoal.get(goalKey);
56304
+ if (bucket !== void 0 && bucket.includes(key)) return null;
56305
+ const next = bucket ?? [];
56306
+ next.push(key);
56307
+ if (next.length > MAX_SEEN_PER_GOAL) next.shift();
56308
+ this.seenByGoal.set(goalKey, next);
56309
+ return normalized;
56310
+ }
56311
+ /** Clear the deduplication bucket for a goal (e.g. on goal change). */
56312
+ resetGoal(goalKey) {
56313
+ this.seenByGoal.delete(goalKey);
56314
+ }
56315
+ };
56316
+ //#endregion
56190
56317
  //#region ../../packages/agent-core/src/tools/builtin/goal/update-goal.ts
56191
56318
  const UpdateGoalToolInputSchema = z.object({
56192
56319
  status: z.enum([
@@ -56198,6 +56325,13 @@ const UpdateGoalToolInputSchema = z.object({
56198
56325
  reason: z.string().optional().describe("Optional reason for the status change, especially for blocked.")
56199
56326
  }).strict();
56200
56327
  const MAX_GRADER_OUTPUT_CHARS = 4e3;
56328
+ /** Maximum characters of `git diff --stat HEAD` to append to the grader input. */
56329
+ const MAX_DIFF_STAT_CHARS = 2e3;
56330
+ /**
56331
+ * Module-level emission guard. Deduplicates grader FAIL feedback per goal so
56332
+ * the referee cannot trap the agent in a loop by repeating the same reason.
56333
+ */
56334
+ const graderEmissionGuard = new GraderEmissionGuard();
56201
56335
  function extractRecentOutput(history) {
56202
56336
  const parts = [];
56203
56337
  for (let i = history.length - 1; i >= 0; i--) {
@@ -56210,6 +56344,51 @@ function extractRecentOutput(history) {
56210
56344
  const joined = parts.join("\n\n");
56211
56345
  return joined.length > MAX_GRADER_OUTPUT_CHARS ? `${joined.slice(0, MAX_GRADER_OUTPUT_CHARS)}…` : joined;
56212
56346
  }
56347
+ /**
56348
+ * Append cross-turn working notes to the output seen by the grader. Notes are
56349
+ * written by the agent across continuation turns, so they provide focused
56350
+ * context (key findings, constraints, partial results) without exposing the
56351
+ * full text of previously rejected outputs.
56352
+ */
56353
+ function appendGoalNotes(output, notes) {
56354
+ if (notes.length === 0) return output;
56355
+ return `${output}\n\n## Cross-turn working notes\n${notes.map((note) => `• ${note.content}`).join("\n")}`;
56356
+ }
56357
+ const execFileAsync = promisify(execFile);
56358
+ /**
56359
+ * Fetch a concise git diff stat against HEAD for the current working directory.
56360
+ * Using HEAD includes both staged and unstaged changes, so the reviewer sees
56361
+ * every file touched during the turn even if the agent ran `git add`. Returns
56362
+ * `null` when there are no changes. Throws when git is unavailable or the
56363
+ * directory is not a git repository.
56364
+ */
56365
+ /** Exported for testing only. */
56366
+ async function fetchGitDiffStat(cwd) {
56367
+ const { stdout } = await execFileAsync("git", [
56368
+ "diff",
56369
+ "--stat",
56370
+ "HEAD"
56371
+ ], { cwd });
56372
+ const stat = stdout.trim();
56373
+ return stat.length > 0 ? stat : null;
56374
+ }
56375
+ /**
56376
+ * Append a git diff stat to the grader input so the reviewer can correlate the
56377
+ * agent's claims with the actual files changed during the turn. Very large
56378
+ * stats are truncated to avoid overflowing the reviewer's context window. When
56379
+ * git is unavailable, a note is appended so the reviewer knows why no diff is
56380
+ * present.
56381
+ */
56382
+ async function appendGitDiffStat(output, cwd) {
56383
+ let stat;
56384
+ try {
56385
+ stat = await fetchGitDiffStat(cwd);
56386
+ } catch {
56387
+ return `${output}\n\n## Changes this turn\n(Git diff unavailable — workspace may not be a git repository or git is not installed.)`;
56388
+ }
56389
+ if (stat === null) return output;
56390
+ return `${output}\n\n## Changes this turn\n${stat.length > MAX_DIFF_STAT_CHARS ? `${stat.slice(0, MAX_DIFF_STAT_CHARS)}…\n(diff stat truncated)` : stat}`;
56391
+ }
56213
56392
  var UpdateGoalTool = class {
56214
56393
  agent;
56215
56394
  grader;
@@ -56256,7 +56435,7 @@ var UpdateGoalTool = class {
56256
56435
  async handleComplete(goal) {
56257
56436
  const goalState = goal.getGoal().goal;
56258
56437
  if (!goalState) return { output: "No active goal." };
56259
- const output = extractRecentOutput(this.agent.context.history);
56438
+ const outputWithContext = await appendGitDiffStat(appendGoalNotes(extractRecentOutput(this.agent.context.history), goalState.notes), this.agent.config?.cwd ?? "");
56260
56439
  try {
56261
56440
  await goal.pauseGoal({ reason: "verifying" }, "system");
56262
56441
  } catch (error) {
@@ -56264,7 +56443,7 @@ var UpdateGoalTool = class {
56264
56443
  }
56265
56444
  let rawGrade;
56266
56445
  try {
56267
- rawGrade = await this.grader(goalState.objective, goalState.completionCriterion, output);
56446
+ rawGrade = await this.grader(goalState.objective, goalState.completionCriterion, outputWithContext);
56268
56447
  } catch (error) {
56269
56448
  const resumeError = await resumeAfterGrading(goal);
56270
56449
  if (resumeError !== void 0) return resumeError;
@@ -56289,6 +56468,7 @@ var UpdateGoalTool = class {
56289
56468
  if (grade.pass) {
56290
56469
  try {
56291
56470
  const completed = await goal.markComplete({}, "model");
56471
+ graderEmissionGuard.resetGoal(goalState.objective);
56292
56472
  if (completed === null) return toolError("Failed to mark verified goal complete", goal);
56293
56473
  this.agent.context.appendSystemReminder(buildGoalCompletionSummaryPrompt(completed), {
56294
56474
  kind: "system_trigger",
@@ -56302,8 +56482,12 @@ var UpdateGoalTool = class {
56302
56482
  stopTurn: true
56303
56483
  };
56304
56484
  }
56305
- this.appendGradingFeedback(grade.reason);
56306
- return { output: `Verification failed: ${grade.reason}. Continue working.` };
56485
+ const denoised = graderEmissionGuard.filter(grade.reason, goalState.objective);
56486
+ if (denoised !== null) {
56487
+ this.appendGradingFeedback(denoised);
56488
+ return { output: `Verification failed: ${denoised}. Continue working.` };
56489
+ }
56490
+ return { output: "Previous verification feedback still applies. Address the earlier feedback and retry." };
56307
56491
  }
56308
56492
  appendGradingFeedback(reason) {
56309
56493
  this.agent.context.appendSystemReminder(buildGradingFeedbackPrompt(reason), {
@@ -56340,11 +56524,11 @@ function errorMessage$4(error) {
56340
56524
  }
56341
56525
  //#endregion
56342
56526
  //#region ../../packages/agent-core/src/tools/builtin/goal/write-goal-note.ts
56343
- const WriteGoalNoteInputSchema = z.object({ content: z.string().min(1).max(200).describe("A concise note about what you learned, verified, or decided. Notes are injected into future continuation turns so you can build on prior work.") }).strict();
56527
+ const WriteGoalNoteInputSchema = z.object({ content: z.string().min(1).max(400).describe("A concise note about what you learned, verified, or decided. Notes are injected into future continuation turns so you can build on prior work.") }).strict();
56344
56528
  var WriteGoalNoteTool = class {
56345
56529
  agent;
56346
56530
  name = "WriteGoalNote";
56347
- description = "Record a working note during goal execution. Notes persist across continuation turns and are injected automatically. Use this to record facts you verified, dead ends you hit, decisions you made, or anything future-you should not re-derive.";
56531
+ description = "Record a working note during goal execution. Use this for dead ends you hit, decisions you made, or anything future-you should not re-derive.";
56348
56532
  parameters = toInputJsonSchema(WriteGoalNoteInputSchema);
56349
56533
  constructor(agent) {
56350
56534
  this.agent = agent;
@@ -57577,6 +57761,16 @@ function parseMemoryMemos(text) {
57577
57761
  }
57578
57762
  return memos;
57579
57763
  }
57764
+ /**
57765
+ * Strip injected memory-memo content from text before writing it back to the
57766
+ * store, preventing feedback loops where recalled/compacted content gets
57767
+ * re-stored. Removes fenced ```memory-memo blocks (the format emitted by
57768
+ * compaction and exit-time extraction) and any <memories>...</memories>
57769
+ * wrapper an injector might use.
57770
+ */
57771
+ function stripMemoryTags(text) {
57772
+ return text.replaceAll(/```memory-memo[\s\S]*?```/gi, "").replaceAll(/<memories>[\s\S]*?<\/memories>/gi, "").trim();
57773
+ }
57580
57774
  /** System prompt for exit-time extraction — instructs the LLM how to extract. */
57581
57775
  const EXIT_EXTRACTION_SYSTEM_PROMPT = "你是一个任务经验提取助手。任务是从对话记录中识别已完成的任务闭环,提炼出任务经验记录。用对话的主要语言输出(中文对话用中文,英文对话用英文)。只输出指定的 JSON 格式,不要调用任何工具。";
57582
57776
  /** Build the user prompt for exit-time extraction, including a conversation sample. */
@@ -58210,7 +58404,7 @@ const MemoryLookupInputSchema = z.object({
58210
58404
  var MemoryLookupTool = class {
58211
58405
  agent;
58212
58406
  name = "MemoryLookup";
58213
- description = "Search the memory memo store for historical experiences from past user tasks. Call this when the current task may benefit from prior work, when you encounter a repeating error or pattern, or when you are unsure of the best approach. Returns memos ranked by relevance, including the approach taken, the outcome, what failed, what worked, project, and tags. By default searches globally; use scope: project to restrict results to the current project.";
58407
+ description = "Search the memory memo store for historical experiences from past user tasks. Call this when the current task may benefit from prior work, when you encounter a repeating error or pattern, or when you are unsure of the best approach. Returns memos ranked by relevance, including the approach taken, the outcome, what failed, what worked, project, and tags.";
58214
58408
  parameters = toInputJsonSchema(MemoryLookupInputSchema);
58215
58409
  constructor(agent) {
58216
58410
  this.agent = agent;
@@ -58312,7 +58506,7 @@ const MemoryWriteInputSchema = z.object({
58312
58506
  var MemoryWriteTool = class {
58313
58507
  agent;
58314
58508
  name = "MemoryWrite";
58315
- description = "Write a new memory memo to the global memory memo store. Call this when the user explicitly asks to save an experience, lesson, or summary to memory, for example \"保存到记忆\", \"保存到备忘录\", \"总结并保存\", \"永久记忆\", \"记录我的记忆\", \"记住这个\", \"添加到记忆\", or \"存入记忆库\". Summarize the user need, approach taken, final outcome, what failed, what worked, and 3-5 tags.";
58509
+ description = "Write a new memory memo to the global memory memo store. Call this when the user explicitly asks to save an experience, lesson, or summary to memory, for example \"保存到记忆\", \"保存到备忘录\", \"总结并保存\", \"永久记忆\", \"记录我的记忆\", \"记住这个\", \"添加到记忆\", or \"存入记忆库\".";
58316
58510
  parameters = toInputJsonSchema(MemoryWriteInputSchema);
58317
58511
  constructor(agent) {
58318
58512
  this.agent = agent;
@@ -58329,17 +58523,20 @@ var MemoryWriteTool = class {
58329
58523
  };
58330
58524
  const sessionId = this.agent.homedir ? basename$1(dirname$2(dirname$2(this.agent.homedir))) : "unknown";
58331
58525
  const sourceSessionTitle = await this.agent.getSessionTitle();
58332
- const whatFailed = args.whatFailed?.trim();
58333
- const whatWorked = args.whatWorked?.trim();
58334
- const tags = normalizeTags(args.tags !== void 0 && args.tags.length > 0 ? args.tags : generateTags(`${args.userNeed} ${args.approach}`));
58526
+ const userNeed = stripMemoryTags(args.userNeed);
58527
+ const approach = stripMemoryTags(args.approach);
58528
+ const outcome = stripMemoryTags(args.outcome);
58529
+ const whatFailed = stripMemoryTags(args.whatFailed ?? "");
58530
+ const whatWorked = stripMemoryTags(args.whatWorked ?? "");
58531
+ const tags = normalizeTags(args.tags !== void 0 && args.tags.length > 0 ? args.tags : generateTags(`${userNeed} ${approach}`));
58335
58532
  const memo = createMemoryMemo({
58336
58533
  sourceSessionId: sessionId,
58337
58534
  sourceSessionTitle,
58338
- userNeed: args.userNeed,
58339
- approach: args.approach,
58340
- outcome: args.outcome,
58341
- whatFailed: whatFailed === void 0 || whatFailed.length === 0 ? "none" : whatFailed,
58342
- whatWorked: whatWorked === void 0 || whatWorked.length === 0 ? "none" : whatWorked,
58535
+ userNeed,
58536
+ approach,
58537
+ outcome,
58538
+ whatFailed: whatFailed.length === 0 ? "none" : whatFailed,
58539
+ whatWorked: whatWorked.length === 0 ? "none" : whatWorked,
58343
58540
  tags,
58344
58541
  extractionSource: "manual",
58345
58542
  projectDir: this.agent.config.cwd
@@ -58875,8 +59072,7 @@ var LspTool = class {
58875
59072
  lspRegistry;
58876
59073
  name = "LSP";
58877
59074
  description = [
58878
- "Query a language server for code intelligence.",
58879
- "Use 'references' to find all usages of a symbol, 'definition' to jump to where a symbol is defined, 'diagnostics' to get type errors and warnings for a file, and 'rename' to rename a symbol across all its references.",
59075
+ "Query a language server for code intelligence: find usages, jump to definitions, get diagnostics, or rename a symbol across all references.",
58880
59076
  "The language server is started automatically for supported file types (TypeScript/JavaScript, Python, Rust, Go).",
58881
59077
  "Rename requires the typescript-language-server (or equivalent) binary on PATH for the file type."
58882
59078
  ].join(" ");
@@ -71155,7 +71351,7 @@ function validateSkillPlan(plan, nameHint) {
71155
71351
  }
71156
71352
  //#endregion
71157
71353
  //#region ../../packages/agent-core/src/tools/builtin/collaboration/wolfpack.md
71158
- var wolfpack_default = "Use WolfPack to spawn multiple subagents in parallel for batch operations.\nThis is ideal when processing many independent items (files, checks, searches)\nthat all use the same subagent type and follow a similar pattern.\n\nInput:\n- description: Brief (3-5 word) task summary.\n- subagent_type: Subagent profile name. Defaults to \"coder\". Choose the profile\n that best matches the batch task — using the right type materially improves\n output quality. See the agent type list below for which type fits which job.\n- prompt_template: A prompt pattern where each item value is substituted in\n to produce a per-item prompt. See the parameter schema for placeholder syntax.\n- items: Array of item strings. Each item gets its own subagent (no limit).\n\nItems must be independent no subagent depends on another's output.\nIf items depend on each other, use separate Agent calls instead.\n\nChoosing subagent_type for the batch:\n- Batch code review, audit, or bug-finding across files reviewer\n- Batch writing, reports, or long-form content writer\n- Batch read-only exploration (find files, grep, understand modules) explore\n- Batch verification (run build/test/lint per item) verify\n- Batch deep debugging or architecture decisions oracle\n- Batch planning or design work plan\n- General engineering tasks with no specialised match coder (default)\n\nExample: review source files for OWASP vulnerabilities by setting items to the file\npaths, subagent_type to \"reviewer\", and prompt_template to the review instruction.\nAll items are processed in parallel.\n\n";
71354
+ var wolfpack_default = "Use WolfPack to spawn multiple subagents in parallel for batch operations.\nThis is ideal when processing many independent items (files, checks, searches)\nthat all use the same subagent type and follow a similar pattern.\n\nItems must be independent - no subagent depends on another's output.\nIf items depend on each other, use separate Agent calls instead.\n\nChoosing subagent_type for the batch:\n- Batch code review, audit, or bug-finding across files -> reviewer\n- Batch writing, reports, or long-form content -> writer\n- Batch read-only exploration (find files, grep, understand modules) -> explore\n- Batch verification (run build/test/lint per item) -> verify\n- Batch deep debugging or architecture decisions -> oracle\n- Batch planning or design work -> plan\n- General engineering tasks with no specialised match -> coder (default)\n\nExample: review source files for OWASP vulnerabilities by setting items to the file\npaths, subagent_type to \"reviewer\", and prompt_template to the review instruction.\nAll items are processed in parallel.\n";
71159
71355
  //#endregion
71160
71356
  //#region ../../packages/agent-core/src/tools/builtin/collaboration/wolfpack.ts
71161
71357
  /**
@@ -71353,6 +71549,68 @@ function withTimeout$1(promise, timeoutMs, parentSignal) {
71353
71549
  });
71354
71550
  }
71355
71551
  //#endregion
71552
+ //#region ../../packages/agent-core/src/tools/support/scan-cache.ts
71553
+ var FsScanCache = class {
71554
+ cache = /* @__PURE__ */ new Map();
71555
+ ttlMs;
71556
+ maxEntries;
71557
+ constructor({ ttlMs = 1e3, maxEntries = 16 } = {}) {
71558
+ this.ttlMs = ttlMs;
71559
+ this.maxEntries = maxEntries;
71560
+ }
71561
+ key(root, pattern, includeDirs) {
71562
+ return `${root}\0${pattern}\0${String(includeDirs)}`;
71563
+ }
71564
+ /**
71565
+ * Returns the cached output if present and not expired. Expired entries are
71566
+ * deleted on access.
71567
+ */
71568
+ get(root, pattern, includeDirs) {
71569
+ const k = this.key(root, pattern, includeDirs);
71570
+ const entry = this.cache.get(k);
71571
+ if (entry === void 0) return void 0;
71572
+ if (Date.now() - entry.createdAt > this.ttlMs) {
71573
+ this.cache.delete(k);
71574
+ return;
71575
+ }
71576
+ return entry.output;
71577
+ }
71578
+ /** Cache a scan output. Evicts the oldest entry when at capacity. */
71579
+ set(root, pattern, includeDirs, output) {
71580
+ if (this.cache.size >= this.maxEntries) {
71581
+ let oldestKey;
71582
+ let oldestTime = Infinity;
71583
+ for (const [k, v] of this.cache) if (v.createdAt < oldestTime) {
71584
+ oldestTime = v.createdAt;
71585
+ oldestKey = k;
71586
+ }
71587
+ if (oldestKey !== void 0) this.cache.delete(oldestKey);
71588
+ }
71589
+ this.cache.set(this.key(root, pattern, includeDirs), {
71590
+ output,
71591
+ createdAt: Date.now()
71592
+ });
71593
+ }
71594
+ /** Invalidate all cached entries rooted under the given directory path. */
71595
+ invalidateByRoot(root) {
71596
+ const prefix = `${root}\0`;
71597
+ for (const k of this.cache.keys()) if (k.startsWith(prefix)) this.cache.delete(k);
71598
+ }
71599
+ /** Clear the entire cache (useful for tests). */
71600
+ clear() {
71601
+ this.cache.clear();
71602
+ }
71603
+ /** Number of cached entries (for tests / introspection). */
71604
+ size() {
71605
+ return this.cache.size;
71606
+ }
71607
+ };
71608
+ /**
71609
+ * Module-level singleton shared by Glob (producer) and Edit/Write (invalidator).
71610
+ * Importing this instance ensures all tools reference the same cache.
71611
+ */
71612
+ const scanCache = new FsScanCache();
71613
+ //#endregion
71356
71614
  //#region ../../packages/agent-core/src/tools/builtin/file/conflict-detect.ts
71357
71615
  /**
71358
71616
  * Detect unresolved git merge conflict markers in read output.
@@ -71599,7 +71857,7 @@ function hashEditPayload(input) {
71599
71857
  }
71600
71858
  //#endregion
71601
71859
  //#region ../../packages/agent-core/src/tools/builtin/file/edit.md
71602
- var edit_default = "Perform exact string replacements against the text view returned by Read.\n\n- When copying from Read output, omit the line-number prefix and tab; match only the file content.\n- By default, old_string must occur exactly once. If it matches multiple locations, add surrounding context or set replace_all when every occurrence should change.\n- Prefer Edit for targeted changes to existing files; use Write only for new files or complete overwrites.\n- To modify a file, always use Edit; do not run a Shell `sed` command for edits.\n- When making several independent changes, issue multiple Edit calls in parallel within a single response; edits to the same file are serialized automatically by a write lock.\n- When several parallel Edit calls target the same file, a write lock serializes them; they apply in the order the calls appear in your response. An edit fails with `old_string not found` if its old_string was taken from text an earlier edit already replaced base every old_string on the latest Read view and order dependent edits accordingly.\n- For pure CRLF files, Read shows LF and Edit.old_string/new_string should use LF; Edit writes the file back with CRLF preserved.\n- For mixed line endings or lone carriage returns, Read displays carriage returns as \\r; include actual \\r escapes in old_string/new_string for those positions.\n- When Read returned an `Anchor:` value in its status block, pass it as `anchor` to verify the file has not changed since it was read. If the anchor does not match, re-read the file before editing.\n- Edit refuses if `old_string` lands inside an unresolved merge conflict block, or if `new_string` would introduce `<<<<<<<`/`=======`/`>>>>>>>` markers. To clean up a conflict, include the markers in `old_string` so Edit replaces them.";
71860
+ var edit_default = "Perform exact string replacements against the text view returned by Read.\n\n- By default, old_string must occur exactly once. If it matches multiple locations, add surrounding context.\n- Prefer Edit for targeted changes to existing files; use Write only for new files or complete overwrites.\n- To modify a file, always use Edit; do not run a Shell `sed` command for edits.\n- When making several independent changes, issue multiple Edit calls in parallel within a single response; edits to the same file are serialized automatically by a write lock.\n- When several parallel Edit calls target the same file, a write lock serializes them; they apply in the order the calls appear in your response. An edit fails with `old_string not found` if its old_string was taken from text an earlier edit already replaced - base every old_string on the latest Read view and order dependent edits accordingly.\n- Edit refuses if `old_string` lands inside an unresolved merge conflict block, or if `new_string` would introduce `<<<<<<<`/`=======`/`>>>>>>>` markers. To clean up a conflict, include the markers in `old_string` so Edit replaces them.\n";
71603
71861
  //#endregion
71604
71862
  //#region ../../packages/agent-core/src/tools/builtin/file/edit.ts
71605
71863
  const EditInputSchema = z.object({
@@ -71648,10 +71906,11 @@ var EditTool = class {
71648
71906
  pathClass: this.jian.pathClass(),
71649
71907
  homeDir: this.jian.gethome()
71650
71908
  }),
71651
- execute: () => this.execution(args, path)
71909
+ execute: ({ signal }) => this.execution(args, path, signal)
71652
71910
  };
71653
71911
  }
71654
- async execution(args, safePath) {
71912
+ async execution(args, safePath, signal) {
71913
+ signal?.throwIfAborted();
71655
71914
  const result = await this.executionCore(args, safePath);
71656
71915
  const inputHash = hashEditPayload({
71657
71916
  path: args.path,
@@ -71748,6 +72007,7 @@ var EditTool = class {
71748
72007
  }
71749
72008
  const newContent = replaceOnceLiteral(content, args.old_string, args.new_string);
71750
72009
  await this.jian.writeText(safePath, materializeModelText(newContent, modelView.lineEndingStyle));
72010
+ scanCache.clear();
71751
72011
  const { notice, hasErrors } = await this.appendDiagnostics(safePath);
71752
72012
  const output = `Replaced 1 occurrence in ${args.path}${notice}`;
71753
72013
  return hasErrors ? {
@@ -71766,6 +72026,7 @@ var EditTool = class {
71766
72026
  }
71767
72027
  const newContent = parts.join(args.new_string);
71768
72028
  await this.jian.writeText(safePath, materializeModelText(newContent, modelView.lineEndingStyle));
72029
+ scanCache.clear();
71769
72030
  const { notice, hasErrors } = await this.appendDiagnostics(safePath);
71770
72031
  const output = `Replaced ${String(replacementCount)} occurrences in ${args.path}${notice}`;
71771
72032
  return hasErrors ? {
@@ -71862,7 +72123,7 @@ async function listDirectory(jian, workDir = jian.getcwd()) {
71862
72123
  }
71863
72124
  //#endregion
71864
72125
  //#region ../../packages/agent-core/src/tools/builtin/file/glob.md
71865
- var glob_default = "Find files (and optionally directories) by glob pattern, sorted by modification time (most recent first).\n\nGood patterns:\n- `*.ts` — files in the current directory matching an extension\n- `src/**/*.ts` — recursive with a subdirectory anchor and extension\n- `test_*.py` — files whose name starts with a literal prefix\n\nRejected patterns (no literal anchor — nothing bounds the result set):\n- `**`, `**/*`, `*/*` — pure wildcards. Add an extension or subdirectory to give the walk a concrete target.\n- Anything that starts with `**/` (e.g. `**/*.md`, `**/main/*.py`). The leading `**/` has no literal anchor in front of it. Anchor it with a top-level subdirectory like `src/**/*.md`.\n- `*.{ts,tsx}` — brace expansion is not supported. Issue two calls: `*.ts` and `*.tsx`.\n\nLarge-directory warning — avoid recursing into dependency/build output even with an anchor:\n- `node_modules/**/*.js`, `.venv/**/*.py`, `__pycache__/**`, `target/**` all match technically but\n typically produce thousands of results that truncate at the match cap and waste the caller context.\n Prefer specific subpaths like `node_modules/react/src/**/*.js`.";
72126
+ var glob_default = "Find files (and optionally directories) by glob pattern, sorted by modification time (most recent first).\n\nGood patterns:\n- `*.ts` — files in the current directory matching an extension\n- `src/**/*.ts` — recursive with a subdirectory anchor and extension\n- `test_*.py` — files whose name starts with a literal prefix\n\nRejected patterns (no literal anchor — nothing bounds the result set):\n- `**`, `**/*`, `*/*` — pure wildcards. Add an extension or subdirectory to give the walk a concrete target.\n- Anything that starts with `**/` (e.g. `**/*.md`, `**/main/*.py`). The leading `**/` has no literal anchor in front of it. Anchor it with a top-level subdirectory like `src/**/*.md`.\n- `*.{ts,tsx}` — brace expansion is not supported. Issue two calls: `*.ts` and `*.tsx`.\n\nLarge-directory warning — avoid recursing into dependency/build output even with an anchor:\n- `node_modules/**/*.js`, `.venv/**/*.py`, `__pycache__/**`, `target/**` all match technically but\n typically produce thousands of results that truncate at the {{ MAX_MATCHES }}-match cap and waste the caller context.\n Prefer specific subpaths like `node_modules/react/src/**/*.js`.";
71866
72127
  //#endregion
71867
72128
  //#region ../../packages/agent-core/src/tools/builtin/file/glob.ts
71868
72129
  const GlobInputSchema = z.object({
@@ -71871,6 +72132,7 @@ const GlobInputSchema = z.object({
71871
72132
  include_dirs: z.boolean().default(true).optional().describe("Whether to include directories in results. Defaults to true. Set false to return only files.")
71872
72133
  });
71873
72134
  const MAX_MATCHES = 1e3;
72135
+ const GLOB_DESCRIPTION = renderPrompt(glob_default, { MAX_MATCHES });
71874
72136
  /**
71875
72137
  * Path-shape hint appended to the tool description only on a Windows
71876
72138
  * (`win32` path class) backend. The `path` argument accepts both native
@@ -71900,7 +72162,7 @@ var GlobTool = class {
71900
72162
  constructor(jian, workspace) {
71901
72163
  this.jian = jian;
71902
72164
  this.workspace = workspace;
71903
- this.description = this.jian.pathClass() === "win32" ? glob_default + WINDOWS_PATH_HINT : glob_default;
72165
+ this.description = this.jian.pathClass() === "win32" ? GLOB_DESCRIPTION + WINDOWS_PATH_HINT : GLOB_DESCRIPTION;
71904
72166
  }
71905
72167
  async resolveExecution(args) {
71906
72168
  let path;
@@ -71924,10 +72186,11 @@ var GlobTool = class {
71924
72186
  },
71925
72187
  approvalRule: literalRulePattern(this.name, args.pattern),
71926
72188
  matchesRule: (ruleArgs) => matchesGlobRuleSubject(ruleArgs, args.pattern),
71927
- execute: () => this.execution(args, searchRoots)
72189
+ execute: ({ signal }) => this.execution(args, searchRoots, signal)
71928
72190
  };
71929
72191
  }
71930
- async execution(args, searchRoots) {
72192
+ async execution(args, searchRoots, signal) {
72193
+ signal?.throwIfAborted();
71931
72194
  if (startsWithDoubleStarPrefix(args.pattern)) {
71932
72195
  let tree;
71933
72196
  try {
@@ -71976,6 +72239,8 @@ var GlobTool = class {
71976
72239
  }
71977
72240
  }
71978
72241
  try {
72242
+ const cachedOutput = scanCache.get(searchRoots[0], args.pattern, includeDirs);
72243
+ if (cachedOutput !== void 0) return { output: cachedOutput };
71979
72244
  const seen = /* @__PURE__ */ new Set();
71980
72245
  const entries = [];
71981
72246
  const YIELD_SAFETY_CAP = MAX_MATCHES * 2;
@@ -71983,6 +72248,7 @@ var GlobTool = class {
71983
72248
  let truncated = false;
71984
72249
  outer: for (const root of searchRoots) for await (const filePath of this.jian.glob(root, args.pattern, { allowedRoots: [root] })) {
71985
72250
  yielded++;
72251
+ if (signal && yielded % 128 === 0) signal.throwIfAborted();
71986
72252
  if (yielded >= YIELD_SAFETY_CAP) {
71987
72253
  truncated = true;
71988
72254
  break outer;
@@ -72011,16 +72277,22 @@ var GlobTool = class {
72011
72277
  const pathClass = this.jian.pathClass();
72012
72278
  const relBase = searchRoots[0] ?? this.workspace.workspaceDir;
72013
72279
  const displayLines = paths.map((p) => relativizeIfUnder$1(p, relBase, pathClass));
72014
- if (entries.length === 0 && !truncated) return { output: "No matches found" };
72015
- const lines = [];
72016
- if (truncated) {
72017
- lines.push(`[Truncated at ${String(MAX_MATCHES)} matches — use a more specific pattern]`);
72018
- lines.push(`Only the first ${String(MAX_MATCHES)} matches are returned.`);
72280
+ let output;
72281
+ if (entries.length === 0 && !truncated) output = "No matches found";
72282
+ else {
72283
+ const lines = [];
72284
+ if (truncated) {
72285
+ lines.push(`[Truncated at ${String(MAX_MATCHES)} matches — use a more specific pattern]`);
72286
+ lines.push(`Only the first ${String(MAX_MATCHES)} matches are returned.`);
72287
+ }
72288
+ lines.push(...displayLines);
72289
+ if (!truncated && entries.length === 1e3) lines.push(`Found ${String(entries.length)} matches`);
72290
+ output = lines.join("\n");
72019
72291
  }
72020
- lines.push(...displayLines);
72021
- if (!truncated && entries.length === 1e3) lines.push(`Found ${String(entries.length)} matches`);
72022
- return { output: lines.join("\n") };
72292
+ scanCache.set(searchRoots[0], args.pattern, includeDirs, output);
72293
+ return { output };
72023
72294
  } catch (error) {
72295
+ if (signal?.aborted) throw error;
72024
72296
  if (error !== null && typeof error === "object" && "code" in error) {
72025
72297
  const code = error.code;
72026
72298
  const path = searchRoots[0] ?? this.workspace.workspaceDir;
@@ -75297,6 +75569,85 @@ function rgUnavailableMessage(cause) {
75297
75569
  Error: ${cause instanceof Error ? cause.message : typeof cause === "string" ? cause : "unknown error"}\n\nFix options:\n macOS: brew install ripgrep\n Ubuntu: sudo apt-get install ripgrep\n Other: https://github.com/BurntSushi/ripgrep#installation\n\nAlternatively, drop a static rg binary at ${join$1(getShareDir(), "bin", rgBinaryName())}`;
75298
75570
  }
75299
75571
  //#endregion
75572
+ //#region ../../packages/agent-core/src/tools/support/rg-pattern-sanitize.ts
75573
+ /**
75574
+ * Ripgrep pattern sanitization and error detection.
75575
+ *
75576
+ * Users frequently search for code fragments that contain regex metacharacters
75577
+ * (e.g. `fetchFoo(`, `${platform}`, `a{3`). ripgrep uses the Rust `regex`
75578
+ * crate, which rejects unescaped `{`/`}` that do not form a valid repetition
75579
+ * quantifier (`{N}`, `{N,}`, `{N,M}`). This module pre-cleans such braces so
75580
+ * the common case "search for a literal-ish fragment" succeeds without a
75581
+ * syntax error.
75582
+ *
75583
+ * When sanitization is insufficient (e.g. unbalanced parentheses), the caller
75584
+ * can detect the regex error from ripgrep stderr and retry with
75585
+ * `--fixed-strings`, which treats the pattern as a literal string.
75586
+ */
75587
+ /** Matches the inner content of a valid repetition quantifier: `N`, `N,`, `N,M`. */
75588
+ const QUANTIFIER_INNER_RE = /^\d+(?:,\d*)?$/;
75589
+ /**
75590
+ * Escape `{` and `}` that cannot form a valid repetition quantifier so they
75591
+ * are treated as literals by the regex engine.
75592
+ *
75593
+ * Already-escaped braces (`\{`, `\}`) are preserved. Valid quantifiers
75594
+ * (`{3}`, `{2,}`, `{1,5}`) are preserved. Everything else is escaped.
75595
+ *
75596
+ * Escaping braces inside character classes (e.g. `[{]`) is also safe:
75597
+ * `\{` inside `[...]` matches a literal `{` just like an unescaped `{` would.
75598
+ */
75599
+ function sanitizeRgPattern(pattern) {
75600
+ let result = "";
75601
+ let i = 0;
75602
+ while (i < pattern.length) {
75603
+ const ch = pattern[i];
75604
+ if (ch === "\\" && i + 1 < pattern.length) {
75605
+ result += pattern.slice(i, i + 2);
75606
+ i += 2;
75607
+ continue;
75608
+ }
75609
+ if (ch === "{") {
75610
+ const closeIdx = pattern.indexOf("}", i + 1);
75611
+ if (closeIdx !== -1) {
75612
+ const inner = pattern.slice(i + 1, closeIdx);
75613
+ if (QUANTIFIER_INNER_RE.test(inner)) {
75614
+ result += pattern.slice(i, closeIdx + 1);
75615
+ i = closeIdx + 1;
75616
+ continue;
75617
+ }
75618
+ }
75619
+ result += "\\{";
75620
+ i++;
75621
+ continue;
75622
+ }
75623
+ if (ch === "}") {
75624
+ result += "\\}";
75625
+ i++;
75626
+ continue;
75627
+ }
75628
+ result += ch;
75629
+ i++;
75630
+ }
75631
+ return result;
75632
+ }
75633
+ /** Substrings that indicate ripgrep failed to parse the regex pattern. */
75634
+ const REGEX_ERROR_MARKERS = [
75635
+ "regex parse error",
75636
+ "unrecognized escape",
75637
+ "unclosed group",
75638
+ "unbalanced",
75639
+ "unexpected repetition"
75640
+ ];
75641
+ /**
75642
+ * Detect whether ripgrep's stderr indicates a regex syntax error (as opposed
75643
+ * to a filesystem or runtime error). Used to decide whether a
75644
+ * `--fixed-strings` fallback retry is worthwhile.
75645
+ */
75646
+ function isRegexSyntaxError(stderr) {
75647
+ const lower = stderr.toLowerCase();
75648
+ return REGEX_ERROR_MARKERS.some((marker) => lower.includes(marker));
75649
+ }
75650
+ //#endregion
75300
75651
  //#region ../../packages/agent-core/src/tools/support/result-builder.ts
75301
75652
  const DEFAULT_MAX_CHARS = 5e4;
75302
75653
  const DEFAULT_TAIL_CHARS = 2e4;
@@ -75307,17 +75658,25 @@ var ToolResultBuilder = class {
75307
75658
  maxChars;
75308
75659
  maxTailChars;
75309
75660
  maxLineLength;
75661
+ artifactSink;
75662
+ maxFullOutputChars;
75310
75663
  buffer = [];
75311
75664
  nCharsValue = 0;
75312
75665
  truncationHappened = false;
75313
75666
  headTruncated = false;
75314
75667
  tailBuf = [];
75315
75668
  tailCharsValue = 0;
75669
+ fullOutput;
75670
+ fullOutputChars = 0;
75671
+ totalLinesWritten = 0;
75316
75672
  constructor(options = {}) {
75317
75673
  this.maxChars = options.maxChars ?? DEFAULT_MAX_CHARS;
75318
75674
  this.maxTailChars = options.maxTailChars ?? DEFAULT_TAIL_CHARS;
75319
75675
  this.maxLineLength = options.maxLineLength === void 0 ? DEFAULT_MAX_LINE_LENGTH : options.maxLineLength;
75320
75676
  if (this.maxLineLength !== null && this.maxLineLength <= 14) throw new Error("maxLineLength must be greater than the truncation marker length.");
75677
+ this.artifactSink = options.artifactSink;
75678
+ this.maxFullOutputChars = options.maxFullOutputChars ?? 1e6;
75679
+ if (this.artifactSink !== void 0) this.fullOutput = [];
75321
75680
  }
75322
75681
  get nChars() {
75323
75682
  return this.nCharsValue + this.tailCharsValue;
@@ -75327,12 +75686,19 @@ var ToolResultBuilder = class {
75327
75686
  if (!this.headTruncated || this.tailCharsValue === 0) return head;
75328
75687
  this.trimTail();
75329
75688
  const tail = this.tailBuf.join("");
75330
- return `${head}${head.endsWith("\n") ? "" : "\n"}${TRUNCATION_MARKER}\n${tail}`;
75689
+ const separator = head.endsWith("\n") ? "" : "\n";
75690
+ const elided = this.computeElidedLines(head, tail);
75691
+ return `${head}${separator}${elided > 0 ? `[…${String(elided)} lines elided…]\n${TRUNCATION_MARKER}` : TRUNCATION_MARKER}\n${tail}`;
75331
75692
  }
75332
75693
  write(text) {
75333
75694
  if (text.length === 0) return 0;
75334
75695
  const lines = text.match(/[^\r\n]*(?:\r\n|[\n\r])|[^\r\n]+/g) ?? [];
75335
75696
  if (lines.length === 0) return 0;
75697
+ this.totalLinesWritten += lines.length;
75698
+ if (this.fullOutput !== void 0 && this.fullOutputChars < this.maxFullOutputChars) {
75699
+ this.fullOutput.push(text);
75700
+ this.fullOutputChars += text.length;
75701
+ }
75336
75702
  let charsWritten = 0;
75337
75703
  for (const originalLine of lines) if (this.nCharsValue < this.maxChars) {
75338
75704
  const remainingChars = this.maxChars - this.nCharsValue;
@@ -75380,11 +75746,36 @@ var ToolResultBuilder = class {
75380
75746
  this.tailBuf.push(trimmed);
75381
75747
  this.tailCharsValue = trimmed.length;
75382
75748
  }
75383
- ok(message = "", options = {}) {
75749
+ computeElidedLines(head, tail) {
75750
+ if (this.totalLinesWritten === 0) return 0;
75751
+ if (this.fullOutput !== void 0) {
75752
+ const total = countLines(this.fullOutput.join(""));
75753
+ return Math.max(0, total - countLines(head) - countLines(tail));
75754
+ }
75755
+ return Math.max(0, this.totalLinesWritten - countLines(head) - countLines(tail));
75756
+ }
75757
+ async maybeWriteArtifact() {
75758
+ if (this.artifactSink === void 0 || this.fullOutput === void 0) return void 0;
75759
+ if (!this.truncationHappened) return void 0;
75760
+ const full = this.fullOutput.join("");
75761
+ if (full.length === 0) return void 0;
75762
+ try {
75763
+ return await this.artifactSink(full);
75764
+ } catch {
75765
+ return;
75766
+ }
75767
+ }
75768
+ appendArtifactRef(output, ref) {
75769
+ const line = `[full output saved: ${ref}]`;
75770
+ return output.length === 0 ? line : output.endsWith("\n") ? `${output}${line}` : `${output}\n${line}`;
75771
+ }
75772
+ async ok(message = "", options = {}) {
75384
75773
  let finalMessage = message;
75385
75774
  if (finalMessage.length > 0 && !finalMessage.endsWith(".")) finalMessage += ".";
75386
75775
  if (this.truncationHappened) finalMessage = finalMessage.length === 0 ? TRUNCATION_MESSAGE : `${finalMessage} ${TRUNCATION_MESSAGE}`;
75387
- const output = this.toString();
75776
+ const baseOutput = this.toString();
75777
+ const artifactRef = await this.maybeWriteArtifact();
75778
+ const output = artifactRef === void 0 ? baseOutput : this.appendArtifactRef(baseOutput, artifactRef);
75388
75779
  return {
75389
75780
  isError: false,
75390
75781
  output: finalMessage.length > 0 && (this.truncationHappened || output.length === 0) ? output.length === 0 ? finalMessage : output.endsWith("\n") ? `${output}${finalMessage}` : `${output}\n${finalMessage}` : output,
@@ -75393,9 +75784,11 @@ var ToolResultBuilder = class {
75393
75784
  brief: options.brief
75394
75785
  };
75395
75786
  }
75396
- error(message, options = {}) {
75787
+ async error(message, options = {}) {
75397
75788
  const finalMessage = this.truncationHappened ? message.length === 0 ? TRUNCATION_MESSAGE : `${message} ${TRUNCATION_MESSAGE}` : message;
75398
- const output = this.toString();
75789
+ const baseOutput = this.toString();
75790
+ const artifactRef = await this.maybeWriteArtifact();
75791
+ const output = artifactRef === void 0 ? baseOutput : this.appendArtifactRef(baseOutput, artifactRef);
75399
75792
  return {
75400
75793
  isError: true,
75401
75794
  output: finalMessage.length === 0 ? output : output.length === 0 ? finalMessage : output.endsWith("\n") ? `${output}${finalMessage}` : `${output}\n${finalMessage}`,
@@ -75405,9 +75798,13 @@ var ToolResultBuilder = class {
75405
75798
  };
75406
75799
  }
75407
75800
  };
75801
+ function countLines(text) {
75802
+ if (text.length === 0) return 0;
75803
+ return text.split("\n").length - (text.endsWith("\n") ? 1 : 0);
75804
+ }
75408
75805
  //#endregion
75409
75806
  //#region ../../packages/agent-core/src/tools/builtin/file/grep.md
75410
- var grep_default = "Search file contents using regular expressions (powered by ripgrep).\n\nUse Grep when the task is to find unknown content or unknown file locations. Do not use shell `grep` or `rg` directly; this tool applies workspace path policy, output limits, and sensitive-file filtering.\nALWAYS use Grep tool instead of running `grep` or `rg` from a shell direct shell calls bypass workspace policy, output limits, and sensitive-file filtering.\nIf you already know a concrete file path and need to inspect its contents, use Read directly instead.\n\nWrite patterns in ripgrep regex syntax, which differs from POSIX `grep` syntax. For example, braces are special, so escape them as `\\{` to match a literal `{`.\n\nHidden files (dotfiles such as `.gitlab-ci.yml` or `.eslintrc.json`) are searched by default. To also search files excluded by `.gitignore` (such as `node_modules` or build outputs), set `include_ignored` to `true`. Sensitive files (such as `.env`) are always skipped for safety, even when `include_ignored` is `true`.\n";
75807
+ var grep_default = "Search file contents using regular expressions (powered by ripgrep).\n\nUse Grep when the task is to find unknown content or unknown file locations. Do not use shell `grep` or `rg` directly; this tool applies workspace path policy, output limits, and sensitive-file filtering.\nALWAYS use Grep tool instead of running `grep` or `rg` from a shell - direct shell calls bypass workspace policy, output limits, and sensitive-file filtering.\n\nWrite patterns in ripgrep regex syntax, which differs from POSIX `grep` syntax. For example, braces are special, so escape them as `\\{` to match a literal `{`.\n\nHidden files (dotfiles such as `.gitlab-ci.yml` or `.eslintrc.json`) are searched by default.\n";
75411
75808
  //#endregion
75412
75809
  //#region ../../packages/agent-core/src/tools/builtin/file/grep.ts
75413
75810
  const GrepInputSchema = z.object({
@@ -75475,11 +75872,12 @@ const SENSITIVE_GLOBS_TO_EXCLUDE = [
75475
75872
  "**/.gcp/credentials/**"
75476
75873
  ];
75477
75874
  const CONTENT_LINE_RE = /^(.*?)([:-])(\d+)\2/;
75875
+ const GREP_DESCRIPTION = renderPrompt(grep_default, { DEFAULT_HEAD_LIMIT });
75478
75876
  var GrepTool = class {
75479
75877
  jian;
75480
75878
  workspace;
75481
75879
  name = "Grep";
75482
- description = grep_default;
75880
+ description = GREP_DESCRIPTION;
75483
75881
  parameters = toInputJsonSchema(GrepInputSchema);
75484
75882
  constructor(jian, workspace) {
75485
75883
  this.jian = jian;
@@ -75536,6 +75934,15 @@ var GrepTool = class {
75536
75934
  runResult = await runRipgrepOnce(this.jian, buildRgArgs(rgPath, args, searchPaths, true), signal);
75537
75935
  if (runResult.kind === "tool-error") return runResult.result;
75538
75936
  }
75937
+ if (runResult.exitCode !== 0 && runResult.exitCode !== 1 && !runResult.timedOut && isRegexSyntaxError(runResult.stderrText)) {
75938
+ const fallback = await runRipgrepOnce(this.jian, buildRgArgs(rgPath, args, searchPaths, false, true), signal);
75939
+ if (fallback.kind === "tool-error") return fallback.result;
75940
+ if (shouldRetryRipgrepEagain(fallback)) {
75941
+ const retry = await runRipgrepOnce(this.jian, buildRgArgs(rgPath, args, searchPaths, true, true), signal);
75942
+ if (retry.kind === "tool-error") return retry.result;
75943
+ if (retry.exitCode === 0 || retry.exitCode === 1) runResult = retry;
75944
+ } else if (fallback.exitCode === 0 || fallback.exitCode === 1) runResult = fallback;
75945
+ }
75539
75946
  const { exitCode, stderrText, bufferTruncated, stderrTruncated, timedOut } = runResult;
75540
75947
  let { stdoutText } = runResult;
75541
75948
  if (exitCode !== 0 && exitCode !== 1 && !timedOut) return {
@@ -75594,7 +76001,7 @@ var GrepTool = class {
75594
76001
  const combined = visibleBody === "" && messages.length === 0 ? emptyResultMessage : messages.length > 0 ? visibleBody === "" ? messages.join("\n") : `${visibleBody}\n${messages.join("\n")}` : visibleBody;
75595
76002
  const builder = new ToolResultBuilder();
75596
76003
  builder.write(combined);
75597
- const result = builder.ok(sideChannelMessages.join("\n"));
76004
+ const result = await builder.ok(sideChannelMessages.join("\n"));
75598
76005
  const display = buildSearchResultsDisplay(args, limited, mode, contentIncludesLineNumbers);
75599
76006
  if (display === void 0) return result;
75600
76007
  if (result.isError === true) return result;
@@ -75790,10 +76197,11 @@ async function mapWithConcurrency(items, concurrency, signal, mapper) {
75790
76197
  if (signal.aborted) throw new GrepAbortedError();
75791
76198
  return results;
75792
76199
  }
75793
- function buildRgArgs(rgPath, args, searchPaths, singleThreaded = false) {
76200
+ function buildRgArgs(rgPath, args, searchPaths, singleThreaded = false, literal = false) {
75794
76201
  const cmd = [rgPath];
75795
76202
  if (singleThreaded) cmd.push("-j", "1");
75796
76203
  cmd.push("--hidden");
76204
+ if (literal) cmd.push("--fixed-strings");
75797
76205
  const mode = args.output_mode ?? "files_with_matches";
75798
76206
  if (mode !== "content") cmd.push("--max-columns", String(RG_MAX_COLUMNS));
75799
76207
  cmd.push("--null");
@@ -75816,7 +76224,8 @@ function buildRgArgs(rgPath, args, searchPaths, singleThreaded = false) {
75816
76224
  if (args.multiline) cmd.push("-U", "--multiline-dotall");
75817
76225
  if (args.include_ignored) cmd.push("--no-ignore");
75818
76226
  for (const glob of SENSITIVE_GLOBS_TO_EXCLUDE) cmd.push("--glob", `!${glob}`);
75819
- cmd.push("--", args.pattern, ...searchPaths);
76227
+ const pattern = literal ? args.pattern : sanitizeRgPattern(args.pattern);
76228
+ cmd.push("--", pattern, ...searchPaths);
75820
76229
  return cmd;
75821
76230
  }
75822
76231
  function splitRgLines(text) {
@@ -76530,7 +76939,7 @@ async function partitionExistingPaths(paths, jian, workspace) {
76530
76939
  }
76531
76940
  //#endregion
76532
76941
  //#region ../../packages/agent-core/src/tools/builtin/file/read.md
76533
- var read_default = "Read a text file from the local filesystem.\n\nIf the user provides a concrete file path to a text file, call Read directly. Do not `Glob`, `ls`, or otherwise pre-check known text file paths; missing or invalid file paths return errors you can handle. Do not use Read for directories; use `ls` via Bash for a known directory, or Glob when you need files/directories matching a pattern. Use `Grep` only when the task is to search for unknown content or locations.\n\nWhen you need several files, prefer to read them in parallel: emit multiple `Read` calls in a single response instead of reading one file per turn.\n\n- Relative paths resolve against the working directory; a path outside the working directory must be absolute.\n- Returns up to {{ MAX_LINES }} lines or {{ MAX_BYTES_KB }} KB per call, whichever comes first; lines longer than {{ MAX_LINE_LENGTH }} chars are truncated mid-line.\n- Page larger files with `line_offset` (1-based start line) and `n_lines`. Omit `n_lines` to read up to the {{ MAX_LINES }}-line cap.\n- Sensitive files (`.env` files, credential stores, SSH keys, and similar secrets) are refused to protect secrets; do not attempt to read them.\n- Only UTF-8 text files can be read. Non-UTF-8 encodings, binary files, and files containing NUL bytes are refused; use `ReadMediaFile` for images or video, and Bash or an MCP tool for other binary formats.\n- Negative line_offset reads from the end of the file (for example, -100 reads the last 100 lines); the absolute value cannot exceed {{ MAX_LINES }}.\n- Output format: `<line-number>\\t<content>` per line.\n- A `<system>...</system>` status block is appended after the file content; it summarizes how much was read (line and byte counts, truncation, line-ending notes) and is not part of the file itself. The status block includes an `Anchor: <hash>` value that can be passed to `Edit.anchor` to verify the file has not changed before editing.\n- Pure CRLF files are displayed with LF line endings; `Edit` matches this output and preserves CRLF when writing back.\n- Mixed or lone carriage-return line endings are shown as `\\r` and require exact `Edit.old_string` escapes.\n- After a successful `Edit`/`Write`, do not re-read solely to prove the write landed. When the task depends on an exact file, API, or output shape, inspect the final external contract before finishing.\n";
76942
+ var read_default = "Read a text file from the local filesystem.\n\nIf the user provides a concrete file path to a text file, call Read directly. Do not `Glob`, `ls`, or otherwise pre-check known text file paths; missing or invalid file paths return errors you can handle. Do not use Read for directories; use `ls` via Bash for a known directory, or Glob when you need files/directories matching a pattern. Use `Grep` only when the task is to search for unknown content or locations.\n\nWhen you need several files, prefer to read them in parallel: emit multiple `Read` calls in a single response instead of reading one file per turn.\n\n- Returns up to {{ MAX_BYTES_KB }} KB per call; lines longer than {{ MAX_LINE_LENGTH }} chars are truncated mid-line.\n- Sensitive files (`.env` files, credential stores, SSH keys, and similar secrets) are refused to protect secrets; do not attempt to read them.\n- Only UTF-8 text files can be read. Non-UTF-8 encodings, binary files, and files containing NUL bytes are refused; use `ReadMediaFile` for images or video, and Bash or an MCP tool for other binary formats.\n- Output format: `<line-number>\\t<content>` per line.\n- A `<system>...</system>` status block is appended after the file content; it summarizes how much was read (line and byte counts, truncation, line-ending notes) and is not part of the file itself. The status block includes an `Anchor: <hash>` value that can be passed to `Edit.anchor` to verify the file has not changed before editing.\n- Pure CRLF files are displayed with LF line endings; `Edit` matches this output and preserves CRLF when writing back.\n- Mixed or lone carriage-return line endings are shown as `\\r` and require exact `Edit.old_string` escapes.\n- After a successful `Edit`/`Write`, do not re-read solely to prove the write landed. When the task depends on an exact file, API, or output shape, inspect the final external contract before finishing.\n";
76534
76943
  //#endregion
76535
76944
  //#region ../../packages/agent-core/src/tools/builtin/file/read.ts
76536
76945
  const MAX_LINES = 1e3;
@@ -76886,7 +77295,7 @@ var ReadTool = class {
76886
77295
  if (input.maxLinesReached) parts.push(`Max ${String(MAX_LINES)} lines reached.`);
76887
77296
  else if (input.maxBytesReached) parts.push(`Max ${String(MAX_BYTES)} bytes reached.`);
76888
77297
  else if (lineCount < input.requestedLines) parts.push("End of file reached.");
76889
- if (input.truncatedLineNumbers.length > 0) parts.push(`Lines [${input.truncatedLineNumbers.join(", ")}] were truncated.`);
77298
+ if (input.truncatedLineNumbers.length > 0) parts.push(`Lines [${input.truncatedLineNumbers.join(", ")}] exceed the ${String(MAX_LINE_LENGTH)}-char limit and were truncated. Re-read with line_offset and n_lines=1 to view individual long lines in full.`);
76890
77299
  if (input.lineEndingStyle === "mixed") parts.push("Mixed or lone carriage-return line endings are shown as \\r. Use exact \\r\\n or \\r escapes in Edit.old_string for those lines.");
76891
77300
  parts.push(`Anchor: ${input.anchor}`);
76892
77301
  return parts.join(" ");
@@ -77285,7 +77694,7 @@ var ReadMediaFileTool = class {
77285
77694
  };
77286
77695
  //#endregion
77287
77696
  //#region ../../packages/agent-core/src/tools/builtin/file/write.md
77288
- var write_default = "Overwrite or append to a file with content exactly as provided, creating the file if needed; the parent directory must already exist. Defaults to overwrite; append adds content to the end without adding a newline. Write does not use the Read/Edit model text view and does not preserve or infer the previous line-ending style: \\n stays LF, \\r\\n stays CRLF. Use Edit for targeted changes to existing files. When the content is very large, you can split it across multiple calls: write the first chunk with overwrite, then add the remaining chunks with append. Write refuses content containing `<<<<<<<`/`=======`/`>>>>>>>` merge conflict markers resolve conflicts first.\n";
77697
+ var write_default = "Write does not preserve or infer the previous line-ending style: \\n stays LF, \\r\\n stays CRLF. Use Edit for targeted changes to existing files. When the content is very large, you can split it across multiple calls: write the first chunk with overwrite, then add the remaining chunks with append. Write refuses content containing `<<<<<<<`/`=======`/`>>>>>>>` merge conflict markers - resolve conflicts first.\n";
77289
77698
  //#endregion
77290
77699
  //#region ../../packages/agent-core/src/tools/builtin/file/write.ts
77291
77700
  /** Mask isolating the file-type bits of a stat mode. */
@@ -77333,10 +77742,11 @@ var WriteTool = class {
77333
77742
  pathClass: this.jian.pathClass(),
77334
77743
  homeDir: this.jian.gethome()
77335
77744
  }),
77336
- execute: () => this.execution(args, path)
77745
+ execute: ({ signal }) => this.execution(args, path, signal)
77337
77746
  };
77338
77747
  }
77339
- async execution(args, safePath) {
77748
+ async execution(args, safePath, signal) {
77749
+ signal?.throwIfAborted();
77340
77750
  const parentError = await this.checkParentDirectory(safePath);
77341
77751
  if (parentError !== void 0) return {
77342
77752
  isError: true,
@@ -77351,6 +77761,7 @@ var WriteTool = class {
77351
77761
  const mode = args.mode ?? "overwrite";
77352
77762
  if (mode === "append") await this.jian.writeText(safePath, args.content, { mode: "a" });
77353
77763
  else await this.jian.writeText(safePath, args.content);
77764
+ scanCache.clear();
77354
77765
  const bytesWritten = Buffer.byteLength(args.content, "utf8");
77355
77766
  const { notice, hasErrors } = await this.appendDiagnostics(safePath);
77356
77767
  const output = `${mode === "append" ? "Appended" : "Wrote"} ${String(bytesWritten)} bytes to ${args.path}${notice}`;
@@ -77400,7 +77811,7 @@ var WriteTool = class {
77400
77811
  };
77401
77812
  //#endregion
77402
77813
  //#region ../../packages/agent-core/src/tools/builtin/planning/enter-plan-mode.md
77403
- var enter_plan_mode_default = "Use this tool proactively when you're about to start a non-trivial implementation task.\nGetting user sign-off on your approach via ExitPlanMode before writing code prevents wasted effort.\n\n## Planning Modes\n\nThe host supports two planning strategies. You can request either one via this tool:\n\n- **Normal plan** (default): You investigate the codebase, design a single implementation approach, write it to the plan file, and present it for approval. Best when the task is straightforward or you are already confident about the right approach.\n- **Fusion plan**: Invoke the FusionPlan tool to spawn multiple independent planning subagents in parallel, each exploring a different angle, then synthesize their outputs into one consolidated plan. Best when the task is ambiguous, has many valid approaches, crosses many files, or when exploration itself adds significant value. Fusion plan may take longer but tends to surface risks and alternatives you might miss.\n\nTo request a fusion plan, include `mode: 'fusion'` in your tool arguments. When in fusion strategy, call the FusionPlan tool to generate the plan.\n\n### When to choose which mode\n\nPrefer **normal plan** when:\n1. The user gave specific, detailed instructions.\n2. The change is small or localized (1-3 files, single concern).\n3. You are confident about the codebase structure and the right approach.\n4. Speed matters more than exploring alternatives.\n\nPrefer **fusion plan** when:\n1. The task is open-ended or ambiguous (e.g. \"improve performance\", \"refactor auth\").\n2. Multiple valid architectures or approaches exist.\n3. The change spans more than 3-5 files or touches core abstractions.\n4. You are unfamiliar with the relevant code paths and want parallel exploration.\n5. The user explicitly asked for a thorough plan or mentioned comparing options.\n\nIf unsure, choose normal plan for small fixes and fusion plan for larger design tasks.\n\n## When to Use\n\nUse this tool when ANY of these conditions apply:\n\n1. New Feature Implementation - e.g. \"Add a caching layer to the API\"\n2. Multiple Valid Approaches - e.g. \"Optimize database queries\" (indexing vs rewrite vs caching)\n3. Code Modifications - e.g. \"Refactor auth module to support OAuth\"\n4. Architectural Decisions - e.g. \"Add WebSocket support\"\n5. Multi-File Changes - involves more than 2-3 files\n6. Unclear Requirements - need exploration to understand scope\n7. User Preferences Matter - if user input would materially change the implementation approach, use EnterPlanMode to structure the decision\n\nPermission mode notes:\n- EnterPlanMode enters plan mode automatically without an approval prompt in all permission modes.\n- In yolo and manual modes, ExitPlanMode still presents the plan to the user for approval.\n- In auto permission mode, do not use AskUserQuestion; make the best decision from available context.\n- In auto permission mode, ExitPlanMode exits plan mode without asking the user.\n- Use EnterPlanMode only when planning itself adds value.\n\nWhen NOT to use:\n- Single-line or few-line fixes (typos, obvious bugs, small tweaks)\n- User gave very specific, detailed instructions\n- Pure research/exploration tasks\n\n## What Happens in Plan Mode\nIn plan mode, you will:\n1. Identify 2-3 key questions about the codebase that are critical to your plan. If you are not confident about the codebase structure or relevant code paths, use `Agent(subagent_type=\"explore\")` to investigate these questions first - this is strongly recommended for non-trivial tasks.\n2. Explore the codebase using Glob, Grep, Read, and other read-only tools for any remaining quick lookups. Use Bash only when needed; Bash follows the normal permission mode and rules.\n3. Design an implementation approach based on your findings (or, for fusion plan, call FusionPlan to generate the synthesized plan).\n4. Review the generated plan, fill in any gaps, and ensure it matches the user's intent.\n5. Present your plan to the user via ExitPlanMode for approval\n\nFor fusion plan, the FusionPlan tool performs the parallel exploration and synthesis for you; you should still review the result before exiting plan mode.\n";
77814
+ var enter_plan_mode_default = "Use this tool proactively when you're about to start a non-trivial implementation task.\nGetting user sign-off on your approach via ExitPlanMode before writing code prevents wasted effort.\n\n## Planning Modes\n\n- **Normal plan**: You investigate the codebase, design a single approach, write it to the plan file, and present it for approval.\n- **Fusion plan**: Invoke the FusionPlan tool to spawn parallel planning subagents, each exploring a different angle, then synthesize their outputs into one plan. Fusion plan may take longer but tends to surface risks and alternatives you might miss.\n\n### When to choose which mode\n\nPrefer **normal plan** when:\n1. The user gave specific, detailed instructions.\n2. The change is small or localized (1-3 files, single concern).\n3. You are confident about the codebase structure and the right approach.\n4. Speed matters more than exploring alternatives.\n\nPrefer **fusion plan** when:\n1. The task is open-ended or ambiguous (e.g. \"improve performance\", \"refactor auth\").\n2. Multiple valid architectures or approaches exist.\n3. The change spans more than 3-5 files or touches core abstractions.\n4. You are unfamiliar with the relevant code paths and want parallel exploration.\n5. The user explicitly asked for a thorough plan or mentioned comparing options.\n\nIf unsure, choose normal plan for small fixes and fusion plan for larger design tasks.\n\n## When to Use\n\nUse this tool when ANY of these conditions apply:\n\n1. New Feature Implementation - e.g. \"Add a caching layer to the API\"\n2. Multiple Valid Approaches - e.g. \"Optimize database queries\" (indexing vs rewrite vs caching)\n3. Code Modifications - e.g. \"Refactor auth module to support OAuth\"\n4. Architectural Decisions - e.g. \"Add WebSocket support\"\n5. Multi-File Changes - involves more than 2-3 files\n6. Unclear Requirements - need exploration to understand scope\n7. User Preferences Matter - if user input would materially change the implementation approach, use EnterPlanMode to structure the decision\n\nPermission mode notes:\n- EnterPlanMode enters plan mode automatically without an approval prompt in all permission modes.\n- In yolo and manual modes, ExitPlanMode still presents the plan to the user for approval.\n- In auto permission mode, do not use AskUserQuestion; make the best decision from available context.\n- In auto permission mode, ExitPlanMode exits plan mode without asking the user.\n- Use EnterPlanMode only when planning itself adds value.\n\nWhen NOT to use:\n- Single-line or few-line fixes (typos, obvious bugs, small tweaks)\n- User gave very specific, detailed instructions\n- Pure research/exploration tasks\n\n## What Happens in Plan Mode\nIn plan mode, you will:\n1. Identify 2-3 key questions about the codebase that are critical to your plan. If you are not confident about the codebase structure or relevant code paths, use `Agent(subagent_type=\"explore\")` to investigate these questions first - this is strongly recommended for non-trivial tasks.\n2. Explore the codebase using Glob, Grep, Read, and other read-only tools for any remaining quick lookups. Use Bash only when needed; Bash follows the normal permission mode and rules.\n3. Design an implementation approach based on your findings (or, for fusion plan, call FusionPlan to generate the synthesized plan).\n4. Review the generated plan, fill in any gaps, and ensure it matches the user's intent.\n5. Present your plan to the user via ExitPlanMode for approval\n\nFor fusion plan, the FusionPlan tool performs the parallel exploration and synthesis for you; you should still review the result before exiting plan mode.\n";
77404
77815
  //#endregion
77405
77816
  //#region ../../packages/agent-core/src/tools/builtin/planning/enter-plan-mode.ts
77406
77817
  const EnterPlanModeInputSchema = z.object({ mode: z.enum(["normal", "fusion"]).default("normal").describe("Planning strategy. 'normal' (default): you design the plan. 'fusion': the host spawns parallel planning subagents and synthesizes a single plan for you to review. Use 'fusion' for ambiguous, large, or multi-approach tasks.") }).strict();
@@ -77485,7 +77896,7 @@ function enteredPlanModeMessage(mode, planPath) {
77485
77896
  }
77486
77897
  //#endregion
77487
77898
  //#region ../../packages/agent-core/src/tools/builtin/planning/exit-plan-mode.md
77488
- var exit_plan_mode_default = "Use this tool when you are in plan mode and have finished writing your plan to the plan file and are ready for user approval.\n\n## How This Tool Works\n- You should have already written your plan to the plan file specified in the plan mode reminder.\n- This tool does NOT take the plan content as a parameter - it reads the plan from the file you wrote.\n- The user will see the contents of your plan file when they review it. In auto permission mode, the tool reads the file and exits plan mode without asking the user.\n\n## When to Use\nOnly use this tool for tasks that require planning implementation steps. For research tasks (searching files, reading code, understanding the codebase), do NOT use this tool.\n\n## Multiple Approaches\nIf your plan contains multiple alternative approaches:\n- Pass them via the `options` parameter so the user can choose which approach to execute.\n- Each option should have a concise label and a brief description of trade-offs.\n- If you recommend one option, append \"(Recommended)\" to its label.\n- In yolo and manual modes, the user will see all options alongside Reject and Revise choices.\n- Provide up to 3 options; the host adds the standard rejection and revision controls. When the plan offers a real choice, 2-3 distinct approaches work best.\n- Passing a single option is allowed and is equivalent to a plain plan approval (no approach choice is surfaced to the user).\n- Do NOT use \"Reject\", \"Reject and Exit\", \"Revise\", or \"Approve\" as option labels - these are reserved by the system.\n\n## Before Using\n- In auto permission mode, do NOT use AskUserQuestion; make the best decision from available context.\n- In auto permission mode, this tool exits plan mode without asking the user.\n- In yolo and manual modes, this tool still presents the plan to the user for approval.\n- If auto permission mode is not active and you have unresolved questions, use AskUserQuestion first.\n- If auto permission mode is not active and you have multiple approaches and haven't narrowed down yet, consider using AskUserQuestion first to let the user choose, then write a plan for the chosen approach only.\n- Once your plan is finalized, use THIS tool to request approval.\n- Do NOT use AskUserQuestion to ask \"Is this plan OK?\" or \"Should I proceed?\" - that is exactly what ExitPlanMode does.\n- If rejected, revise based on feedback and call ExitPlanMode again.\n";
77899
+ var exit_plan_mode_default = "Use this tool when you are in plan mode and have finished writing your plan to the plan file and are ready for user approval.\n\n## How This Tool Works\n- You should have already written your plan to the plan file specified in the plan mode reminder.\n- This tool does NOT take the plan content as a parameter - it reads the plan from the file you wrote.\n- The user will see the contents of your plan file when they review it. In auto permission mode, the tool reads the file and exits plan mode without asking the user.\n\n## When to Use\nOnly use this tool for tasks that require planning implementation steps. For research tasks (searching files, reading code, understanding the codebase), do NOT use this tool.\n\n## Multiple Approaches\nIf your plan contains multiple alternative approaches:\n- If you recommend one option, append \"(Recommended)\" to its label.\n- In yolo and manual modes, the user will see all options alongside Reject and Revise choices.\n- Passing a single option is allowed and is equivalent to a plain plan approval (no approach choice is surfaced to the user).\n\n## Before Using\n- In auto permission mode, do NOT use AskUserQuestion; make the best decision from available context.\n- In auto permission mode, this tool exits plan mode without asking the user.\n- In yolo and manual modes, this tool still presents the plan to the user for approval.\n- If auto permission mode is not active and you have unresolved questions, use AskUserQuestion first.\n- If auto permission mode is not active and you have multiple approaches and haven't narrowed down yet, consider using AskUserQuestion first to let the user choose, then write a plan for the chosen approach only.\n- Once your plan is finalized, use THIS tool to request approval.\n- Do NOT use AskUserQuestion to ask \"Is this plan OK?\" or \"Should I proceed?\" - that is exactly what ExitPlanMode does.\n- If rejected, revise based on feedback and call ExitPlanMode again.\n";
77489
77900
  //#endregion
77490
77901
  //#region ../../packages/agent-core/src/tools/builtin/planning/exit-plan-mode.ts
77491
77902
  const RESERVED_OPTION_LABELS = new Set([
@@ -77814,6 +78225,30 @@ function truncateUtf8$1(input, maxBytes) {
77814
78225
  var bash_default = "Execute a `{{ SHELL_NAME }}` command. Use this for shell semantics — pipes, env, processes, git, package managers, build/test runners, anything genuinely interactive or multi-step.\n\n**Translate these to a dedicated tool instead:**\n- `cat` / `head` / `tail` (known path) → `Read`\n- `sed` / `awk` (in-place edit) → `Edit`\n- `echo > file` / `cat <<EOF` → `Write`\n- `find` / recursive `ls` to locate files by name pattern → `Glob` (plain `ls <known-directory>` is fine for listing a directory)\n- `grep` / `rg` (search file contents) → `Grep`\n- `echo` / `printf` (talk to the user) → just output text directly\n\nThe dedicated tools render in the per-tool permission UI and keep raw stdout out of the conversation; that is why they are worth reaching for whenever one fits.\n\n**Output:**\nThe stdout and stderr will be combined and returned as a string. The output may be truncated if it is too long. If the command failed, the output will end with a `Command failed with exit code: N` line stating the non-zero exit code.\n\nIf `run_in_background=true`, the command will be started as a background task and this tool will return a task ID instead of waiting for command completion. When doing that, you must provide a short `description`. Background commands default to a {{ DEFAULT_BACKGROUND_TIMEOUT_S }}s timeout and `timeout` is capped at {{ MAX_BACKGROUND_TIMEOUT_S }}s; set `disable_timeout=true` only when the task should run without a timeout. You will be automatically notified when the task completes. Use `TaskOutput` for a non-blocking status/output snapshot, and only set `block=true` when you explicitly want to wait for completion. Use `TaskStop` only if the task must be cancelled. If a human user wants to inspect background tasks themselves, point them to the `/tasks` command, which opens an interactive panel; it has no subcommands.\n\n**Guidelines for safety and security:**\n- Each shell tool call will be executed in a fresh shell environment. The shell variables, current working directory changes, and the shell history is not preserved between calls.\n- The tool call will return after the command is finished. You shall not use this tool to execute an interactive command or a command that may run forever. For possibly long-running foreground commands, set the `timeout` argument in seconds. Foreground commands default to {{ DEFAULT_TIMEOUT_S }}s and allow up to {{ MAX_TIMEOUT_S }}s.\n- Avoid using `..` to access files or directories outside of the working directory.\n- Avoid modifying files outside of the working directory unless explicitly instructed to do so.\n- Never run commands that require superuser privileges unless explicitly instructed to do so.\n\n**Guidelines for efficiency:**\n- For multiple related commands, use `&&` to chain them in a single call, e.g. `cd /path && ls -la`\n- Use `;` to run commands sequentially regardless of success/failure\n- Use `||` for conditional execution (run second command only if first fails)\n- Use pipe operations (`|`) and redirections (`>`, `>>`) to chain input and output between commands\n- Always quote file paths containing spaces with double quotes (e.g., cd \"/path with spaces/\")\n- Compose multi-step logic in a single call with `if` / `case` / `for` / `while` control flows.\n- Prefer `run_in_background=true` for long-running builds, tests, watchers, or servers when you need the conversation to continue before the command finishes.\n\n**Commands available:**\nThe following common command categories are usually available. Availability still depends on the host, so when in doubt run `which <command>` first to confirm a command exists before relying on it.\n- Navigation and inspection: `ls`, `pwd`, `cd`, `stat`, `file`, `du`, `df`, `tree`\n- File and directory management: `cp`, `mv`, `rm`, `mkdir`, `touch`, `ln`, `chmod`, `chown`\n- Text and data processing: `wc`, `sort`, `uniq`, `cut`, `tr`, `diff`, `xargs`\n- Archives and compression: `tar`, `gzip`, `gunzip`, `zip`, `unzip`\n- Networking and transfer: `curl`, `wget`, `ping`, `ssh`, `scp`\n- Version control: `git`\n- Process and system: `ps`, `kill`, `top`, `env`, `date`, `uname`, `whoami`\n- Language and package toolchains: `node`, `npm`, `pnpm`, `yarn`, `python`, `pip` (use whichever the project actually relies on)\n";
77815
78226
  //#endregion
77816
78227
  //#region ../../packages/agent-core/src/tools/builtin/shell/bash.ts
78228
+ /**
78229
+ * BashTool — execute shell commands.
78230
+ *
78231
+ * Invokes bash (POSIX) according to an injected `Environment`. On Windows
78232
+ * the shell is Git Bash; the path is resolved by `detectEnvironment`.
78233
+ *
78234
+ * Dependencies injected via constructor:
78235
+ * - `Jian` — shell execution abstraction (exec / execWithEnv)
78236
+ * - `cwd` — default working directory for commands
78237
+ * - `Environment` — cross-platform probe (shellName / shellPath)
78238
+ * - `BackgroundProcessManager?` — optional: required iff run_in_background=true
78239
+ *
78240
+ * Execution goes through Jian, never directly via node:child_process.
78241
+ *
78242
+ * Hardening:
78243
+ * - `args.timeout` (seconds) and the ambient `signal` both drive
78244
+ * `Promise.race`; fire-a-kill on either edge.
78245
+ * - stdin is closed immediately so interactive commands (`cat`, `read`,
78246
+ * `python -c 'input()'`) receive EOF instead of hanging.
78247
+ * - Two-phase kill: SIGTERM → 5s grace → SIGKILL (Jian honours this
78248
+ * contract cross-platform).
78249
+ * - stdout/stderr stream into ToolResultBuilder; excess is replaced with a
78250
+ * truncation marker so a runaway command cannot OOM the host.
78251
+ */
77817
78252
  const MS_PER_SECOND = 1e3;
77818
78253
  const DEFAULT_TIMEOUT_S = 60;
77819
78254
  const MAX_TIMEOUT_S = 300;
@@ -78064,7 +78499,11 @@ var BashTool = class {
78064
78499
  killProc();
78065
78500
  }, timeoutMs);
78066
78501
  try {
78067
- const builder = new ToolResultBuilder();
78502
+ const builder = new ToolResultBuilder({ artifactSink: async (fullOutput) => {
78503
+ const artifactPath = join(tmpdir(), `scream-bash-output-${randomUUID()}.log`);
78504
+ await writeFile(artifactPath, fullOutput, "utf8");
78505
+ return artifactPath;
78506
+ } });
78068
78507
  const [, exitCode] = await Promise.all([Promise.all([readStreamIntoBuilder(proc.stdout, builder), readStreamIntoBuilder(proc.stderr, builder)]), proc.wait()]);
78069
78508
  if (timedOut) {
78070
78509
  const timeoutLabel = timeoutMs % 1e3 === 0 ? `${String(timeoutMs / 1e3)}s` : `${String(timeoutMs)}ms`;
@@ -79655,7 +80094,8 @@ const DEFAULT_CONFIG = {
79655
80094
  minContentTokens: 100,
79656
80095
  minContextUsageRatio: .5,
79657
80096
  truncatedMarker: "[Old tool result content cleared]",
79658
- uselessMarker: "[Uneventful result elided]"
80097
+ uselessMarker: "[Uneventful result elided]",
80098
+ noMatchesMarker: "[no matches]"
79659
80099
  };
79660
80100
  /**
79661
80101
  * Compute the cutoff index: everything at index < cutoff is eligible for
@@ -79676,30 +80116,107 @@ function computeCutoff(messages, config) {
79676
80116
  }
79677
80117
  return cutoff;
79678
80118
  }
80119
+ /** Tool names whose results are file reads eligible for supersede pruning. */
80120
+ const READ_TOOL_NAMES = new Set(["Read", "ReadGroup"]);
80121
+ /** Tool names whose empty results can be elided as a no-match marker. */
80122
+ const SEARCH_TOOL_NAMES = new Set(["Grep", "Glob"]);
80123
+ /** Exact tool-result texts that indicate a zero-match search result. */
80124
+ const ZERO_MATCH_TEXTS = new Set(["No matches found", "No non-sensitive matches found"]);
80125
+ /**
80126
+ * Parse a tool call's arguments JSON. Returns undefined for null or malformed
80127
+ * JSON - persisted history can carry truncated arguments that must not crash
80128
+ * compaction. `ToolCall.arguments` is a JSON string (or null), never a parsed
80129
+ * object, so every consumer must go through this helper.
80130
+ */
80131
+ function parseToolCallArguments$1(argumentsJson) {
80132
+ if (argumentsJson === null || argumentsJson === void 0) return void 0;
80133
+ try {
80134
+ const parsed = JSON.parse(argumentsJson);
80135
+ return typeof parsed === "object" && parsed !== null ? parsed : void 0;
80136
+ } catch {
80137
+ return;
80138
+ }
80139
+ }
79679
80140
  /**
79680
- * Walk the message list and find Read tool calls whose file paths were
79681
- * superseded by a later Read of the same path. Returns a map from the
79682
- * superseded tool call's ID to the file path (for the marker text).
80141
+ * Extract the file paths targeted by a read tool call. `Read` carries a
80142
+ * single `path`; `ReadGroup` carries a `paths` array. Returns an empty array
80143
+ * for non-read tools or calls whose arguments don't yield usable paths.
80144
+ */
80145
+ function extractReadFilePaths(name, args) {
80146
+ if (args === void 0) return [];
80147
+ if (name === "Read") {
80148
+ const path = typeof args["path"] === "string" ? args["path"] : void 0;
80149
+ return path !== void 0 && path.length > 0 ? [path] : [];
80150
+ }
80151
+ if (name === "ReadGroup") {
80152
+ const paths = args["paths"];
80153
+ if (!Array.isArray(paths)) return [];
80154
+ return paths.filter((p) => typeof p === "string" && p.length > 0);
80155
+ }
80156
+ return [];
80157
+ }
80158
+ /** Concatenate the `text` parts of a message's content into a single string. */
80159
+ function extractTextContent(content) {
80160
+ let text = "";
80161
+ for (const part of content) if (typeof part === "object" && part !== null && part.type === "text") text += part.text;
80162
+ return text;
80163
+ }
80164
+ /** Build a toolCallId -> tool-name map by scanning assistant messages. */
80165
+ function buildToolCallNameMap(messages) {
80166
+ const names = /* @__PURE__ */ new Map();
80167
+ for (const msg of messages) {
80168
+ if (msg.role !== "assistant") continue;
80169
+ for (const tc of msg.toolCalls) names.set(tc.id, tc.name);
80170
+ }
80171
+ return names;
80172
+ }
80173
+ /**
80174
+ * Whether a tool result is a zero-match Grep/Glob result eligible for elision.
80175
+ * Only exact-match the canonical empty-result texts so results carrying extra
80176
+ * information (sensitive-file filter notices, pagination notices, errors) are
80177
+ * preserved verbatim.
80178
+ */
80179
+ function isZeroMatchSearchResult(toolName, content) {
80180
+ if (toolName === void 0 || !SEARCH_TOOL_NAMES.has(toolName)) return false;
80181
+ const text = extractTextContent(content).trim();
80182
+ return ZERO_MATCH_TEXTS.has(text);
80183
+ }
80184
+ /**
80185
+ * Walk the message list and find Read/ReadGroup tool calls whose file paths
80186
+ * were superseded by a later read of the same path. Returns a map from the
80187
+ * superseded tool call's ID to the list of file paths covered by the newer
80188
+ * read (a ReadGroup can cover several).
79683
80189
  *
79684
- * Only considers tool results before the cutoff line newer reads are
79685
- * protected and their results are kept verbatim.
80190
+ * Only considers tool results before the cutoff line - newer reads are
80191
+ * protected and their results are kept verbatim. The comparison uses the raw
80192
+ * path strings from the tool arguments; path canonicalization would need
80193
+ * workspace context the compaction layer doesn't have, so the same file read
80194
+ * via two different spellings is treated as two different files (safe: it
80195
+ * just misses a supersede opportunity rather than dropping a distinct result).
79686
80196
  */
79687
80197
  function findSupersededPaths(messages, cutoff) {
79688
80198
  const superseded = /* @__PURE__ */ new Map();
79689
- const readCalls = /* @__PURE__ */ new Map();
80199
+ const latestReadByPath = /* @__PURE__ */ new Map();
79690
80200
  for (let i = 0; i < messages.length; i++) {
79691
80201
  const msg = messages[i];
79692
80202
  if (msg === void 0) continue;
79693
- if (msg.role === "assistant" && msg.toolCalls.length > 0) {
79694
- for (const tc of msg.toolCalls) if (tc.name === "Read" && tc.id !== void 0 && tc.arguments !== void 0) {
79695
- const filePath = typeof tc.arguments === "object" && tc.arguments !== null ? tc.arguments["file_path"] : void 0;
79696
- if (filePath !== void 0) {
79697
- for (const [prevId, prev] of readCalls) if (prev.filePath === filePath && prev.index < cutoff) superseded.set(prevId, filePath);
79698
- readCalls.set(tc.id, {
79699
- filePath,
79700
- index: i
79701
- });
79702
- }
80203
+ if (msg.role !== "assistant" || msg.toolCalls.length === 0) continue;
80204
+ for (const tc of msg.toolCalls) {
80205
+ if (!READ_TOOL_NAMES.has(tc.name)) continue;
80206
+ const args = parseToolCallArguments$1(tc.arguments);
80207
+ const paths = extractReadFilePaths(tc.name, args);
80208
+ if (paths.length === 0) continue;
80209
+ for (const filePath of paths) {
80210
+ const prev = latestReadByPath.get(filePath);
80211
+ if (prev !== void 0 && prev.index < cutoff) {
80212
+ const existing = superseded.get(prev.toolCallId);
80213
+ if (existing === void 0) superseded.set(prev.toolCallId, [filePath]);
80214
+ else if (!existing.includes(filePath)) superseded.set(prev.toolCallId, [...existing, filePath]);
80215
+ }
80216
+ latestReadByPath.set(filePath, {
80217
+ toolCallId: tc.id,
80218
+ index: i
80219
+ });
79703
80220
  }
79704
80221
  }
79705
80222
  }
@@ -79748,25 +80265,33 @@ var MicroCompaction = class {
79748
80265
  const nextCutoff = computeCutoff(history, config);
79749
80266
  if (nextCutoff <= this.cutoff) return;
79750
80267
  const { beforeTokens, afterTokens } = this.measureEffect(history, nextCutoff);
79751
- if (beforeTokens - afterTokens < config.pruneMinReclaimTokens) return;
80268
+ const reclaimTokens = beforeTokens - afterTokens;
80269
+ if (reclaimTokens < config.pruneMinReclaimTokens) return;
80270
+ if (estimateTokensForMessages(history.slice(nextCutoff)) * 1.15 > reclaimTokens) return;
79752
80271
  this.apply(nextCutoff);
79753
80272
  }
79754
80273
  /**
79755
80274
  * Apply micro-compaction to a message list: replace old tool results
79756
- * before the cutoff line with truncated markers. Read results for files
79757
- * that were re-read later get a supersede marker so the model knows
79758
- * the old content is stale. Tool results explicitly marked useless are
79759
- * elided with a short notice regardless of size, since they carry no
79760
- * actionable information.
80275
+ * before the cutoff line with truncated markers. Read/ReadGroup results
80276
+ * for files that were re-read later get a supersede marker (listing the
80277
+ * covered paths) so the model knows the old content is stale. Zero-match
80278
+ * Grep/Glob results are elided to a short `[no matches]` notice regardless
80279
+ * of size. Tool results explicitly marked useless are elided with a short
80280
+ * notice regardless of size, since they carry no actionable information.
79761
80281
  */
79762
80282
  compact(messages) {
79763
80283
  const config = this.config;
79764
80284
  const superseded = findSupersededPaths(messages, this.cutoff);
80285
+ const toolNames = buildToolCallNameMap(messages);
79765
80286
  const result = [];
79766
80287
  let i = 0;
79767
80288
  for (const msg of messages) {
79768
- const isUseless = i < this.cutoff && msg.role === "tool" && msg.toolCallId !== void 0 && msg.useless === true;
79769
- const isOversizedTruncatable = i < this.cutoff && msg.role === "tool" && msg.toolCallId !== void 0 && estimateTokensForMessages([msg]) >= config.minContentTokens;
80289
+ const isOld = i < this.cutoff;
80290
+ const toolCallId = msg.toolCallId;
80291
+ const isTool = msg.role === "tool" && toolCallId !== void 0;
80292
+ const isUseless = isOld && isTool && msg.useless === true;
80293
+ const isZeroMatch = isOld && isTool && toolCallId !== void 0 && isZeroMatchSearchResult(toolNames.get(toolCallId), msg.content);
80294
+ const isOversizedTruncatable = isOld && isTool && estimateTokensForMessages([msg]) >= config.minContentTokens;
79770
80295
  if (isUseless) result.push({
79771
80296
  ...msg,
79772
80297
  content: [{
@@ -79774,8 +80299,16 @@ var MicroCompaction = class {
79774
80299
  text: config.uselessMarker
79775
80300
  }]
79776
80301
  });
80302
+ else if (isZeroMatch) result.push({
80303
+ ...msg,
80304
+ content: [{
80305
+ type: "text",
80306
+ text: config.noMatchesMarker
80307
+ }]
80308
+ });
79777
80309
  else if (isOversizedTruncatable) {
79778
- const marker = msg.toolCallId !== void 0 && superseded.has(msg.toolCallId) ? `[Superseded by a newer read of ${superseded.get(msg.toolCallId)}]` : config.truncatedMarker;
80310
+ const paths = toolCallId !== void 0 ? superseded.get(toolCallId) : void 0;
80311
+ const marker = paths !== void 0 && paths.length > 0 ? `[Superseded by a newer read of ${paths.join(", ")}]` : config.truncatedMarker;
79779
80312
  result.push({
79780
80313
  ...msg,
79781
80314
  content: [{
@@ -79800,20 +80333,28 @@ var MicroCompaction = class {
79800
80333
  measureEffect(messages, cutoff) {
79801
80334
  let markerTokenCount;
79802
80335
  let uselessMarkerTokenCount;
80336
+ let noMatchesMarkerTokenCount;
79803
80337
  let truncatedToolResultCount = 0;
79804
80338
  let beforeTokens = 0;
79805
80339
  let afterTokens = 0;
80340
+ const toolNames = buildToolCallNameMap(messages);
79806
80341
  for (let i = 0; i < messages.length && i < cutoff; i++) {
79807
80342
  const message = messages[i];
79808
80343
  if (message?.role !== "tool" || message.toolCallId === void 0) continue;
79809
80344
  const contentTokens = estimateTokensForMessages([message]);
79810
80345
  const isUseless = message.useless === true;
79811
- if (!isUseless && contentTokens < this.config.minContentTokens) continue;
80346
+ const isZeroMatch = isZeroMatchSearchResult(toolNames.get(message.toolCallId), message.content);
80347
+ if (!isUseless && !isZeroMatch && contentTokens < this.config.minContentTokens) continue;
79812
80348
  if (isUseless) {
79813
80349
  uselessMarkerTokenCount ??= estimateTokens$1(this.config.uselessMarker);
79814
80350
  truncatedToolResultCount += 1;
79815
80351
  beforeTokens += contentTokens;
79816
80352
  afterTokens += uselessMarkerTokenCount;
80353
+ } else if (isZeroMatch) {
80354
+ noMatchesMarkerTokenCount ??= estimateTokens$1(this.config.noMatchesMarker);
80355
+ truncatedToolResultCount += 1;
80356
+ beforeTokens += contentTokens;
80357
+ afterTokens += noMatchesMarkerTokenCount;
79817
80358
  } else {
79818
80359
  markerTokenCount ??= estimateTokens$1(this.config.truncatedMarker);
79819
80360
  truncatedToolResultCount += 1;
@@ -80822,6 +81363,71 @@ var ConfigState = class {
80822
81363
  }
80823
81364
  };
80824
81365
  //#endregion
81366
+ //#region ../../packages/agent-core/src/agent/context/prefix-fingerprint.ts
81367
+ /**
81368
+ * Deterministic non-crypto string hash (djb2). Fast and sufficient for change
81369
+ * detection; not used for any security purpose. Returns a compact base36
81370
+ * string so an array of fingerprints stays small.
81371
+ */
81372
+ function hashString(input) {
81373
+ let hash = 5381;
81374
+ for (let i = 0; i < input.length; i++) hash = (hash << 5) + hash + (input.codePointAt(i) ?? 0) | 0;
81375
+ return (hash >>> 0).toString(36);
81376
+ }
81377
+ /**
81378
+ * Stable JSON serialization that sorts object keys so key insertion order
81379
+ * can't affect the fingerprint. Only used for media parts whose payload is
81380
+ * already a stable base64/url string; text/think parts have a dedicated
81381
+ * fast path.
81382
+ */
81383
+ function stableJson(value) {
81384
+ if (value === null || typeof value !== "object") return JSON.stringify(value);
81385
+ if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
81386
+ const obj = value;
81387
+ return `{${Object.keys(obj).toSorted().map((k) => `${JSON.stringify(k)}:${stableJson(obj[k])}`).join(",")}}`;
81388
+ }
81389
+ function serializeContentPart(part) {
81390
+ switch (part.type) {
81391
+ case "text": return `text:${part.text}`;
81392
+ case "think": return `think:${part.think}${part.encrypted !== void 0 ? `\u0003${part.encrypted}` : ""}`;
81393
+ default: return `${part.type}:${stableJson(part)}`;
81394
+ }
81395
+ }
81396
+ function serializeToolCall(tc) {
81397
+ return `function|${tc.id}|${tc.name}|${tc.arguments ?? ""}`;
81398
+ }
81399
+ /**
81400
+ * Serialize a message to a deterministic string covering all
81401
+ * provider-visible bytes. Two messages with the same serialization produce
81402
+ * identical provider bytes (modulo serialization the provider adapter
81403
+ * normalizes). Separator bytes (\x00-\x03) are used so concatenated fields
81404
+ * can't alias.
81405
+ */
81406
+ function serializeMessage$1(message) {
81407
+ const content = message.content.map(serializeContentPart).join("");
81408
+ const toolCalls = message.toolCalls.map(serializeToolCall).join("");
81409
+ return `${message.role}\u0000${message.name ?? ""}\u0000${message.toolCallId ?? ""}\u0000${content}\u0000${toolCalls}`;
81410
+ }
81411
+ /** Per-message fingerprint. Equal fingerprints => equal provider bytes. */
81412
+ function messageFingerprint(message) {
81413
+ return hashString(serializeMessage$1(message));
81414
+ }
81415
+ /**
81416
+ * Longest common prefix length by provider-visible bytes. This is the number
81417
+ * of leading messages a provider prompt cache could reuse from the previous
81418
+ * call. When this is less than the previous call's message count, an early
81419
+ * message mutated and the cache broke from that index.
81420
+ *
81421
+ * `prev` is the array of per-message fingerprints captured last call;
81422
+ * `current` is the live messages this call.
81423
+ */
81424
+ function stablePrefixLength(prev, current) {
81425
+ const n = Math.min(prev.length, current.length);
81426
+ let i = 0;
81427
+ for (; i < n; i++) if (prev[i] !== messageFingerprint(current[i])) break;
81428
+ return i;
81429
+ }
81430
+ //#endregion
80825
81431
  //#region ../../packages/agent-core/src/agent/context/types.ts
80826
81432
  const USER_PROMPT_ORIGIN = { kind: "user" };
80827
81433
  //#endregion
@@ -80844,6 +81450,18 @@ var ContextMemory = class {
80844
81450
  openSteps = /* @__PURE__ */ new Map();
80845
81451
  pendingToolResultIds = /* @__PURE__ */ new Set();
80846
81452
  deferredMessages = [];
81453
+ /**
81454
+ * Per-message fingerprints captured from the last message list handed to
81455
+ * the LLM via {@link messagesForLLM}. Used to measure prefix stability
81456
+ * across calls: a provider prompt cache only hits when the leading
81457
+ * messages are byte-identical to the previous request, so the length of
81458
+ * the matching prefix here approximates the cacheable prefix length.
81459
+ *
81460
+ * Reset on {@link clear}; a compaction naturally produces a 0-length
81461
+ * stable prefix (the summary replaces the head), which is the correct
81462
+ * cache-break signal rather than a reset.
81463
+ */
81464
+ lastSentFingerprints = [];
80847
81465
  constructor(agent) {
80848
81466
  this.agent = agent;
80849
81467
  }
@@ -80893,6 +81511,7 @@ var ContextMemory = class {
80893
81511
  this.openSteps.clear();
80894
81512
  this.pendingToolResultIds.clear();
80895
81513
  this.deferredMessages = [];
81514
+ this.lastSentFingerprints = [];
80896
81515
  this.agent.injection.onContextClear();
80897
81516
  this.agent.emitStatusUpdated();
80898
81517
  }
@@ -80937,6 +81556,19 @@ var ContextMemory = class {
80937
81556
  }
80938
81557
  if (!this.agent.records.restoring && (stoppedAtBoundary || removedUserCount < count)) {}
80939
81558
  }
81559
+ /**
81560
+ * Apply a full compaction summary.
81561
+ *
81562
+ * Prefix-stability note: this is a **replaceHead** operation, not a
81563
+ * replaceTail. The first `compactedCount` messages are collapsed into a
81564
+ * single summary message; the trailing recent messages are preserved
81565
+ * verbatim. This necessarily breaks the provider prompt cache for the
81566
+ * whole prefix (the summary is new content), which is inherent to
81567
+ * summarization and cannot be avoided. After compaction the new prefix
81568
+ * `[summary, ...tail]` is stable again until the next compaction or
81569
+ * micro-compaction cutoff advance, so subsequent append-only steps resume
81570
+ * hitting the cache.
81571
+ */
80940
81572
  applyCompaction(summary) {
80941
81573
  this.agent.records.logRecord({
80942
81574
  type: "context.apply_compaction",
@@ -80980,6 +81612,48 @@ var ContextMemory = class {
80980
81612
  this.agent.microCompaction.detect();
80981
81613
  return project(this.agent.microCompaction.compact(this.history));
80982
81614
  }
81615
+ /**
81616
+ * Build the message list for an LLM call, with prefix-stability
81617
+ * observation.
81618
+ *
81619
+ * This is the LLM-bound counterpart of the {@link messages} getter: it
81620
+ * runs the same detect + compact + project pipeline, then fingerprints
81621
+ * the result and logs how much of the prefix survived since the last
81622
+ * call. A stable prefix length equal to the previous message count means
81623
+ * the provider prompt cache should hit; a smaller value means an early
81624
+ * message mutated (compaction summary, micro-compaction truncation, or a
81625
+ * projection repair) and the cache broke from that index.
81626
+ *
81627
+ * Behavior is otherwise identical to the getter - this is observation
81628
+ * only, it does not alter the messages returned.
81629
+ */
81630
+ messagesForLLM() {
81631
+ this.agent.microCompaction.detect();
81632
+ const messages = project(this.agent.microCompaction.compact(this.history));
81633
+ this.observePrefixStability(messages);
81634
+ return messages;
81635
+ }
81636
+ /**
81637
+ * Compare the projected messages against the last LLM-bound batch and
81638
+ * log the stable-prefix length. Pure observation: no state that affects
81639
+ * message content is mutated, only the fingerprint baseline used by the
81640
+ * next call's comparison.
81641
+ */
81642
+ observePrefixStability(messages) {
81643
+ const prev = this.lastSentFingerprints;
81644
+ const stable = stablePrefixLength(prev, messages);
81645
+ this.lastSentFingerprints = messages.map(messageFingerprint);
81646
+ if (prev.length === 0) return;
81647
+ const appended = messages.length - prev.length;
81648
+ if (stable >= prev.length) return;
81649
+ this.agent.log.debug("prefix-stability: provider prompt cache prefix broke", {
81650
+ stablePrefixLength: stable,
81651
+ prevMessageCount: prev.length,
81652
+ currentMessageCount: messages.length,
81653
+ appendedSinceLast: appended,
81654
+ breakIndex: stable
81655
+ });
81656
+ }
80983
81657
  appendLoopEvent(event) {
80984
81658
  this.agent.records.logRecord({
80985
81659
  type: "context.append_loop_event",
@@ -81089,22 +81763,38 @@ function toolResultOutputForModel(result) {
81089
81763
  }, ...truncateContentParts(output)];
81090
81764
  return truncateContentParts(output);
81091
81765
  }
81092
- /** Truncate a plain-text tool output that exceeds MAX_TOOL_RESULT_TOKENS. */
81766
+ /** Truncate a plain-text tool output that exceeds MAX_TOOL_RESULT_TOKENS.
81767
+ * Tail-biased: keeps 25% head + 75% tail so error messages and test
81768
+ * failures (usually at the end) survive truncation. */
81093
81769
  function truncateToolOutput(text) {
81094
81770
  if (estimateTokens$1(text) <= MAX_TOOL_RESULT_TOKENS) return text;
81095
81771
  const budget = MAX_TOOL_RESULT_TOKENS - estimateTokens$1(TOOL_TRUNCATION_NOTICE);
81096
81772
  if (budget <= 0) return TOOL_TRUNCATION_NOTICE.trim();
81097
- let kept = "";
81098
- let tokens = 0;
81773
+ const headBudget = Math.floor(budget * .25);
81774
+ const tailBudget = budget - headBudget;
81775
+ let head = "";
81776
+ let headTokens = 0;
81099
81777
  for (const ch of text) {
81100
81778
  const chTokens = ch.codePointAt(0) <= 127 ? 1 / 4 : 1;
81101
- if (tokens + chTokens > budget) break;
81102
- kept += ch;
81103
- tokens += chTokens;
81779
+ if (headTokens + chTokens > headBudget) break;
81780
+ head += ch;
81781
+ headTokens += chTokens;
81782
+ }
81783
+ const reversed = [...text].toReversed();
81784
+ const tailChars = [];
81785
+ let tailTokens = 0;
81786
+ for (const ch of reversed) {
81787
+ const chTokens = ch.codePointAt(0) <= 127 ? 1 / 4 : 1;
81788
+ if (tailTokens + chTokens > tailBudget) break;
81789
+ tailChars.push(ch);
81790
+ tailTokens += chTokens;
81104
81791
  }
81105
- return kept + TOOL_TRUNCATION_NOTICE;
81792
+ const tail = tailChars.toReversed().join("");
81793
+ const notice = `\n[content truncated - ~${Math.max(0, Math.round(estimateTokens$1(text) - headTokens - tailTokens))} tokens omitted]\n`;
81794
+ return head + notice + tail;
81106
81795
  }
81107
- /** Truncate oversized text parts in a ContentPart array. */
81796
+ /** Truncate oversized text parts in a ContentPart array.
81797
+ * Tail-biased: keeps 25% head + 75% tail. */
81108
81798
  function truncateContentParts(parts) {
81109
81799
  let totalTokens = 0;
81110
81800
  for (const p of parts) if (p.type === "text") totalTokens += estimateTokens$1(p.text);
@@ -81114,39 +81804,83 @@ function truncateContentParts(parts) {
81114
81804
  type: "text",
81115
81805
  text: TOOL_TRUNCATION_NOTICE.trim()
81116
81806
  }];
81117
- const result = [];
81118
- let used = 0;
81119
- for (const p of parts) {
81807
+ const headBudget = Math.floor(budget * .25);
81808
+ const tailBudget = budget - headBudget;
81809
+ const headParts = [];
81810
+ let headUsed = 0;
81811
+ let headEnd = 0;
81812
+ for (let i = 0; i < parts.length; i++) {
81813
+ const p = parts[i];
81120
81814
  if (p.type !== "text") {
81121
- result.push(p);
81815
+ headParts.push(p);
81816
+ headEnd = i + 1;
81122
81817
  continue;
81123
81818
  }
81124
81819
  const partTokens = estimateTokens$1(p.text);
81125
- if (used + partTokens <= budget) {
81126
- result.push(p);
81127
- used += partTokens;
81820
+ if (headUsed + partTokens <= headBudget) {
81821
+ headParts.push(p);
81822
+ headUsed += partTokens;
81823
+ headEnd = i + 1;
81128
81824
  } else {
81129
- const remaining = budget - used;
81130
- let kept = "";
81131
- let t = 0;
81132
- for (const ch of p.text) {
81133
- const chTokens = ch.codePointAt(0) <= 127 ? 1 / 4 : 1;
81134
- if (t + chTokens > remaining) break;
81135
- kept += ch;
81136
- t += chTokens;
81137
- }
81138
- if (kept.length > 0) result.push({
81139
- type: "text",
81140
- text: kept
81141
- });
81825
+ const remaining = headBudget - headUsed;
81826
+ if (remaining > 0) {
81827
+ let kept = "";
81828
+ let t = 0;
81829
+ for (const ch of p.text) {
81830
+ const chTokens = ch.codePointAt(0) <= 127 ? 1 / 4 : 1;
81831
+ if (t + chTokens > remaining) break;
81832
+ kept += ch;
81833
+ t += chTokens;
81834
+ }
81835
+ if (kept.length > 0) headParts.push({
81836
+ type: "text",
81837
+ text: kept
81838
+ });
81839
+ }
81142
81840
  break;
81143
81841
  }
81144
81842
  }
81145
- result.push({
81146
- type: "text",
81147
- text: TOOL_TRUNCATION_NOTICE
81148
- });
81149
- return result;
81843
+ const tailParts = [];
81844
+ let tailUsed = 0;
81845
+ for (let i = parts.length - 1; i >= headEnd; i--) {
81846
+ const p = parts[i];
81847
+ if (p.type !== "text") {
81848
+ tailParts.unshift(p);
81849
+ continue;
81850
+ }
81851
+ const partTokens = estimateTokens$1(p.text);
81852
+ if (tailUsed + partTokens <= tailBudget) {
81853
+ tailParts.unshift(p);
81854
+ tailUsed += partTokens;
81855
+ } else {
81856
+ const remaining = tailBudget - tailUsed;
81857
+ if (remaining > 0) {
81858
+ const chars = [...p.text];
81859
+ let kept = "";
81860
+ let t = 0;
81861
+ for (let j = chars.length - 1; j >= 0; j--) {
81862
+ const chTokens = chars[j].codePointAt(0) <= 127 ? 1 / 4 : 1;
81863
+ if (t + chTokens > remaining) break;
81864
+ kept = chars[j] + kept;
81865
+ t += chTokens;
81866
+ }
81867
+ if (kept.length > 0) tailParts.unshift({
81868
+ type: "text",
81869
+ text: kept
81870
+ });
81871
+ }
81872
+ break;
81873
+ }
81874
+ }
81875
+ const omitted = Math.max(0, Math.round(totalTokens - headUsed - tailUsed));
81876
+ return [
81877
+ ...headParts,
81878
+ {
81879
+ type: "text",
81880
+ text: `[content truncated - ~${omitted} tokens omitted]`
81881
+ },
81882
+ ...tailParts
81883
+ ];
81150
81884
  }
81151
81885
  function isEmptyOutputText(output) {
81152
81886
  return output.length === 0 || output.trim() === TOOL_OUTPUT_EMPTY_TEXT;
@@ -81616,6 +82350,8 @@ function buildGoalReminder(goal) {
81616
82350
  lines.push("Goal mode is iterative. Keep the self-audit brief each turn. Do not explore unrelated interpretations once the goal can be decided. If the objective is simple, already answered, impossible, unsafe, or contradictory, do not run another goal turn. Explain briefly if useful, then call UpdateGoal with `complete` or `blocked` in the same turn. Otherwise, self-audit against the objective and any completion criteria above, then do one coherent slice of work toward the objective. Use multiple turns when the task naturally has multiple phases. Call UpdateGoal with `complete` only when all required work is done, any stated validation has passed, and there is no useful next action. Do not mark complete after only producing a plan, summary, first pass, or partial result. If an external condition or required user input prevents progress, or the objective cannot be completed as stated, call UpdateGoal with `blocked`. Otherwise keep working — after your turn ends you will be prompted to continue. Call UpdateGoal as soon as the goal is genuinely done or cannot proceed; don't keep going once there is nothing left to do.");
81617
82351
  lines.push("");
81618
82352
  lines.push("When you call UpdateGoal with `complete`, an independent reviewer will verify that the completion criteria are met. In your final response before calling UpdateGoal, provide a structured summary: what was done, which files changed, the verification command and result, and any remaining work or blockers. Do not rely on the UpdateGoal argument alone; the reviewer and the user must see this summary in your natural-language reply.");
82353
+ lines.push("");
82354
+ lines.push("Important: before calling UpdateGoal with `complete`, always call WriteGoalNote first to summarize the key findings, constraints, or partial results from this turn. The reviewer will see these notes together with your final output, so include anything that helps them evaluate cross-turn context. Keep the note concise and actionable.");
81619
82355
  return lines.join("\n");
81620
82356
  }
81621
82357
  function maxBudgetFraction(goal) {
@@ -97329,6 +98065,13 @@ function buildGraderPrompt(objective, criteria, output) {
97329
98065
  "## Agent Output",
97330
98066
  output || "(no output captured)",
97331
98067
  "",
98068
+ "The Agent Output section contains the following optional parts, in order:",
98069
+ "1. The agent's natural-language summary of what was done this turn.",
98070
+ "2. \"## Cross-turn working notes\" — short notes the agent recorded using the WriteGoalNote tool across continuation turns. These notes capture key findings, constraints, decisions, and partial results. Treat them as supplementary context when evaluating whether the acceptance criteria are met; they are not the deliverable itself.",
98071
+ "3. \"## Changes this turn\" — a git diff stat (or a note if git is unavailable) showing which files were modified. Use it to verify that the claimed work has an actual code footprint.",
98072
+ "",
98073
+ "If the Agent Output does not contain a \"## Cross-turn working notes\" section, add a non-blocking issue: \"No cross-turn working notes were provided. Use WriteGoalNote to record key findings, constraints, and partial results across turns.\" This issue alone must not cause a FAIL.",
98074
+ "",
97332
98075
  "Evaluate each dimension independently against the acceptance criteria, then decide overall PASS/FAIL.",
97333
98076
  "When FAIL, list every specific issue with an actionable fix direction so the agent knows exactly what to address next.",
97334
98077
  "Respond with JSON:",
@@ -98019,6 +98762,11 @@ const GOAL_CONTINUATION_ORIGIN = {
98019
98762
  kind: "system_trigger",
98020
98763
  name: "goal_continuation"
98021
98764
  };
98765
+ const GOAL_BUDGET_STEER_PROMPT = "Budget nearly exhausted. Wrap up immediately: verify your work, run tests, and call UpdateGoal with status \"complete\" or \"blocked\". Do not start any new work.";
98766
+ const GOAL_BUDGET_STEER_ORIGIN = {
98767
+ kind: "system_trigger",
98768
+ name: "goal_budget_steer"
98769
+ };
98022
98770
  var TurnFlow = class {
98023
98771
  agent;
98024
98772
  steerBuffer = [];
@@ -98183,6 +98931,14 @@ var TurnFlow = class {
98183
98931
  }
98184
98932
  }
98185
98933
  await this.agent.goal.incrementTurn();
98934
+ const budgetSnapshot = this.agent.goal.getGoal().goal;
98935
+ if (budgetSnapshot !== null && budgetSnapshot.status === "active" && (budgetSnapshot.budget.overBudget || isBudgetNearExhaustion(budgetSnapshot.budget, .2))) {
98936
+ turnInput = [{
98937
+ type: "text",
98938
+ text: GOAL_BUDGET_STEER_PROMPT
98939
+ }];
98940
+ turnOrigin = GOAL_BUDGET_STEER_ORIGIN;
98941
+ }
98186
98942
  const end = await this.runOneTurn(turnId, turnInput, turnOrigin, signal, false);
98187
98943
  if (end.event.reason === "cancelled") {
98188
98944
  await this.agent.goal.pauseOnInterrupt({ reason: "Paused after interruption" });
@@ -98374,7 +99130,7 @@ var TurnFlow = class {
98374
99130
  turnId: String(turnId),
98375
99131
  signal,
98376
99132
  llm: this.agent.llm,
98377
- buildMessages: () => this.agent.context.messages,
99133
+ buildMessages: () => this.agent.context.messagesForLLM(),
98378
99134
  dispatchEvent: this.buildDispatchEvent(turnId),
98379
99135
  tools: this.agent.tools.loopTools,
98380
99136
  log: this.agent.log,
@@ -99509,6 +100265,9 @@ var Agent = class {
99509
100265
  if (status === "paused") return this.goal.pauseGoal({}, "user");
99510
100266
  return this.goal.resumeGoal({}, "user");
99511
100267
  },
100268
+ updateGoalObjective: async (payload) => {
100269
+ return this.goal.updateObjective({ objective: payload.objective }, "user");
100270
+ },
99512
100271
  cancelGoal: async () => {
99513
100272
  return this.goal.cancelGoal("user");
99514
100273
  },
@@ -119565,6 +120324,9 @@ var SessionAPIImpl = class {
119565
120324
  updateGoalStatus({ agentId, ...payload }) {
119566
120325
  return this.getAgent(agentId).updateGoalStatus(payload);
119567
120326
  }
120327
+ updateGoalObjective({ agentId, ...payload }) {
120328
+ return this.getAgent(agentId).updateGoalObjective(payload);
120329
+ }
119568
120330
  cancelGoal({ agentId, ...payload }) {
119569
120331
  return this.getAgent(agentId).cancelGoal(payload);
119570
120332
  }
@@ -121307,6 +122069,9 @@ var ScreamCore = class {
121307
122069
  updateGoalStatus({ sessionId, ...payload }) {
121308
122070
  return this.sessionApi(sessionId).updateGoalStatus(payload);
121309
122071
  }
122072
+ updateGoalObjective({ sessionId, ...payload }) {
122073
+ return this.sessionApi(sessionId).updateGoalObjective(payload);
122074
+ }
121310
122075
  cancelGoal({ sessionId, ...payload }) {
121311
122076
  return this.sessionApi(sessionId).cancelGoal(payload);
121312
122077
  }
@@ -121760,6 +122525,13 @@ var SDKRpcClient = class {
121760
122525
  status: input.status
121761
122526
  });
121762
122527
  }
122528
+ async updateGoalObjective(input) {
122529
+ return (await this.getRpc()).updateGoalObjective({
122530
+ sessionId: input.sessionId,
122531
+ agentId: this.interactiveAgentId,
122532
+ objective: input.objective
122533
+ });
122534
+ }
121763
122535
  async cancelGoal(input) {
121764
122536
  return (await this.getRpc()).cancelGoal({
121765
122537
  sessionId: input.sessionId,
@@ -122520,6 +123292,13 @@ var Session = class {
122520
123292
  status
122521
123293
  });
122522
123294
  }
123295
+ async updateGoalObjective(objective) {
123296
+ this.ensureOpen();
123297
+ return this.rpc.updateGoalObjective({
123298
+ sessionId: this.id,
123299
+ objective
123300
+ });
123301
+ }
122523
123302
  async cancelGoal() {
122524
123303
  this.ensureOpen();
122525
123304
  return this.rpc.cancelGoal({ sessionId: this.id });
@@ -122979,7 +123758,7 @@ function optionalBuildString(value) {
122979
123758
  return typeof value === "string" && value.length > 0 ? value : void 0;
122980
123759
  }
122981
123760
  const SCREAM_BUILD_INFO = {
122982
- version: optionalBuildString("0.10.7"),
123761
+ version: optionalBuildString("0.10.9"),
122983
123762
  channel: optionalBuildString(""),
122984
123763
  commit: optionalBuildString(""),
122985
123764
  buildTarget: optionalBuildString("darwin-arm64")
@@ -126197,9 +126976,15 @@ function formatGoalDuration(ms) {
126197
126976
  const seconds = totalSeconds % 60;
126198
126977
  return seconds > 0 ? `${minutes}m${seconds}s` : `${minutes}m`;
126199
126978
  }
126200
- /** Build the footer goal badge: `GOAL 3m · 7 turns`. */
126201
- function formatGoalBadge(wallClockMs, turnsUsed) {
126202
- return `GOAL ${formatGoalDuration(wallClockMs)} · ${turnsUsed} turns`;
126979
+ /** Build the footer goal badge: `GOAL 3m · 7 turns`.
126980
+ *
126981
+ * The TUI only receives `goal.updated` events on state changes, so we keep a
126982
+ * local base timestamp and add the elapsed time since the last snapshot to
126983
+ * produce a live wall-clock reading between sparse events.
126984
+ */
126985
+ function formatGoalBadge(goal) {
126986
+ const elapsedSinceSnapshot = Date.now() - goal.wallClockBaseAt;
126987
+ return `GOAL ${formatGoalDuration(goal.wallClockMs + Math.max(0, elapsedSinceSnapshot))} · ${goal.turnsUsed} turns`;
126203
126988
  }
126204
126989
  const CONTEXT_WARNING_PERCENT_THRESHOLD = 60;
126205
126990
  const CONTEXT_ERROR_PERCENT_THRESHOLD = 90;
@@ -126270,6 +127055,30 @@ function formatFooterGitBadge(status, colors) {
126270
127055
  if (status.pullRequest === null) return base;
126271
127056
  return `${base} ${chalk.hex(colors.primary)(formatPullRequestBadge(status.pullRequest, { linkPullRequest: true }))}`;
126272
127057
  }
127058
+ /**
127059
+ * Middle-truncate a (possibly ANSI-colored) string to `maxWidth` visible
127060
+ * columns, keeping a head and a tail fragment joined by `ellipsis`. The
127061
+ * remaining budget is split roughly in half so both the start and the end of
127062
+ * the original content stay visible - e.g. `GOAL 3m · 7 turns` becomes
127063
+ * `GOAL… turns` (head `GOAL`, tail `turns`). ANSI styling is preserved via
127064
+ * sliceByColumn, which carries the active SGR state into the tail; a reset is
127065
+ * emitted around the ellipsis so a colour opened in the head never bleeds
127066
+ * across it. Exported for unit testing.
127067
+ */
127068
+ function truncateMiddle(line, maxWidth, ellipsis) {
127069
+ if (maxWidth <= 0) return "";
127070
+ const textWidth = visibleWidth(line);
127071
+ if (textWidth <= maxWidth) return line;
127072
+ const ellipsisWidth = visibleWidth(ellipsis);
127073
+ if (ellipsisWidth >= maxWidth) return truncateToWidth(line, maxWidth, ellipsis);
127074
+ const keepWidth = maxWidth - ellipsisWidth;
127075
+ const headWidth = Math.max(0, Math.ceil(keepWidth / 2));
127076
+ const tailWidth = keepWidth - headWidth;
127077
+ const head = headWidth > 0 ? sliceByColumn(line, 0, headWidth) : "";
127078
+ const tail = tailWidth > 0 ? sliceByColumn(line, textWidth - tailWidth, tailWidth) : "";
127079
+ const RESET = "\x1B[0m";
127080
+ return head + RESET + ellipsis + RESET + tail;
127081
+ }
126273
127082
  var FooterComponent = class {
126274
127083
  state;
126275
127084
  colors;
@@ -126295,15 +127104,17 @@ var FooterComponent = class {
126295
127104
  this.onGitStatusChange = onGitStatusChange;
126296
127105
  this.gitCacheWorkDir = state.workDir;
126297
127106
  this.gitCache = createGitStatusCache(state.workDir, { onChange: this.onGitStatusChange });
127107
+ this.#restartStatusTimer(state.streamingPhase, state.goalActive);
126298
127108
  }
126299
127109
  setState(state) {
126300
127110
  const previousPhase = this.state?.streamingPhase;
127111
+ const previousGoalActive = this.state?.goalActive;
126301
127112
  if (state.workDir !== this.gitCacheWorkDir) {
126302
127113
  this.gitCacheWorkDir = state.workDir;
126303
127114
  this.gitCache = createGitStatusCache(state.workDir, { onChange: this.onGitStatusChange });
126304
127115
  }
126305
127116
  this.state = state;
126306
- if (state.streamingPhase !== previousPhase) this.#restartStatusTimer(state.streamingPhase);
127117
+ if (state.streamingPhase !== previousPhase || state.goalActive !== previousGoalActive) this.#restartStatusTimer(state.streamingPhase, state.goalActive);
126307
127118
  }
126308
127119
  setColors(colors) {
126309
127120
  this.colors = colors;
@@ -126331,9 +127142,9 @@ var FooterComponent = class {
126331
127142
  dispose() {
126332
127143
  this.#stopStatusTimer();
126333
127144
  }
126334
- #restartStatusTimer(phase) {
127145
+ #restartStatusTimer(phase, goalActive) {
126335
127146
  this.#stopStatusTimer();
126336
- if (phase === "idle") return;
127147
+ if (phase === "idle" && !goalActive) return;
126337
127148
  const intervalMs = 1e3 / 60;
126338
127149
  this.statusTimer = setInterval(() => {
126339
127150
  this.ui.requestRender();
@@ -126354,8 +127165,7 @@ var FooterComponent = class {
126354
127165
  }
126355
127166
  if (state.wolfpackMode) left.push(chalk.hex(colors.wolfpackMode).bold(t("badge.wolfpack")));
126356
127167
  if (state.goalActive && state.goal) {
126357
- const g = state.goal;
126358
- const goalLabel = formatGoalBadge(g.wallClockMs, g.turnsUsed);
127168
+ const goalLabel = formatGoalBadge(state.goal);
126359
127169
  left.push(chalk.hex(colors.primary).bold(goalLabel));
126360
127170
  }
126361
127171
  const model = shortenModel(modelDisplayName(state));
@@ -126377,13 +127187,21 @@ var FooterComponent = class {
126377
127187
  }
126378
127188
  const rightWidth = visibleWidth(rightText);
126379
127189
  const gap = 3;
127190
+ const ellipsis = chalk.hex(colors.textDim)("…");
126380
127191
  let line1;
126381
127192
  if (leftWidth + gap + rightWidth <= width) {
126382
127193
  const pad = width - leftWidth - rightWidth;
126383
127194
  line1 = leftLine + " ".repeat(pad) + rightText;
126384
- } else if (leftWidth <= width) line1 = leftLine;
126385
- else line1 = truncateToWidth(leftLine, width, "…");
126386
- return [truncateToWidth(line1, width)];
127195
+ } else {
127196
+ const targetLeft = width - gap - rightWidth;
127197
+ if (targetLeft > 0) {
127198
+ const shrunkLeft = truncateMiddle(leftLine, targetLeft, ellipsis);
127199
+ const pad = width - visibleWidth(shrunkLeft) - rightWidth;
127200
+ line1 = shrunkLeft + " ".repeat(pad) + rightText;
127201
+ } else if (leftWidth <= width) line1 = leftLine + " ".repeat(width - leftWidth);
127202
+ else line1 = truncateMiddle(leftLine, width, ellipsis);
127203
+ }
127204
+ return [truncateToWidth(line1, width, "…")];
126387
127205
  }
126388
127206
  };
126389
127207
  //#endregion
@@ -126731,10 +127549,63 @@ function createThemeStyles(colors) {
126731
127549
  //#endregion
126732
127550
  //#region src/tui/theme/pi-tui-theme.ts
126733
127551
  const HEADING_HASH_PREFIX = /^((?:\u001B\[[0-9;]*m)*)#{1,6}[ \t]+/;
127552
+ /**
127553
+ * Map cli-highlight syntax tokens onto the ColorPalette so code blocks follow
127554
+ * the active theme instead of cli-highlight's built-in colors. cli-highlight
127555
+ * takes one formatter function per token; tokens not listed here fall back to
127556
+ * its DEFAULT_THEME.
127557
+ */
127558
+ function createCodeHighlightTheme(colors) {
127559
+ const keyword = chalk.hex(colors.primary);
127560
+ const str = chalk.hex(colors.success);
127561
+ const comment = chalk.hex(colors.textDim);
127562
+ const num = chalk.hex(colors.warning);
127563
+ const fn = chalk.hex(colors.primary);
127564
+ const cls = chalk.hex(colors.accent);
127565
+ const text = chalk.hex(colors.text);
127566
+ return {
127567
+ keyword,
127568
+ built_in: fn,
127569
+ type: cls,
127570
+ literal: num,
127571
+ number: num,
127572
+ regexp: str,
127573
+ string: str,
127574
+ subst: str,
127575
+ symbol: num,
127576
+ class: cls,
127577
+ function: fn,
127578
+ title: fn,
127579
+ params: text,
127580
+ comment,
127581
+ doctag: comment,
127582
+ meta: chalk.hex(colors.textMuted),
127583
+ "meta-keyword": keyword,
127584
+ "meta-string": str,
127585
+ section: keyword,
127586
+ tag: cls,
127587
+ name: fn,
127588
+ "builtin-name": fn,
127589
+ attr: num,
127590
+ attribute: num,
127591
+ variable: text,
127592
+ bullet: num,
127593
+ code: str,
127594
+ emphasis: (s) => chalk.italic(s),
127595
+ strong: (s) => chalk.bold(s),
127596
+ formula: text,
127597
+ link: chalk.hex(colors.mdLink),
127598
+ quote: chalk.hex(colors.mdQuote),
127599
+ addition: chalk.hex(colors.diffAdded),
127600
+ deletion: chalk.hex(colors.diffRemoved),
127601
+ default: text
127602
+ };
127603
+ }
126734
127604
  function createMarkdownTheme(colors) {
126735
127605
  const stripHash = (text) => text.replace(HEADING_HASH_PREFIX, "$1");
126736
127606
  const muted = chalk.hex(colors.textMuted);
126737
127607
  const border = chalk.hex(colors.border);
127608
+ const codeTheme = createCodeHighlightTheme(colors);
126738
127609
  return {
126739
127610
  heading: (text) => chalk.bold.hex(colors.text)(stripHash(text)),
126740
127611
  link: (text) => chalk.hex(colors.mdLink)(text),
@@ -126756,7 +127627,8 @@ function createMarkdownTheme(colors) {
126756
127627
  try {
126757
127628
  return highlight(code, {
126758
127629
  language,
126759
- ignoreIllegals: true
127630
+ ignoreIllegals: true,
127631
+ theme: codeTheme
126760
127632
  }).split("\n");
126761
127633
  } catch {
126762
127634
  return code.split("\n");
@@ -128185,6 +129057,10 @@ function buildEmptyGoalLines(colors) {
128185
129057
  value(t("goalpanel.no_goal")),
128186
129058
  "",
128187
129059
  `${muted("/goal")} ${value(t("goalpanel.goal_placeholder"))} ${muted(t("goalpanel.create_goal"))}`,
129060
+ `${muted("/goal setup")} ${muted(t("goalpanel.setup_goal"))}`,
129061
+ `${muted("/goal update")} ${value("<new objective>")} ${muted(t("goalpanel.update_goal"))}`,
129062
+ `${muted("/goal status")} ${muted(t("goalpanel.status_goal"))}`,
129063
+ `${muted("/goal replace")} ${value("<new objective>")} ${muted(t("goalpanel.replace_goal"))}`,
128188
129064
  `${muted("/goal pause")} ${muted(t("goalpanel.pause_goal"))}`,
128189
129065
  `${muted("/goal resume")} ${muted(t("goalpanel.resume_goal"))}`,
128190
129066
  `${muted("/goaloff")} ${muted(t("goalpanel.cancel_goal"))}`
@@ -128247,6 +129123,19 @@ function parseGoalCommand(rawArgs) {
128247
129123
  const tokens = args.split(/\s+/);
128248
129124
  const first = tokens[0];
128249
129125
  if (first !== void 0 && CONTROL_SUBCOMMANDS.has(first) && tokens.length === 1) return { kind: first };
129126
+ if (first === "setup") return { kind: "setup" };
129127
+ if (first === "update") {
129128
+ const objective = tokens.slice(1).join(" ").trim();
129129
+ if (objective.length === 0) return {
129130
+ kind: "error",
129131
+ severity: "hint",
129132
+ message: t("goal.need_desc")
129133
+ };
129134
+ return {
129135
+ kind: "update",
129136
+ objective
129137
+ };
129138
+ }
128250
129139
  let index = 0;
128251
129140
  let replace = false;
128252
129141
  if (tokens[index] === "replace") {
@@ -128276,6 +129165,9 @@ async function handleGoalCommand(host, args) {
128276
129165
  case "status":
128277
129166
  await showGoalStatus(host);
128278
129167
  return;
129168
+ case "setup":
129169
+ await guidedGoalSetup(host);
129170
+ return;
128279
129171
  case "pause":
128280
129172
  await pauseGoal(host);
128281
129173
  return;
@@ -128285,6 +129177,9 @@ async function handleGoalCommand(host, args) {
128285
129177
  case "off":
128286
129178
  await handleGoalOffCommand(host);
128287
129179
  return;
129180
+ case "update":
129181
+ await updateGoalObjective(host, parsed);
129182
+ return;
128288
129183
  case "create":
128289
129184
  await createGoal(host, parsed);
128290
129185
  return;
@@ -128302,8 +129197,51 @@ async function createGoal(host, parsed) {
128302
129197
  }
128303
129198
  await showGoalConfigWizard(host, session, parsed.objective, parsed.replace);
128304
129199
  }
129200
+ const GOAL_REFINER_SYSTEM_PROMPT = "You are a goal refiner. Given a brief task description, produce a single clear, actionable objective sentence (max 200 chars). Do not add explanations, quotes, or prefixes.";
129201
+ /**
129202
+ * Guided goal creation: collect a brief task description, refine it via the
129203
+ * LLM into a single objective sentence, let the user confirm/edit, then enter
129204
+ * the standard configuration wizard. Falls back to the raw description if the
129205
+ * LLM call fails.
129206
+ */
129207
+ async function guidedGoalSetup(host) {
129208
+ const session = host.session;
129209
+ if (session === void 0) {
129210
+ host.showError(t("error.no_session"));
129211
+ return;
129212
+ }
129213
+ if (detectGoalConflict(host.state.appState, "enable_goal") === "goal_active") {
129214
+ host.showNotice(t("goal.storm_breaker"), t("goal.conflict_loop"));
129215
+ return;
129216
+ }
129217
+ const { TextInputDialogComponent } = await import("./text-input-dialog-B1ak519Y.mjs");
129218
+ const initialDesc = await promptText(host, TextInputDialogComponent, {
129219
+ title: t("goal.setup_title_initial"),
129220
+ subtitle: t("goal.setup_desc_hint"),
129221
+ placeholder: t("goal.setup_desc_placeholder"),
129222
+ allowEmpty: false
129223
+ });
129224
+ if (initialDesc === void 0) return;
129225
+ host.showStatus(t("goal.setup_refining"));
129226
+ let objective;
129227
+ try {
129228
+ objective = (await session.generateText(GOAL_REFINER_SYSTEM_PROMPT, initialDesc)).trim();
129229
+ if (objective.length === 0) objective = initialDesc;
129230
+ } catch {
129231
+ objective = initialDesc;
129232
+ }
129233
+ const confirmed = await promptText(host, TextInputDialogComponent, {
129234
+ title: t("goal.setup_title_confirm"),
129235
+ subtitle: t("goal.setup_confirm_hint"),
129236
+ placeholder: objective,
129237
+ initialValue: objective,
129238
+ allowEmpty: true
129239
+ });
129240
+ if (confirmed === void 0) return;
129241
+ await showGoalConfigWizard(host, session, confirmed.trim() || objective, false);
129242
+ }
128305
129243
  async function showGoalConfigWizard(host, session, objective, replace) {
128306
- const { TextInputDialogComponent } = await import("./text-input-dialog-DqBy9bEe.mjs");
129244
+ const { TextInputDialogComponent } = await import("./text-input-dialog-B1ak519Y.mjs");
128307
129245
  const turnInput = await promptNumber(host, TextInputDialogComponent, {
128308
129246
  title: t("goal.wizard_title", { objective }),
128309
129247
  subtitle: t("goal.budget_turns_hint"),
@@ -128373,6 +129311,27 @@ function promptNumber(host, TextInputDialogComponent, opts) {
128373
129311
  host.mountEditorReplacement(dialog);
128374
129312
  });
128375
129313
  }
129314
+ /** Prompt user for free-form text. Returns undefined on cancel. */
129315
+ function promptText(host, TextInputDialogComponent, opts) {
129316
+ return new Promise((resolve) => {
129317
+ const dialog = new TextInputDialogComponent((result) => {
129318
+ host.restoreEditor();
129319
+ if (result.kind !== "ok") {
129320
+ resolve(void 0);
129321
+ return;
129322
+ }
129323
+ resolve(result.value.trim());
129324
+ }, {
129325
+ title: opts.title,
129326
+ subtitle: opts.subtitle,
129327
+ placeholder: opts.placeholder,
129328
+ initialValue: opts.initialValue,
129329
+ allowEmpty: opts.allowEmpty,
129330
+ colors: host.state.theme.colors
129331
+ });
129332
+ host.mountEditorReplacement(dialog);
129333
+ });
129334
+ }
128376
129335
  async function pauseGoal(host) {
128377
129336
  const session = host.session;
128378
129337
  if (session === void 0) {
@@ -128413,6 +129372,24 @@ async function resumeGoal(host) {
128413
129372
  host.showError(t("goal.resume_failed", { msg: message }));
128414
129373
  }
128415
129374
  }
129375
+ async function updateGoalObjective(host, parsed) {
129376
+ const session = host.session;
129377
+ if (session === void 0) {
129378
+ host.showError(t("error.no_session"));
129379
+ return;
129380
+ }
129381
+ try {
129382
+ if ((await session.getGoal()).goal === null) {
129383
+ host.showStatus(t("goal.no_active"));
129384
+ return;
129385
+ }
129386
+ await session.updateGoalObjective(parsed.objective);
129387
+ host.showStatus(t("goal.updated", { objective: parsed.objective }));
129388
+ } catch (error) {
129389
+ const message = error instanceof Error ? error.message : String(error);
129390
+ host.showError(t("goal.update_failed", { msg: message }));
129391
+ }
129392
+ }
128416
129393
  async function handleGoalOffCommand(host) {
128417
129394
  const session = host.session;
128418
129395
  if (session === void 0) {
@@ -129442,6 +130419,12 @@ function easeSpeedRatio(ratio) {
129442
130419
  }
129443
130420
  //#endregion
129444
130421
  //#region src/tui/components/messages/thinking.ts
130422
+ /** gpt-5 reasoning summaries contain empty HTML comment padding sentinels
130423
+ * like `<!-- -->`. Strip them to keep the thinking display clean. */
130424
+ const EMPTY_COMMENT_RE = /<!--\s*-->/g;
130425
+ function filterThinkingNoise(text) {
130426
+ return text.replace(EMPTY_COMMENT_RE, "");
130427
+ }
129445
130428
  var ThinkingComponent = class {
129446
130429
  text;
129447
130430
  color;
@@ -129480,7 +130463,7 @@ var ThinkingComponent = class {
129480
130463
  this.textComponent.setText(this.styled(trimmed));
129481
130464
  }
129482
130465
  styled(text) {
129483
- return chalk.hex(this.color).italic(text);
130466
+ return chalk.hex(this.color).italic(filterThinkingNoise(text));
129484
130467
  }
129485
130468
  finalize() {
129486
130469
  if (this.mode === "finalized") return;
@@ -129657,6 +130640,68 @@ function makeDiffStyles(colors) {
129657
130640
  meta: (s) => chalk.hex(colors.diffMeta)(s)
129658
130641
  };
129659
130642
  }
130643
+ const ANSI_RE = /\u001B\[[0-9;]*m/;
130644
+ const ANSI_RESET_RE = /\u001B\[(?:0|39)m/g;
130645
+ /**
130646
+ * Extract the leading ANSI SGR prefix a chalk-style color function applies,
130647
+ * e.g. `\x1b[38;2;121;235;0m` for a truecolor hex. Returns `''` when the
130648
+ * function emits no color (chalk level 0 / non-color environment).
130649
+ */
130650
+ function extractAnsiPrefix(fn) {
130651
+ const sentinel = "\0";
130652
+ const styled = fn(sentinel);
130653
+ const idx = styled.indexOf(sentinel);
130654
+ return idx >= 0 ? styled.slice(0, idx) : "";
130655
+ }
130656
+ /** Visualize leading whitespace: tabs as `->`, leading spaces as `·`. */
130657
+ function visualizeIndent(line) {
130658
+ let i = 0;
130659
+ let visual = "";
130660
+ while (i < line.length) {
130661
+ const ch = line[i];
130662
+ if (ch === " ") {
130663
+ visual += "->";
130664
+ i++;
130665
+ continue;
130666
+ }
130667
+ if (ch === " ") {
130668
+ visual += "·";
130669
+ i++;
130670
+ continue;
130671
+ }
130672
+ break;
130673
+ }
130674
+ return {
130675
+ text: visual + line.slice(i),
130676
+ indentEnd: visual.length
130677
+ };
130678
+ }
130679
+ /**
130680
+ * Render a diff line's code with leading-whitespace visualization (tabs as
130681
+ * `->`, spaces as `·`, dimmed) and optional syntax highlighting. Highlighting
130682
+ * runs on the raw code first; indent visualization then runs on the
130683
+ * highlighted string - leading whitespace carries no token color, so it is
130684
+ * still plain and can be replaced and dimmed without disturbing the tokens.
130685
+ *
130686
+ * When highlighting is on and produced token colors, the diff line color is
130687
+ * layered as the base foreground: syntax tokens override it, but every ANSI
130688
+ * reset re-injects the diff color so non-token spans (punctuation, operators,
130689
+ * whitespace) stay green/red instead of falling back to the terminal default.
130690
+ * When highlighting is off (streaming) or produced no token colors, the code
130691
+ * part is colored with the diff line color instead.
130692
+ */
130693
+ function renderDiffCode(code, colorFn, highlight, lang) {
130694
+ const { text, indentEnd } = visualizeIndent(highlight ? highlightLines(code, lang)[0] ?? code : code);
130695
+ const indent = text.slice(0, indentEnd);
130696
+ const rest = text.slice(indentEnd);
130697
+ const dimIndent = indent.length > 0 ? chalk.dim(indent) : indent;
130698
+ if (highlight && ANSI_RE.test(rest)) {
130699
+ const prefix = extractAnsiPrefix(colorFn);
130700
+ if (prefix.length > 0) return dimIndent + prefix + rest.replace(ANSI_RESET_RE, (m) => m + prefix) + "\x1B[39m";
130701
+ return dimIndent + rest;
130702
+ }
130703
+ return dimIndent + colorFn(rest);
130704
+ }
129660
130705
  /**
129661
130706
  * Compute word-level diff between two single lines and highlight the changed
129662
130707
  * words with `chalk.inverse()`. Only the first removed/added part has its
@@ -129763,6 +130808,8 @@ function computeDiffLines(oldText, newText, oldStart = 1, newStart = 1, isIncomp
129763
130808
  }
129764
130809
  function renderDiffLines(oldText, newText, path, colors, isIncomplete = false, oldStart, newStart, maxLines) {
129765
130810
  const s = makeDiffStyles(colors);
130811
+ const lang = langFromPath(path);
130812
+ const doHighlight = !isIncomplete && lang !== void 0;
129766
130813
  const changedLines = computeDiffLines(oldText, newText, oldStart ?? 1, newStart ?? 1, isIncomplete).filter((l) => l.kind !== "context");
129767
130814
  const added = changedLines.filter((l) => l.kind === "add").length;
129768
130815
  const removed = changedLines.filter((l) => l.kind === "delete").length;
@@ -129787,7 +130834,7 @@ function renderDiffLines(oldText, newText, path, colors, isIncomplete = false, o
129787
130834
  const line = shown[i];
129788
130835
  const marker = line.kind === "add" ? "+" : "-";
129789
130836
  const color = line.kind === "add" ? s.add : s.del;
129790
- output.push(s.gutter(String(line.lineNum).padStart(4) + " ") + color(marker + " " + line.code));
130837
+ output.push(s.gutter(String(line.lineNum).padStart(4) + " ") + color(`${marker} `) + renderDiffCode(line.code, color, doHighlight, lang));
129791
130838
  i += 1;
129792
130839
  }
129793
130840
  const hidden = changedLines.length - shown.length;
@@ -129838,11 +130885,11 @@ function buildClusters(diffLines, contextLines) {
129838
130885
  removedCount: removed
129839
130886
  };
129840
130887
  }
129841
- function formatDiffRow(line, s) {
130888
+ function formatDiffRow(line, s, doHighlight, lang) {
129842
130889
  const gutter = s.gutter(String(line.lineNum).padStart(4) + " ");
129843
- if (line.kind === "add") return gutter + s.add("+ " + line.code);
129844
- if (line.kind === "delete") return gutter + s.del("- " + line.code);
129845
- return gutter + " " + line.code;
130890
+ if (line.kind === "add") return gutter + s.add("+ ") + renderDiffCode(line.code, s.add, doHighlight, lang);
130891
+ if (line.kind === "delete") return gutter + s.del("- ") + renderDiffCode(line.code, s.del, doHighlight, lang);
130892
+ return gutter + " " + renderDiffCode(line.code, (x) => x, doHighlight, lang);
129846
130893
  }
129847
130894
  /**
129848
130895
  * Render a diff with surrounding context, eliding unchanged middle
@@ -129855,6 +130902,8 @@ function formatDiffRow(line, s) {
129855
130902
  */
129856
130903
  function renderDiffLinesClustered(oldText, newText, path, colors, opts = {}) {
129857
130904
  const s = makeDiffStyles(colors);
130905
+ const lang = langFromPath(path);
130906
+ const doHighlight = !(opts.isIncomplete ?? false) && lang !== void 0;
129858
130907
  const contextLines = opts.contextLines ?? 3;
129859
130908
  const maxLines = opts.maxLines;
129860
130909
  const diffLines = computeDiffLines(oldText, newText, 1, 1, opts.isIncomplete ?? false);
@@ -129911,7 +130960,7 @@ function renderDiffLinesClustered(oldText, newText, path, colors, opts = {}) {
129911
130960
  i += 2;
129912
130961
  continue;
129913
130962
  }
129914
- output.push(formatDiffRow(line, s));
130963
+ output.push(formatDiffRow(line, s, doHighlight, lang));
129915
130964
  body++;
129916
130965
  if (line.kind !== "context") shownChanges++;
129917
130966
  prevEnd = i;
@@ -130077,34 +131126,43 @@ function truncateTailBytes(text, maxBytes) {
130077
131126
  }
130078
131127
  /**
130079
131128
  * Component that renders tool output with wrap-aware line truncation.
130080
- * Uses pi-tui's Text component to compute actual visual wrapped lines,
130081
- * then caps at PREVIEW_LINES. This handles long single-line output (e.g.
130082
- * JSON blobs) that would otherwise wrap to dozens of visual rows.
131129
+ * Uses pi-tui's Text component to compute actual visual wrapped lines, then
131130
+ * caps at `maxLines`. When collapsed the TAIL is shown (newest output, where
131131
+ * command errors land) with an expand hint at the top; when expanded the full
131132
+ * output is shown with a collapse hint at the top. Handles long single-line
131133
+ * output (e.g. JSON blobs) that would otherwise wrap to dozens of visual rows.
130083
131134
  */
130084
131135
  var TruncatedOutputComponent = class {
130085
131136
  textComponent;
130086
131137
  expanded;
130087
131138
  maxLines;
130088
131139
  hintFormatter;
131140
+ collapseHintFormatter;
130089
131141
  constructor(output, options) {
130090
131142
  this.expanded = options.expanded;
130091
131143
  this.maxLines = options.maxLines ?? PREVIEW_LINES;
130092
131144
  this.hintFormatter = options.hintFormatter;
131145
+ this.collapseHintFormatter = options.collapseHintFormatter;
130093
131146
  const tint = options.isError ? chalk.hex(options.colors.error) : chalk.dim;
130094
131147
  const cleaned = trimTrailingEmptyLines(output.split("\n")).join("\n");
130095
- const truncated = options.maxBytes === void 0 ? cleaned : truncateTailBytes(cleaned, options.maxBytes);
130096
- this.textComponent = new Text(tint(truncated), 2, 0);
131148
+ const stripped = options.isError ? cleaned.replaceAll(/\u001B\[[0-9;]*m/g, "") : cleaned;
131149
+ const tinted = (options.maxBytes === void 0 ? stripped : truncateTailBytes(stripped, options.maxBytes)).split("\n").map((line) => tint(line)).join("\n");
131150
+ this.textComponent = new Text(tinted, 2, 0);
130097
131151
  }
130098
131152
  invalidate() {
130099
131153
  this.textComponent.invalidate();
130100
131154
  }
130101
131155
  render(width) {
130102
131156
  const contentLines = this.textComponent.render(width);
130103
- if (this.expanded || contentLines.length <= this.maxLines) return contentLines;
130104
- const shown = contentLines.slice(0, this.maxLines);
131157
+ if (contentLines.length <= this.maxLines) return contentLines;
131158
+ if (this.expanded) {
131159
+ const collapseHint = this.collapseHintFormatter ? this.collapseHintFormatter() : t("shell.collapse_hint");
131160
+ return [chalk.dim(collapseHint), ...contentLines];
131161
+ }
130105
131162
  const remaining = contentLines.length - this.maxLines;
130106
- const hint = this.hintFormatter ? this.hintFormatter(remaining) : `... (${String(remaining)} more lines, ctrl+o to expand)`;
130107
- return [...shown, chalk.dim(hint)];
131163
+ const tail = contentLines.slice(-this.maxLines);
131164
+ const expandHint = this.hintFormatter ? this.hintFormatter(remaining) : t("shell.more_lines", { count: String(remaining) });
131165
+ return [chalk.dim(expandHint), ...tail];
130108
131166
  }
130109
131167
  };
130110
131168
  const renderTruncated = (_toolCall, result, ctx) => {
@@ -130121,7 +131179,7 @@ var ShellExecutionComponent = class extends Container {
130121
131179
  constructor(options) {
130122
131180
  super();
130123
131181
  if (options.showCommand === true) this.addCommandPreview(options.command ?? "", options.commandPreviewLines);
130124
- if (options.result !== void 0) this.addResultPreview(options.result, options.colors, options.expanded ?? false, options.resultPreviewLines ?? PREVIEW_LINES);
131182
+ if (options.result !== void 0) this.addResultPreview(options.result, options.colors, options.expanded ?? false, options.resultPreviewLines ?? 15);
130125
131183
  }
130126
131184
  addCommandPreview(command, previewLines) {
130127
131185
  if (command.length === 0) return;
@@ -130140,7 +131198,8 @@ var ShellExecutionComponent = class extends Container {
130140
131198
  colors,
130141
131199
  maxLines: previewLines,
130142
131200
  maxBytes: MAX_SHELL_OUTPUT_BYTES,
130143
- hintFormatter: (remaining) => t("shell.more_lines", { count: String(remaining) })
131201
+ hintFormatter: (remaining) => t("shell.more_lines", { count: String(remaining) }),
131202
+ collapseHintFormatter: () => t("shell.collapse_hint")
130144
131203
  }));
130145
131204
  }
130146
131205
  };
@@ -138363,6 +139422,8 @@ var SessionEventHandler = class {
138363
139422
  this.host.showNotice(title, detail);
138364
139423
  }
138365
139424
  handleStepRetrying(event) {
139425
+ this.host.streamingUI.resetLiveText();
139426
+ this.host.streamingUI.resetToolUi();
138366
139427
  this.host.setAppState({ reconnectAttempt: event.nextAttempt });
138367
139428
  }
138368
139429
  maybeShowDebugTiming(event) {
@@ -138652,7 +139713,8 @@ var SessionEventHandler = class {
138652
139713
  goal: {
138653
139714
  objective: snapshot.objective,
138654
139715
  turnsUsed: snapshot.turnsUsed ?? 0,
138655
- wallClockMs: snapshot.wallClockMs ?? 0
139716
+ wallClockMs: snapshot.wallClockMs ?? 0,
139717
+ wallClockBaseAt: Date.now()
138656
139718
  },
138657
139719
  goalActive: snapshot.status === "active"
138658
139720
  });
@@ -144729,7 +145791,8 @@ var SessionManager = class {
144729
145791
  goal: goal ? {
144730
145792
  objective: goal.objective,
144731
145793
  turnsUsed: goal.turnsUsed ?? 0,
144732
- wallClockMs: goal.wallClockMs ?? 0
145794
+ wallClockMs: goal.wallClockMs ?? 0,
145795
+ wallClockBaseAt: Date.now()
144733
145796
  } : null,
144734
145797
  goalActive: goal?.status === "active",
144735
145798
  goalContinuationCount: 0
@@ -146975,6 +148038,7 @@ var ScreamTUI = class {
146975
148038
  deferUserMessages = false;
146976
148039
  aborted = false;
146977
148040
  isShuttingDown = false;
148041
+ tightModeHandler = null;
146978
148042
  reverseRpcDisposers = [];
146979
148043
  startupNotice;
146980
148044
  updatePrefetched;
@@ -147071,6 +148135,11 @@ var ScreamTUI = class {
147071
148135
  this.inputController.setupAutocomplete();
147072
148136
  }
147073
148137
  async start() {
148138
+ this.tightModeHandler = () => {
148139
+ setTightMode((process.stdout.columns ?? 80) < 60);
148140
+ };
148141
+ this.tightModeHandler();
148142
+ process.stdout.on("resize", this.tightModeHandler);
147074
148143
  this.lifecycleController.installSignalHandlers();
147075
148144
  try {
147076
148145
  const shouldReplayHistory = await this.initMainTui();
@@ -147148,6 +148217,11 @@ var ScreamTUI = class {
147148
148217
  async stop(exitCode) {
147149
148218
  if (this.isShuttingDown) return;
147150
148219
  this.isShuttingDown = true;
148220
+ if (this.tightModeHandler !== null) {
148221
+ process.stdout.off("resize", this.tightModeHandler);
148222
+ this.tightModeHandler = null;
148223
+ }
148224
+ setTightMode(false);
147151
148225
  this.lifecycleController.stopCcConnectPolling();
147152
148226
  this.lifecycleController.uninstallSignalHandlers();
147153
148227
  this.aborted = true;