scream-code 0.10.7 → 0.10.8

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-CiWeHRNJ.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";
@@ -54,7 +55,6 @@ import chalk, { chalkStderr } from "chalk";
54
55
  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
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
  /**
@@ -55791,14 +55791,32 @@ const MAX_GOAL_OBJECTIVE_LENGTH = 4e3;
55791
55791
  /** Consecutive markBlocked calls with the same reason required before blocking. */
55792
55792
  const BLOCKED_STREAK_THRESHOLD = 3;
55793
55793
  /** Maximum number of working notes kept per goal. */
55794
- const MAX_GOAL_NOTES = 30;
55794
+ const MAX_GOAL_NOTES = 60;
55795
55795
  /** Maximum characters per note. */
55796
- const MAX_NOTE_LENGTH = 200;
55796
+ const MAX_NOTE_LENGTH = 400;
55797
55797
  const GOAL_CANCELLED_REMINDER = [
55798
55798
  "The user cancelled the current goal.",
55799
55799
  "Ignore earlier active-goal reminders for that goal.",
55800
55800
  "Handle the next user request normally unless the user starts or resumes a goal."
55801
55801
  ].join(" ");
55802
+ /**
55803
+ * Returns true when any configured budget has less than `threshold` of its
55804
+ * allowance remaining (e.g. 0.2 = under 20% left). Budgets that are not
55805
+ * configured (null) are ignored. Used to steer the model toward convergence
55806
+ * before a hard over-budget block fires.
55807
+ */
55808
+ function isBudgetNearExhaustion(budget, threshold) {
55809
+ if (budget.turnBudget !== null && budget.remainingTurns !== null && budget.turnBudget > 0) {
55810
+ if (budget.remainingTurns / budget.turnBudget < threshold) return true;
55811
+ }
55812
+ if (budget.tokenBudget !== null && budget.remainingTokens !== null && budget.tokenBudget > 0) {
55813
+ if (budget.remainingTokens / budget.tokenBudget < threshold) return true;
55814
+ }
55815
+ if (budget.wallClockBudgetMs !== null && budget.remainingWallClockMs !== null && budget.wallClockBudgetMs > 0) {
55816
+ if (budget.remainingWallClockMs / budget.wallClockBudgetMs < threshold) return true;
55817
+ }
55818
+ return false;
55819
+ }
55802
55820
  const GOAL_COMPLETION_REMINDER_NAME = "goal_completion_summary";
55803
55821
  const GOAL_BLOCKED_REMINDER_NAME = "goal_blocked_reason";
55804
55822
  var GoalMode = class {
@@ -55858,6 +55876,7 @@ var GoalMode = class {
55858
55876
  state.wallClockResumedAt = void 0;
55859
55877
  }
55860
55878
  if (record.budgetLimits !== void 0) state.budgetLimits = record.budgetLimits;
55879
+ if (record.objective !== void 0) state.objective = record.objective;
55861
55880
  }
55862
55881
  restoreClear(_record) {
55863
55882
  this.state = void 0;
@@ -55958,6 +55977,23 @@ var GoalMode = class {
55958
55977
  this.appendGoalUpdate({ budgetLimits: state.budgetLimits });
55959
55978
  return this.toSnapshot(state);
55960
55979
  }
55980
+ async updateObjective(input, _actor = "user") {
55981
+ const state = this.requireState();
55982
+ const objective = input.objective.trim();
55983
+ if (objective.length === 0) throw new ScreamError(ErrorCodes.GOAL_OBJECTIVE_EMPTY, "Goal objective cannot be empty");
55984
+ if (state.status === "complete") throw new ScreamError(ErrorCodes.GOAL_STATUS_INVALID, "Cannot update a completed goal.");
55985
+ if (state.status === "blocked") throw new ScreamError(ErrorCodes.GOAL_STATUS_INVALID, "Cannot update a blocked goal. Resume it first.");
55986
+ state.objective = objective;
55987
+ const noteContent = `Goal objective updated by user: ${objective}`.slice(0, MAX_NOTE_LENGTH);
55988
+ state.notes.push({
55989
+ content: noteContent,
55990
+ time: Date.now()
55991
+ });
55992
+ if (state.notes.length > MAX_GOAL_NOTES) state.notes = state.notes.slice(-60);
55993
+ this.persistState(state);
55994
+ this.appendGoalUpdate({ objective });
55995
+ return this.toSnapshot(state);
55996
+ }
55961
55997
  async cancelGoal(actor = "user") {
55962
55998
  const state = this.requireState();
55963
55999
  const snapshot = this.toSnapshot(state);
@@ -56041,7 +56077,7 @@ var GoalMode = class {
56041
56077
  content: trimmed,
56042
56078
  time: Date.now()
56043
56079
  });
56044
- if (state.notes.length > MAX_GOAL_NOTES) state.notes = state.notes.slice(-30);
56080
+ if (state.notes.length > MAX_GOAL_NOTES) state.notes = state.notes.slice(-60);
56045
56081
  this.persistState(state, { silent: true });
56046
56082
  return this.toSnapshot(state);
56047
56083
  }
@@ -56198,6 +56234,8 @@ const UpdateGoalToolInputSchema = z.object({
56198
56234
  reason: z.string().optional().describe("Optional reason for the status change, especially for blocked.")
56199
56235
  }).strict();
56200
56236
  const MAX_GRADER_OUTPUT_CHARS = 4e3;
56237
+ /** Maximum characters of `git diff --stat HEAD` to append to the grader input. */
56238
+ const MAX_DIFF_STAT_CHARS = 2e3;
56201
56239
  function extractRecentOutput(history) {
56202
56240
  const parts = [];
56203
56241
  for (let i = history.length - 1; i >= 0; i--) {
@@ -56210,6 +56248,51 @@ function extractRecentOutput(history) {
56210
56248
  const joined = parts.join("\n\n");
56211
56249
  return joined.length > MAX_GRADER_OUTPUT_CHARS ? `${joined.slice(0, MAX_GRADER_OUTPUT_CHARS)}…` : joined;
56212
56250
  }
56251
+ /**
56252
+ * Append cross-turn working notes to the output seen by the grader. Notes are
56253
+ * written by the agent across continuation turns, so they provide focused
56254
+ * context (key findings, constraints, partial results) without exposing the
56255
+ * full text of previously rejected outputs.
56256
+ */
56257
+ function appendGoalNotes(output, notes) {
56258
+ if (notes.length === 0) return output;
56259
+ return `${output}\n\n## Cross-turn working notes\n${notes.map((note) => `• ${note.content}`).join("\n")}`;
56260
+ }
56261
+ const execFileAsync = promisify(execFile);
56262
+ /**
56263
+ * Fetch a concise git diff stat against HEAD for the current working directory.
56264
+ * Using HEAD includes both staged and unstaged changes, so the reviewer sees
56265
+ * every file touched during the turn even if the agent ran `git add`. Returns
56266
+ * `null` when there are no changes. Throws when git is unavailable or the
56267
+ * directory is not a git repository.
56268
+ */
56269
+ /** Exported for testing only. */
56270
+ async function fetchGitDiffStat(cwd) {
56271
+ const { stdout } = await execFileAsync("git", [
56272
+ "diff",
56273
+ "--stat",
56274
+ "HEAD"
56275
+ ], { cwd });
56276
+ const stat = stdout.trim();
56277
+ return stat.length > 0 ? stat : null;
56278
+ }
56279
+ /**
56280
+ * Append a git diff stat to the grader input so the reviewer can correlate the
56281
+ * agent's claims with the actual files changed during the turn. Very large
56282
+ * stats are truncated to avoid overflowing the reviewer's context window. When
56283
+ * git is unavailable, a note is appended so the reviewer knows why no diff is
56284
+ * present.
56285
+ */
56286
+ async function appendGitDiffStat(output, cwd) {
56287
+ let stat;
56288
+ try {
56289
+ stat = await fetchGitDiffStat(cwd);
56290
+ } catch {
56291
+ return `${output}\n\n## Changes this turn\n(Git diff unavailable — workspace may not be a git repository or git is not installed.)`;
56292
+ }
56293
+ if (stat === null) return output;
56294
+ 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}`;
56295
+ }
56213
56296
  var UpdateGoalTool = class {
56214
56297
  agent;
56215
56298
  grader;
@@ -56256,7 +56339,7 @@ var UpdateGoalTool = class {
56256
56339
  async handleComplete(goal) {
56257
56340
  const goalState = goal.getGoal().goal;
56258
56341
  if (!goalState) return { output: "No active goal." };
56259
- const output = extractRecentOutput(this.agent.context.history);
56342
+ const outputWithContext = await appendGitDiffStat(appendGoalNotes(extractRecentOutput(this.agent.context.history), goalState.notes), this.agent.config?.cwd ?? "");
56260
56343
  try {
56261
56344
  await goal.pauseGoal({ reason: "verifying" }, "system");
56262
56345
  } catch (error) {
@@ -56264,7 +56347,7 @@ var UpdateGoalTool = class {
56264
56347
  }
56265
56348
  let rawGrade;
56266
56349
  try {
56267
- rawGrade = await this.grader(goalState.objective, goalState.completionCriterion, output);
56350
+ rawGrade = await this.grader(goalState.objective, goalState.completionCriterion, outputWithContext);
56268
56351
  } catch (error) {
56269
56352
  const resumeError = await resumeAfterGrading(goal);
56270
56353
  if (resumeError !== void 0) return resumeError;
@@ -56340,7 +56423,7 @@ function errorMessage$4(error) {
56340
56423
  }
56341
56424
  //#endregion
56342
56425
  //#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();
56426
+ 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
56427
  var WriteGoalNoteTool = class {
56345
56428
  agent;
56346
56429
  name = "WriteGoalNote";
@@ -57577,6 +57660,16 @@ function parseMemoryMemos(text) {
57577
57660
  }
57578
57661
  return memos;
57579
57662
  }
57663
+ /**
57664
+ * Strip injected memory-memo content from text before writing it back to the
57665
+ * store, preventing feedback loops where recalled/compacted content gets
57666
+ * re-stored. Removes fenced ```memory-memo blocks (the format emitted by
57667
+ * compaction and exit-time extraction) and any <memories>...</memories>
57668
+ * wrapper an injector might use.
57669
+ */
57670
+ function stripMemoryTags(text) {
57671
+ return text.replaceAll(/```memory-memo[\s\S]*?```/gi, "").replaceAll(/<memories>[\s\S]*?<\/memories>/gi, "").trim();
57672
+ }
57580
57673
  /** System prompt for exit-time extraction — instructs the LLM how to extract. */
57581
57674
  const EXIT_EXTRACTION_SYSTEM_PROMPT = "你是一个任务经验提取助手。任务是从对话记录中识别已完成的任务闭环,提炼出任务经验记录。用对话的主要语言输出(中文对话用中文,英文对话用英文)。只输出指定的 JSON 格式,不要调用任何工具。";
57582
57675
  /** Build the user prompt for exit-time extraction, including a conversation sample. */
@@ -58329,17 +58422,20 @@ var MemoryWriteTool = class {
58329
58422
  };
58330
58423
  const sessionId = this.agent.homedir ? basename$1(dirname$2(dirname$2(this.agent.homedir))) : "unknown";
58331
58424
  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}`));
58425
+ const userNeed = stripMemoryTags(args.userNeed);
58426
+ const approach = stripMemoryTags(args.approach);
58427
+ const outcome = stripMemoryTags(args.outcome);
58428
+ const whatFailed = stripMemoryTags(args.whatFailed ?? "");
58429
+ const whatWorked = stripMemoryTags(args.whatWorked ?? "");
58430
+ const tags = normalizeTags(args.tags !== void 0 && args.tags.length > 0 ? args.tags : generateTags(`${userNeed} ${approach}`));
58335
58431
  const memo = createMemoryMemo({
58336
58432
  sourceSessionId: sessionId,
58337
58433
  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,
58434
+ userNeed,
58435
+ approach,
58436
+ outcome,
58437
+ whatFailed: whatFailed.length === 0 ? "none" : whatFailed,
58438
+ whatWorked: whatWorked.length === 0 ? "none" : whatWorked,
58343
58439
  tags,
58344
58440
  extractionSource: "manual",
58345
58441
  projectDir: this.agent.config.cwd
@@ -62706,20 +62802,20 @@ function isRecord$6(value) {
62706
62802
  var dream_default = "---\nname: dream\ndescription: 整理记忆库 — 合并重复、解决矛盾、清理过时条目\n---\n\n# Dream: 记忆合并整理\n\n用户调用了 `/dream`。你要对全局记忆库进行一次完整的整理和清理。\n\n记忆库是**全局**的,所有会话的记忆都保存在同一个 SQLite 数据库里(`<screamHomeDir>/memory/memos.sqlite`),不是按会话分散存放。\n\n## 前置检查\n\n1. 调用 `MemoryConsolidatePlan` 工具获取整理计划。\n - 如果返回\"记忆库为空\",告知用户\"记忆库为空,无需整理\"并停止。\n - 否则你会得到一个 JSON 计划,包含:duplicateGroups(重复组)、resolved(已完成条目)、stale(过时条目)、summary(统计)。\n\n2. 不要直接修改记忆文件。所有删除和写入都通过 `MemoryConsolidateApply` 工具完成。\n\n## 整理计划展示\n\n把计划转换成用户可读的格式:\n\n```\n## Dream 整理计划\n\n### 概况\n- 当前共 X 条记忆\n- 重复组:N 组\n- 建议删除:M 条(已完成 + 过时)\n- 整理后预计:Y 条\n\n### 重复合并(N 组)\n**组 1: 修复登录 token 刷新**\n- memo-abc123 (2026-05-01, 完成) — \"登录页 token 过期需要手动刷新\"\n- memo-def456 (2026-05-10, 完成) — \"登录 token 过期问题修复\"\n→ 合并为: \"修复登录页 token 过期问题。方案: 在 axios 拦截器中添加自动 refresh 逻辑...\"\n 结果: 完成\n\n### 建议删除(M 条)\n- memo-xyz789: \"添加暗色模式\" (完成, 2026-04-01) — 已完成超过 2 个月\n- memo-old001: \"尝试某方案\" (放弃, 2026-03-01) — 过时\n\n### 总结\n- 合并: N 组 → 减少 X 条\n- 删除: M 条\n- 整理后: 共 Y 条记忆\n```\n\n用 AskUserQuestion 让用户选择:\n- \"执行整理\" — 按上述计划执行\n- \"仅显示计划\" — 不做修改,直接结束\n- \"取消\"\n\n## 执行整理\n\n如果用户选择\"执行整理\":\n\n1. 直接把 `MemoryConsolidatePlan` 返回的完整 JSON 计划作为参数,调用 `MemoryConsolidateApply`。\n2. 工具会自动完成以下操作:\n - 删除重复组中的原记忆\n - 为每组重复记忆追加一条合并后的新记忆(字段格式由工具保证正确)\n - 删除已完成和过时的记忆\n - 更新 `dream-lock.json`,重置建议计数器\n3. 向用户报告工具返回的结果:\"已删除 X 条,创建 Y 条合并记忆。记忆库整理完成。\"\n\n## 重要规则\n\n- **仅合并高度重复的记忆**。只有当两条记忆描述的是同一件事、同一个问题、同一个修复方案时才可合并。如果只是主题相关但细节不同(如两个不同的 bug、两个不同的优化方向),**绝对不能合并**。宁可多保留十条,不可误删一条。\n- 如果 `MemoryConsolidatePlan` 标记的某组重复实际上并不是同一件事,在展示计划时把它从\"重复合并\"里去掉,只保留你确信重复的组,再把精简后的计划传给 `MemoryConsolidateApply`。\n- 不确定时保留原文,不要猜测删除。\n- 操作前必须得到用户确认,不能擅自执行。\n- 不要手动写 Bash 去修改 `entries.jsonl`,统一通过 `MemoryConsolidateApply` 工具执行。\n";
62707
62803
  //#endregion
62708
62804
  //#region ../../packages/agent-core/src/skill/builtin/dream.ts
62709
- const PSEUDO_PATH$1 = "builtin://dream";
62710
- const parsed$1 = parseSkillText({
62805
+ const PSEUDO_PATH$2 = "builtin://dream";
62806
+ const parsed$2 = parseSkillText({
62711
62807
  skillMdPath: "/builtin/skills/dream.md",
62712
62808
  skillDirName: "dream",
62713
62809
  source: "builtin",
62714
62810
  text: dream_default
62715
62811
  });
62716
62812
  const DREAM_SKILL = {
62717
- ...parsed$1,
62718
- path: PSEUDO_PATH$1,
62719
- dir: PSEUDO_PATH$1,
62813
+ ...parsed$2,
62814
+ path: PSEUDO_PATH$2,
62815
+ dir: PSEUDO_PATH$2,
62720
62816
  metadata: {
62721
- ...parsed$1.metadata,
62722
- type: parsed$1.metadata.type ?? "inline",
62817
+ ...parsed$2.metadata,
62818
+ type: parsed$2.metadata.type ?? "inline",
62723
62819
  disableModelInvocation: true
62724
62820
  }
62725
62821
  };
@@ -62728,14 +62824,36 @@ const DREAM_SKILL = {
62728
62824
  var make_skill_default = "---\nname: make-skill\ndescription: 从当前会话上下文沉淀工作流为可复用 Skill\n---\n\n# Make Skill: 从上下文提炼 Skill\n\n用户调用了 `/make-skill`。你的任务是通过对话引导用户,把当前会话中解决问题的方式沉淀为一个可复用的 Scream Code Skill,并安装到插件中心。\n\n## 激活参数\n\n本次激活的参数:\n\n```json\n$ARGUMENTS\n```\n\n- `initialRequest`:用户输入 `/make-skill` 时附带的一句话描述,可能为空。\n\n## 工作方式\n\n这不是一次性任务。你需要通过**多轮对话**澄清以下信息,每一轮只问一个问题,等用户回答后再进入下一阶段:\n\n1. Skill 类型(`workflow` / `code-pattern` / `troubleshooting` / `tool-chain` / `custom`)\n2. Skill 名称(kebab-case)\n3. 这个 Skill 主要解决什么问题\n4. 希望重点关注哪些内容\n5. 生成草案并确认安装\n\n## 阶段判断\n\n根据当前对话历史判断你处于哪个阶段:\n\n- **阶段 0**:本 Skill 刚激活,还没有问过任何问题。先分析会话上下文和 `initialRequest`,然后用 `AskUserQuestion` 询问 Skill 类型。\n- **阶段 1**:已经确定了 Skill 类型,但还没有确定名称。根据类型和上下文建议一个 kebab-case 名称,用 `AskUserQuestion` 询问用户是否接受或修改。\n- **阶段 2**:已经确定了名称,但还没有明确解决的问题。根据上下文总结一句话描述,用 `AskUserQuestion` 询问用户是否接受或修改。\n- **阶段 3**:已经确定了问题,但还没有明确关注重点。给出 2-4 个关注重点建议,用 `AskUserQuestion` 让用户选择或输入。\n- **阶段 4**:类型、名称、问题、重点都已确定。调用 `MakeSkillPlanTool` 生成草案,展示给用户,并用 `AskUserQuestion` 询问是否确认安装。\n- **阶段 5**:用户已确认安装。调用 `MakeSkillApplyTool` 写入插件中心。\n\n如何判断“已确定”:历史消息中已经有你提出的 `AskUserQuestion` 以及用户给出的明确回答(或选择了你建议的选项)。\n\n## 每轮提问规范\n\n除非处于阶段 4/5,否则**每轮必须也只允许使用一次 `AskUserQuestion`**。不要直接用普通文本提问,这样无法给用户结构化选项。\n\n### 阶段 0:询问 Skill 类型\n\n先分析当前会话上下文,判断最可能想沉淀什么。给出 2-4 个建议选项,不要列出全部 5 种。把最相关的放在最前面,并标记 `(Recommended)`。\n\n示例问题:\n\n```json\n{\n \"questions\": [\n {\n \"question\": \"根据刚才的会话,你想把什么沉淀成 Skill?\",\n \"header\": \"类型\",\n \"options\": [\n { \"label\": \"Code pattern (Recommended)\", \"description\": \"把 React 表单验证的代码模式提炼为可复用模板\" },\n { \"label\": \"Workflow\", \"description\": \"把解决表单验证问题的步骤沉淀为流程\" },\n { \"label\": \"Troubleshooting\", \"description\": \"把常见验证错误排查过程沉淀为诊断指南\" }\n ],\n \"multi_select\": false\n }\n ]\n}\n```\n\n注意:系统会自动添加 \"Other\" 选项,**不要自己添加**。如果用户想选未列出的类型(如 `tool-chain` 或 `custom`),他们会通过 Other 输入。\n\n### 阶段 1:询问 Skill 名称\n\n根据已确定的类型和上下文,建议一个 kebab-case 名称。用 `AskUserQuestion` 让用户接受、修改或自己输入。\n\n示例:\n\n```json\n{\n \"questions\": [\n {\n \"question\": \"建议把这个 Skill 命名为 react-form-validate,是否接受?\",\n \"header\": \"名称\",\n \"options\": [\n { \"label\": \"使用 react-form-validate (Recommended)\", \"description\": \"简洁直观,符合 kebab-case\" },\n { \"label\": \"换一个名称\", \"description\": \"我给出其他建议\" },\n { \"label\": \"我自己输入\", \"description\": \"手动指定名称\" }\n ],\n \"multi_select\": false\n }\n ]\n}\n```\n\n如果用户选择“换一个名称”或“我自己输入”,你需要在下一轮继续用 `AskUserQuestion` 给出新建议或请求输入。\n\n### 阶段 2:询问解决的问题\n\n根据上下文总结一句话描述,让用户接受、修改或自己输入。\n\n示例:\n\n```json\n{\n \"questions\": [\n {\n \"question\": \"这个 Skill 主要用于:在 React 中用 zod + react-hook-form 实现表单验证。是否准确?\",\n \"header\": \"用途\",\n \"options\": [\n { \"label\": \"准确 (Recommended)\", \"description\": \"保持这个描述\" },\n { \"label\": \"不够准确\", \"description\": \"我帮你调整\" },\n { \"label\": \"我自己描述\", \"description\": \"手动输入用途\" }\n ],\n \"multi_select\": false\n }\n ]\n}\n```\n\n### 阶段 3:询问关注重点\n\n给出 2-4 个基于上下文的关注重点建议。\n\n示例:\n\n```json\n{\n \"questions\": [\n {\n \"question\": \"生成时希望重点关注哪些方面?\",\n \"header\": \"重点\",\n \"options\": [\n { \"label\": \"验证 schema 定义 (Recommended)\", \"description\": \"重点提取 zod schema 的编写模式\" },\n { \"label\": \"错误处理与提示\", \"description\": \"重点提取错误展示和反馈逻辑\" },\n { \"label\": \"组件绑定方式\", \"description\": \"重点提取 react-hook-form 的绑定代码\" }\n ],\n \"multi_select\": true\n }\n ]\n}\n```\n\n### 阶段 4:展示草案并确认\n\n调用 `MakeSkillPlanTool`,参数:\n\n- `type`:阶段 0 确定的类型\n- `nameHint`:阶段 1 确定的名称\n- `purpose`:阶段 2 确定的问题描述\n- `focus`:阶段 3 确定的关注重点(多选用逗号连接成字符串)\n\n工具返回 JSON 后,用中文清晰展示:\n\n- Skill 名称和描述\n- 文件清单(至少包含 `SKILL.md`)\n- 适用场景\n- 安装位置:`~/.scream-code/plugins/managed/<name>/`,可通过 `/plugin` 管理\n\n然后用 `AskUserQuestion` 询问:\n\n```json\n{\n \"questions\": [\n {\n \"question\": \"是否安装这个 Skill?\",\n \"header\": \"确认\",\n \"options\": [\n { \"label\": \"确认安装 (Recommended)\", \"description\": \"写入插件中心并在新会话中可用\" },\n { \"label\": \"取消\", \"description\": \"不保存任何内容\" }\n ],\n \"multi_select\": false\n }\n ]\n}\n```\n\n### 阶段 5:执行安装\n\n如果用户选择“确认安装”,调用 `MakeSkillApplyTool`,传入工具返回的完整草案 JSON(`name`、`description`、`content`、`files`)。\n\n把结果告知用户,例如:\n\n- 成功:`Skill 已安装到 ~/.scream-code/plugins/managed/<name>/。新会话中可通过 /<name> 调用。`\n- 失败:说明错误原因,不要重试。\n\n## 重要规则\n\n- 除了阶段 4 的展示文本外,**所有澄清问题都必须通过 `AskUserQuestion` 工具提出**,不要直接用文本回复提问。\n- 每轮只能问一个问题。等用户回答后再推进到下一阶段。\n- 不要在没有调用 `MakeSkillApplyTool` 的情况下直接写文件。\n- 如果用户选择“取消”或关闭问题,礼貌地告知已取消,不做任何修改。\n- 如果 `MakeSkillApplyTool` 返回错误(例如同名 Skill 已存在),向用户说明错误并停止,不要重试。\n- 新安装的 Skill 只在**新会话**中可用;当前会话不会立即加载它。\n- 安装后的 Skill 会出现在 `/plugin` 插件中心里,用户可以统一启用、禁用或卸载。\n";
62729
62825
  //#endregion
62730
62826
  //#region ../../packages/agent-core/src/skill/builtin/make-skill.ts
62731
- const PSEUDO_PATH = "builtin://make-skill";
62732
- const parsed = parseSkillText({
62827
+ const PSEUDO_PATH$1 = "builtin://make-skill";
62828
+ const parsed$1 = parseSkillText({
62733
62829
  skillMdPath: "/builtin/skills/make-skill.md",
62734
62830
  skillDirName: "make-skill",
62735
62831
  source: "builtin",
62736
62832
  text: make_skill_default
62737
62833
  });
62738
62834
  const MAKE_SKILL_SKILL = {
62835
+ ...parsed$1,
62836
+ path: PSEUDO_PATH$1,
62837
+ dir: PSEUDO_PATH$1,
62838
+ metadata: {
62839
+ ...parsed$1.metadata,
62840
+ type: parsed$1.metadata.type ?? "inline",
62841
+ disableModelInvocation: true
62842
+ }
62843
+ };
62844
+ //#endregion
62845
+ //#region ../../packages/agent-core/src/skill/builtin/tool-prompt-optimization/SKILL.md
62846
+ var SKILL_default = "---\nname: tool-prompt-optimization\ndescription: Audit and trim tool prompt text that duplicates information already inferable from the tool's JSON schema, reducing system prompt token cost.\n---\n\n# Tool Prompt Optimization\n\nA meta-skill for cutting system-prompt token waste: tool `description` text often\nrestates field names, types, and constraints that the Zod/JSON schema already\nencodes. Use a probe to measure the overlap, then trim what the model can infer\nfrom the schema alone.\n\n## When to use\n\n- System prompt token cost is high and tool descriptions are a meaningful share.\n- A tool's `description` repeats field names or types already in its schema.\n- Auditing tool definitions for redundancy before a release.\n\n## How to audit\n\n1. For each tool, lay the `description` text next to the Zod/JSON schema fields.\n2. Flag sentences that merely restate field names, types, or constraints already\n encoded in the schema (e.g. \"command is a string\" when the schema already\n declares `z.string()`).\n3. **Probe**: ask the model \"Given only the schema (no description), which\n behaviors and constraints can you infer?\" The intersection is pure redundancy.\n4. Trim description text the model already infers from the schema alone.\n5. **Keep** what the schema cannot express:\n - edge cases, gotchas, and ordering requirements\n - cross-tool interactions and precedence rules\n - safety-critical warnings and permission notes\n - examples that disambiguate ambiguous schema fields\n\n## Rules\n\n- Never remove safety-critical warnings or permission notes.\n- Never remove examples that clarify ambiguous schema fields.\n- Before deleting a sentence, run `git blame` to check whether it was added to\n fix a specific bug; if so, keep it (or confirm the bug is gone).\n- Measure the before/after token count to confirm savings actually materialized.\n- Prefer precise constraints over vague prose; if a constraint is enforceable in\n the schema (e.g. `.min(1)`), move it there instead of describing it in text.\n";
62847
+ //#endregion
62848
+ //#region ../../packages/agent-core/src/skill/builtin/tool-prompt-optimization.ts
62849
+ const PSEUDO_PATH = "builtin://tool-prompt-optimization";
62850
+ const parsed = parseSkillText({
62851
+ skillMdPath: "/builtin/skills/tool-prompt-optimization/SKILL.md",
62852
+ skillDirName: "tool-prompt-optimization",
62853
+ source: "builtin",
62854
+ text: SKILL_default
62855
+ });
62856
+ const TOOL_PROMPT_OPTIMIZATION_SKILL = {
62739
62857
  ...parsed,
62740
62858
  path: PSEUDO_PATH,
62741
62859
  dir: PSEUDO_PATH,
@@ -62750,6 +62868,7 @@ const MAKE_SKILL_SKILL = {
62750
62868
  function registerBuiltinSkills(registry) {
62751
62869
  registry.registerBuiltinSkill(DREAM_SKILL);
62752
62870
  registry.registerBuiltinSkill(MAKE_SKILL_SKILL);
62871
+ registry.registerBuiltinSkill(TOOL_PROMPT_OPTIMIZATION_SKILL);
62753
62872
  }
62754
62873
  //#endregion
62755
62874
  //#region ../../packages/agent-core/src/skill/scanner.ts
@@ -75307,17 +75426,25 @@ var ToolResultBuilder = class {
75307
75426
  maxChars;
75308
75427
  maxTailChars;
75309
75428
  maxLineLength;
75429
+ artifactSink;
75430
+ maxFullOutputChars;
75310
75431
  buffer = [];
75311
75432
  nCharsValue = 0;
75312
75433
  truncationHappened = false;
75313
75434
  headTruncated = false;
75314
75435
  tailBuf = [];
75315
75436
  tailCharsValue = 0;
75437
+ fullOutput;
75438
+ fullOutputChars = 0;
75439
+ totalLinesWritten = 0;
75316
75440
  constructor(options = {}) {
75317
75441
  this.maxChars = options.maxChars ?? DEFAULT_MAX_CHARS;
75318
75442
  this.maxTailChars = options.maxTailChars ?? DEFAULT_TAIL_CHARS;
75319
75443
  this.maxLineLength = options.maxLineLength === void 0 ? DEFAULT_MAX_LINE_LENGTH : options.maxLineLength;
75320
75444
  if (this.maxLineLength !== null && this.maxLineLength <= 14) throw new Error("maxLineLength must be greater than the truncation marker length.");
75445
+ this.artifactSink = options.artifactSink;
75446
+ this.maxFullOutputChars = options.maxFullOutputChars ?? 1e6;
75447
+ if (this.artifactSink !== void 0) this.fullOutput = [];
75321
75448
  }
75322
75449
  get nChars() {
75323
75450
  return this.nCharsValue + this.tailCharsValue;
@@ -75327,12 +75454,19 @@ var ToolResultBuilder = class {
75327
75454
  if (!this.headTruncated || this.tailCharsValue === 0) return head;
75328
75455
  this.trimTail();
75329
75456
  const tail = this.tailBuf.join("");
75330
- return `${head}${head.endsWith("\n") ? "" : "\n"}${TRUNCATION_MARKER}\n${tail}`;
75457
+ const separator = head.endsWith("\n") ? "" : "\n";
75458
+ const elided = this.computeElidedLines(head, tail);
75459
+ return `${head}${separator}${elided > 0 ? `[…${String(elided)} lines elided…]\n${TRUNCATION_MARKER}` : TRUNCATION_MARKER}\n${tail}`;
75331
75460
  }
75332
75461
  write(text) {
75333
75462
  if (text.length === 0) return 0;
75334
75463
  const lines = text.match(/[^\r\n]*(?:\r\n|[\n\r])|[^\r\n]+/g) ?? [];
75335
75464
  if (lines.length === 0) return 0;
75465
+ this.totalLinesWritten += lines.length;
75466
+ if (this.fullOutput !== void 0 && this.fullOutputChars < this.maxFullOutputChars) {
75467
+ this.fullOutput.push(text);
75468
+ this.fullOutputChars += text.length;
75469
+ }
75336
75470
  let charsWritten = 0;
75337
75471
  for (const originalLine of lines) if (this.nCharsValue < this.maxChars) {
75338
75472
  const remainingChars = this.maxChars - this.nCharsValue;
@@ -75380,11 +75514,36 @@ var ToolResultBuilder = class {
75380
75514
  this.tailBuf.push(trimmed);
75381
75515
  this.tailCharsValue = trimmed.length;
75382
75516
  }
75383
- ok(message = "", options = {}) {
75517
+ computeElidedLines(head, tail) {
75518
+ if (this.totalLinesWritten === 0) return 0;
75519
+ if (this.fullOutput !== void 0) {
75520
+ const total = countLines(this.fullOutput.join(""));
75521
+ return Math.max(0, total - countLines(head) - countLines(tail));
75522
+ }
75523
+ return Math.max(0, this.totalLinesWritten - countLines(head) - countLines(tail));
75524
+ }
75525
+ async maybeWriteArtifact() {
75526
+ if (this.artifactSink === void 0 || this.fullOutput === void 0) return void 0;
75527
+ if (!this.truncationHappened) return void 0;
75528
+ const full = this.fullOutput.join("");
75529
+ if (full.length === 0) return void 0;
75530
+ try {
75531
+ return await this.artifactSink(full);
75532
+ } catch {
75533
+ return;
75534
+ }
75535
+ }
75536
+ appendArtifactRef(output, ref) {
75537
+ const line = `[full output saved: ${ref}]`;
75538
+ return output.length === 0 ? line : output.endsWith("\n") ? `${output}${line}` : `${output}\n${line}`;
75539
+ }
75540
+ async ok(message = "", options = {}) {
75384
75541
  let finalMessage = message;
75385
75542
  if (finalMessage.length > 0 && !finalMessage.endsWith(".")) finalMessage += ".";
75386
75543
  if (this.truncationHappened) finalMessage = finalMessage.length === 0 ? TRUNCATION_MESSAGE : `${finalMessage} ${TRUNCATION_MESSAGE}`;
75387
- const output = this.toString();
75544
+ const baseOutput = this.toString();
75545
+ const artifactRef = await this.maybeWriteArtifact();
75546
+ const output = artifactRef === void 0 ? baseOutput : this.appendArtifactRef(baseOutput, artifactRef);
75388
75547
  return {
75389
75548
  isError: false,
75390
75549
  output: finalMessage.length > 0 && (this.truncationHappened || output.length === 0) ? output.length === 0 ? finalMessage : output.endsWith("\n") ? `${output}${finalMessage}` : `${output}\n${finalMessage}` : output,
@@ -75393,9 +75552,11 @@ var ToolResultBuilder = class {
75393
75552
  brief: options.brief
75394
75553
  };
75395
75554
  }
75396
- error(message, options = {}) {
75555
+ async error(message, options = {}) {
75397
75556
  const finalMessage = this.truncationHappened ? message.length === 0 ? TRUNCATION_MESSAGE : `${message} ${TRUNCATION_MESSAGE}` : message;
75398
- const output = this.toString();
75557
+ const baseOutput = this.toString();
75558
+ const artifactRef = await this.maybeWriteArtifact();
75559
+ const output = artifactRef === void 0 ? baseOutput : this.appendArtifactRef(baseOutput, artifactRef);
75399
75560
  return {
75400
75561
  isError: true,
75401
75562
  output: finalMessage.length === 0 ? output : output.length === 0 ? finalMessage : output.endsWith("\n") ? `${output}${finalMessage}` : `${output}\n${finalMessage}`,
@@ -75405,6 +75566,10 @@ var ToolResultBuilder = class {
75405
75566
  };
75406
75567
  }
75407
75568
  };
75569
+ function countLines(text) {
75570
+ if (text.length === 0) return 0;
75571
+ return text.split("\n").length - (text.endsWith("\n") ? 1 : 0);
75572
+ }
75408
75573
  //#endregion
75409
75574
  //#region ../../packages/agent-core/src/tools/builtin/file/grep.md
75410
75575
  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";
@@ -75594,7 +75759,7 @@ var GrepTool = class {
75594
75759
  const combined = visibleBody === "" && messages.length === 0 ? emptyResultMessage : messages.length > 0 ? visibleBody === "" ? messages.join("\n") : `${visibleBody}\n${messages.join("\n")}` : visibleBody;
75595
75760
  const builder = new ToolResultBuilder();
75596
75761
  builder.write(combined);
75597
- const result = builder.ok(sideChannelMessages.join("\n"));
75762
+ const result = await builder.ok(sideChannelMessages.join("\n"));
75598
75763
  const display = buildSearchResultsDisplay(args, limited, mode, contentIncludesLineNumbers);
75599
75764
  if (display === void 0) return result;
75600
75765
  if (result.isError === true) return result;
@@ -77814,6 +77979,30 @@ function truncateUtf8$1(input, maxBytes) {
77814
77979
  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
77980
  //#endregion
77816
77981
  //#region ../../packages/agent-core/src/tools/builtin/shell/bash.ts
77982
+ /**
77983
+ * BashTool — execute shell commands.
77984
+ *
77985
+ * Invokes bash (POSIX) according to an injected `Environment`. On Windows
77986
+ * the shell is Git Bash; the path is resolved by `detectEnvironment`.
77987
+ *
77988
+ * Dependencies injected via constructor:
77989
+ * - `Jian` — shell execution abstraction (exec / execWithEnv)
77990
+ * - `cwd` — default working directory for commands
77991
+ * - `Environment` — cross-platform probe (shellName / shellPath)
77992
+ * - `BackgroundProcessManager?` — optional: required iff run_in_background=true
77993
+ *
77994
+ * Execution goes through Jian, never directly via node:child_process.
77995
+ *
77996
+ * Hardening:
77997
+ * - `args.timeout` (seconds) and the ambient `signal` both drive
77998
+ * `Promise.race`; fire-a-kill on either edge.
77999
+ * - stdin is closed immediately so interactive commands (`cat`, `read`,
78000
+ * `python -c 'input()'`) receive EOF instead of hanging.
78001
+ * - Two-phase kill: SIGTERM → 5s grace → SIGKILL (Jian honours this
78002
+ * contract cross-platform).
78003
+ * - stdout/stderr stream into ToolResultBuilder; excess is replaced with a
78004
+ * truncation marker so a runaway command cannot OOM the host.
78005
+ */
77817
78006
  const MS_PER_SECOND = 1e3;
77818
78007
  const DEFAULT_TIMEOUT_S = 60;
77819
78008
  const MAX_TIMEOUT_S = 300;
@@ -78064,7 +78253,11 @@ var BashTool = class {
78064
78253
  killProc();
78065
78254
  }, timeoutMs);
78066
78255
  try {
78067
- const builder = new ToolResultBuilder();
78256
+ const builder = new ToolResultBuilder({ artifactSink: async (fullOutput) => {
78257
+ const artifactPath = join(tmpdir(), `scream-bash-output-${randomUUID()}.log`);
78258
+ await writeFile(artifactPath, fullOutput, "utf8");
78259
+ return artifactPath;
78260
+ } });
78068
78261
  const [, exitCode] = await Promise.all([Promise.all([readStreamIntoBuilder(proc.stdout, builder), readStreamIntoBuilder(proc.stderr, builder)]), proc.wait()]);
78069
78262
  if (timedOut) {
78070
78263
  const timeoutLabel = timeoutMs % 1e3 === 0 ? `${String(timeoutMs / 1e3)}s` : `${String(timeoutMs)}ms`;
@@ -79655,7 +79848,8 @@ const DEFAULT_CONFIG = {
79655
79848
  minContentTokens: 100,
79656
79849
  minContextUsageRatio: .5,
79657
79850
  truncatedMarker: "[Old tool result content cleared]",
79658
- uselessMarker: "[Uneventful result elided]"
79851
+ uselessMarker: "[Uneventful result elided]",
79852
+ noMatchesMarker: "[no matches]"
79659
79853
  };
79660
79854
  /**
79661
79855
  * Compute the cutoff index: everything at index < cutoff is eligible for
@@ -79676,30 +79870,107 @@ function computeCutoff(messages, config) {
79676
79870
  }
79677
79871
  return cutoff;
79678
79872
  }
79873
+ /** Tool names whose results are file reads eligible for supersede pruning. */
79874
+ const READ_TOOL_NAMES = new Set(["Read", "ReadGroup"]);
79875
+ /** Tool names whose empty results can be elided as a no-match marker. */
79876
+ const SEARCH_TOOL_NAMES = new Set(["Grep", "Glob"]);
79877
+ /** Exact tool-result texts that indicate a zero-match search result. */
79878
+ const ZERO_MATCH_TEXTS = new Set(["No matches found", "No non-sensitive matches found"]);
79679
79879
  /**
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).
79880
+ * Parse a tool call's arguments JSON. Returns undefined for null or malformed
79881
+ * JSON - persisted history can carry truncated arguments that must not crash
79882
+ * compaction. `ToolCall.arguments` is a JSON string (or null), never a parsed
79883
+ * object, so every consumer must go through this helper.
79884
+ */
79885
+ function parseToolCallArguments$1(argumentsJson) {
79886
+ if (argumentsJson === null || argumentsJson === void 0) return void 0;
79887
+ try {
79888
+ const parsed = JSON.parse(argumentsJson);
79889
+ return typeof parsed === "object" && parsed !== null ? parsed : void 0;
79890
+ } catch {
79891
+ return;
79892
+ }
79893
+ }
79894
+ /**
79895
+ * Extract the file paths targeted by a read tool call. `Read` carries a
79896
+ * single `path`; `ReadGroup` carries a `paths` array. Returns an empty array
79897
+ * for non-read tools or calls whose arguments don't yield usable paths.
79898
+ */
79899
+ function extractReadFilePaths(name, args) {
79900
+ if (args === void 0) return [];
79901
+ if (name === "Read") {
79902
+ const path = typeof args["path"] === "string" ? args["path"] : void 0;
79903
+ return path !== void 0 && path.length > 0 ? [path] : [];
79904
+ }
79905
+ if (name === "ReadGroup") {
79906
+ const paths = args["paths"];
79907
+ if (!Array.isArray(paths)) return [];
79908
+ return paths.filter((p) => typeof p === "string" && p.length > 0);
79909
+ }
79910
+ return [];
79911
+ }
79912
+ /** Concatenate the `text` parts of a message's content into a single string. */
79913
+ function extractTextContent(content) {
79914
+ let text = "";
79915
+ for (const part of content) if (typeof part === "object" && part !== null && part.type === "text") text += part.text;
79916
+ return text;
79917
+ }
79918
+ /** Build a toolCallId -> tool-name map by scanning assistant messages. */
79919
+ function buildToolCallNameMap(messages) {
79920
+ const names = /* @__PURE__ */ new Map();
79921
+ for (const msg of messages) {
79922
+ if (msg.role !== "assistant") continue;
79923
+ for (const tc of msg.toolCalls) names.set(tc.id, tc.name);
79924
+ }
79925
+ return names;
79926
+ }
79927
+ /**
79928
+ * Whether a tool result is a zero-match Grep/Glob result eligible for elision.
79929
+ * Only exact-match the canonical empty-result texts so results carrying extra
79930
+ * information (sensitive-file filter notices, pagination notices, errors) are
79931
+ * preserved verbatim.
79932
+ */
79933
+ function isZeroMatchSearchResult(toolName, content) {
79934
+ if (toolName === void 0 || !SEARCH_TOOL_NAMES.has(toolName)) return false;
79935
+ const text = extractTextContent(content).trim();
79936
+ return ZERO_MATCH_TEXTS.has(text);
79937
+ }
79938
+ /**
79939
+ * Walk the message list and find Read/ReadGroup tool calls whose file paths
79940
+ * were superseded by a later read of the same path. Returns a map from the
79941
+ * superseded tool call's ID to the list of file paths covered by the newer
79942
+ * read (a ReadGroup can cover several).
79683
79943
  *
79684
- * Only considers tool results before the cutoff line newer reads are
79685
- * protected and their results are kept verbatim.
79944
+ * Only considers tool results before the cutoff line - newer reads are
79945
+ * protected and their results are kept verbatim. The comparison uses the raw
79946
+ * path strings from the tool arguments; path canonicalization would need
79947
+ * workspace context the compaction layer doesn't have, so the same file read
79948
+ * via two different spellings is treated as two different files (safe: it
79949
+ * just misses a supersede opportunity rather than dropping a distinct result).
79686
79950
  */
79687
79951
  function findSupersededPaths(messages, cutoff) {
79688
79952
  const superseded = /* @__PURE__ */ new Map();
79689
- const readCalls = /* @__PURE__ */ new Map();
79953
+ const latestReadByPath = /* @__PURE__ */ new Map();
79690
79954
  for (let i = 0; i < messages.length; i++) {
79691
79955
  const msg = messages[i];
79692
79956
  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
- }
79957
+ if (msg.role !== "assistant" || msg.toolCalls.length === 0) continue;
79958
+ for (const tc of msg.toolCalls) {
79959
+ if (!READ_TOOL_NAMES.has(tc.name)) continue;
79960
+ const args = parseToolCallArguments$1(tc.arguments);
79961
+ const paths = extractReadFilePaths(tc.name, args);
79962
+ if (paths.length === 0) continue;
79963
+ for (const filePath of paths) {
79964
+ const prev = latestReadByPath.get(filePath);
79965
+ if (prev !== void 0 && prev.index < cutoff) {
79966
+ const existing = superseded.get(prev.toolCallId);
79967
+ if (existing === void 0) superseded.set(prev.toolCallId, [filePath]);
79968
+ else if (!existing.includes(filePath)) superseded.set(prev.toolCallId, [...existing, filePath]);
79969
+ }
79970
+ latestReadByPath.set(filePath, {
79971
+ toolCallId: tc.id,
79972
+ index: i
79973
+ });
79703
79974
  }
79704
79975
  }
79705
79976
  }
@@ -79753,20 +80024,26 @@ var MicroCompaction = class {
79753
80024
  }
79754
80025
  /**
79755
80026
  * 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.
80027
+ * before the cutoff line with truncated markers. Read/ReadGroup results
80028
+ * for files that were re-read later get a supersede marker (listing the
80029
+ * covered paths) so the model knows the old content is stale. Zero-match
80030
+ * Grep/Glob results are elided to a short `[no matches]` notice regardless
80031
+ * of size. Tool results explicitly marked useless are elided with a short
80032
+ * notice regardless of size, since they carry no actionable information.
79761
80033
  */
79762
80034
  compact(messages) {
79763
80035
  const config = this.config;
79764
80036
  const superseded = findSupersededPaths(messages, this.cutoff);
80037
+ const toolNames = buildToolCallNameMap(messages);
79765
80038
  const result = [];
79766
80039
  let i = 0;
79767
80040
  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;
80041
+ const isOld = i < this.cutoff;
80042
+ const toolCallId = msg.toolCallId;
80043
+ const isTool = msg.role === "tool" && toolCallId !== void 0;
80044
+ const isUseless = isOld && isTool && msg.useless === true;
80045
+ const isZeroMatch = isOld && isTool && toolCallId !== void 0 && isZeroMatchSearchResult(toolNames.get(toolCallId), msg.content);
80046
+ const isOversizedTruncatable = isOld && isTool && estimateTokensForMessages([msg]) >= config.minContentTokens;
79770
80047
  if (isUseless) result.push({
79771
80048
  ...msg,
79772
80049
  content: [{
@@ -79774,8 +80051,16 @@ var MicroCompaction = class {
79774
80051
  text: config.uselessMarker
79775
80052
  }]
79776
80053
  });
80054
+ else if (isZeroMatch) result.push({
80055
+ ...msg,
80056
+ content: [{
80057
+ type: "text",
80058
+ text: config.noMatchesMarker
80059
+ }]
80060
+ });
79777
80061
  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;
80062
+ const paths = toolCallId !== void 0 ? superseded.get(toolCallId) : void 0;
80063
+ const marker = paths !== void 0 && paths.length > 0 ? `[Superseded by a newer read of ${paths.join(", ")}]` : config.truncatedMarker;
79779
80064
  result.push({
79780
80065
  ...msg,
79781
80066
  content: [{
@@ -79800,20 +80085,28 @@ var MicroCompaction = class {
79800
80085
  measureEffect(messages, cutoff) {
79801
80086
  let markerTokenCount;
79802
80087
  let uselessMarkerTokenCount;
80088
+ let noMatchesMarkerTokenCount;
79803
80089
  let truncatedToolResultCount = 0;
79804
80090
  let beforeTokens = 0;
79805
80091
  let afterTokens = 0;
80092
+ const toolNames = buildToolCallNameMap(messages);
79806
80093
  for (let i = 0; i < messages.length && i < cutoff; i++) {
79807
80094
  const message = messages[i];
79808
80095
  if (message?.role !== "tool" || message.toolCallId === void 0) continue;
79809
80096
  const contentTokens = estimateTokensForMessages([message]);
79810
80097
  const isUseless = message.useless === true;
79811
- if (!isUseless && contentTokens < this.config.minContentTokens) continue;
80098
+ const isZeroMatch = isZeroMatchSearchResult(toolNames.get(message.toolCallId), message.content);
80099
+ if (!isUseless && !isZeroMatch && contentTokens < this.config.minContentTokens) continue;
79812
80100
  if (isUseless) {
79813
80101
  uselessMarkerTokenCount ??= estimateTokens$1(this.config.uselessMarker);
79814
80102
  truncatedToolResultCount += 1;
79815
80103
  beforeTokens += contentTokens;
79816
80104
  afterTokens += uselessMarkerTokenCount;
80105
+ } else if (isZeroMatch) {
80106
+ noMatchesMarkerTokenCount ??= estimateTokens$1(this.config.noMatchesMarker);
80107
+ truncatedToolResultCount += 1;
80108
+ beforeTokens += contentTokens;
80109
+ afterTokens += noMatchesMarkerTokenCount;
79817
80110
  } else {
79818
80111
  markerTokenCount ??= estimateTokens$1(this.config.truncatedMarker);
79819
80112
  truncatedToolResultCount += 1;
@@ -80822,6 +81115,71 @@ var ConfigState = class {
80822
81115
  }
80823
81116
  };
80824
81117
  //#endregion
81118
+ //#region ../../packages/agent-core/src/agent/context/prefix-fingerprint.ts
81119
+ /**
81120
+ * Deterministic non-crypto string hash (djb2). Fast and sufficient for change
81121
+ * detection; not used for any security purpose. Returns a compact base36
81122
+ * string so an array of fingerprints stays small.
81123
+ */
81124
+ function hashString(input) {
81125
+ let hash = 5381;
81126
+ for (let i = 0; i < input.length; i++) hash = (hash << 5) + hash + (input.codePointAt(i) ?? 0) | 0;
81127
+ return (hash >>> 0).toString(36);
81128
+ }
81129
+ /**
81130
+ * Stable JSON serialization that sorts object keys so key insertion order
81131
+ * can't affect the fingerprint. Only used for media parts whose payload is
81132
+ * already a stable base64/url string; text/think parts have a dedicated
81133
+ * fast path.
81134
+ */
81135
+ function stableJson(value) {
81136
+ if (value === null || typeof value !== "object") return JSON.stringify(value);
81137
+ if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
81138
+ const obj = value;
81139
+ return `{${Object.keys(obj).toSorted().map((k) => `${JSON.stringify(k)}:${stableJson(obj[k])}`).join(",")}}`;
81140
+ }
81141
+ function serializeContentPart(part) {
81142
+ switch (part.type) {
81143
+ case "text": return `text:${part.text}`;
81144
+ case "think": return `think:${part.think}${part.encrypted !== void 0 ? `\u0003${part.encrypted}` : ""}`;
81145
+ default: return `${part.type}:${stableJson(part)}`;
81146
+ }
81147
+ }
81148
+ function serializeToolCall(tc) {
81149
+ return `function|${tc.id}|${tc.name}|${tc.arguments ?? ""}`;
81150
+ }
81151
+ /**
81152
+ * Serialize a message to a deterministic string covering all
81153
+ * provider-visible bytes. Two messages with the same serialization produce
81154
+ * identical provider bytes (modulo serialization the provider adapter
81155
+ * normalizes). Separator bytes (\x00-\x03) are used so concatenated fields
81156
+ * can't alias.
81157
+ */
81158
+ function serializeMessage$1(message) {
81159
+ const content = message.content.map(serializeContentPart).join("");
81160
+ const toolCalls = message.toolCalls.map(serializeToolCall).join("");
81161
+ return `${message.role}\u0000${message.name ?? ""}\u0000${message.toolCallId ?? ""}\u0000${content}\u0000${toolCalls}`;
81162
+ }
81163
+ /** Per-message fingerprint. Equal fingerprints => equal provider bytes. */
81164
+ function messageFingerprint(message) {
81165
+ return hashString(serializeMessage$1(message));
81166
+ }
81167
+ /**
81168
+ * Longest common prefix length by provider-visible bytes. This is the number
81169
+ * of leading messages a provider prompt cache could reuse from the previous
81170
+ * call. When this is less than the previous call's message count, an early
81171
+ * message mutated and the cache broke from that index.
81172
+ *
81173
+ * `prev` is the array of per-message fingerprints captured last call;
81174
+ * `current` is the live messages this call.
81175
+ */
81176
+ function stablePrefixLength(prev, current) {
81177
+ const n = Math.min(prev.length, current.length);
81178
+ let i = 0;
81179
+ for (; i < n; i++) if (prev[i] !== messageFingerprint(current[i])) break;
81180
+ return i;
81181
+ }
81182
+ //#endregion
80825
81183
  //#region ../../packages/agent-core/src/agent/context/types.ts
80826
81184
  const USER_PROMPT_ORIGIN = { kind: "user" };
80827
81185
  //#endregion
@@ -80844,6 +81202,18 @@ var ContextMemory = class {
80844
81202
  openSteps = /* @__PURE__ */ new Map();
80845
81203
  pendingToolResultIds = /* @__PURE__ */ new Set();
80846
81204
  deferredMessages = [];
81205
+ /**
81206
+ * Per-message fingerprints captured from the last message list handed to
81207
+ * the LLM via {@link messagesForLLM}. Used to measure prefix stability
81208
+ * across calls: a provider prompt cache only hits when the leading
81209
+ * messages are byte-identical to the previous request, so the length of
81210
+ * the matching prefix here approximates the cacheable prefix length.
81211
+ *
81212
+ * Reset on {@link clear}; a compaction naturally produces a 0-length
81213
+ * stable prefix (the summary replaces the head), which is the correct
81214
+ * cache-break signal rather than a reset.
81215
+ */
81216
+ lastSentFingerprints = [];
80847
81217
  constructor(agent) {
80848
81218
  this.agent = agent;
80849
81219
  }
@@ -80893,6 +81263,7 @@ var ContextMemory = class {
80893
81263
  this.openSteps.clear();
80894
81264
  this.pendingToolResultIds.clear();
80895
81265
  this.deferredMessages = [];
81266
+ this.lastSentFingerprints = [];
80896
81267
  this.agent.injection.onContextClear();
80897
81268
  this.agent.emitStatusUpdated();
80898
81269
  }
@@ -80937,6 +81308,19 @@ var ContextMemory = class {
80937
81308
  }
80938
81309
  if (!this.agent.records.restoring && (stoppedAtBoundary || removedUserCount < count)) {}
80939
81310
  }
81311
+ /**
81312
+ * Apply a full compaction summary.
81313
+ *
81314
+ * Prefix-stability note: this is a **replaceHead** operation, not a
81315
+ * replaceTail. The first `compactedCount` messages are collapsed into a
81316
+ * single summary message; the trailing recent messages are preserved
81317
+ * verbatim. This necessarily breaks the provider prompt cache for the
81318
+ * whole prefix (the summary is new content), which is inherent to
81319
+ * summarization and cannot be avoided. After compaction the new prefix
81320
+ * `[summary, ...tail]` is stable again until the next compaction or
81321
+ * micro-compaction cutoff advance, so subsequent append-only steps resume
81322
+ * hitting the cache.
81323
+ */
80940
81324
  applyCompaction(summary) {
80941
81325
  this.agent.records.logRecord({
80942
81326
  type: "context.apply_compaction",
@@ -80980,6 +81364,48 @@ var ContextMemory = class {
80980
81364
  this.agent.microCompaction.detect();
80981
81365
  return project(this.agent.microCompaction.compact(this.history));
80982
81366
  }
81367
+ /**
81368
+ * Build the message list for an LLM call, with prefix-stability
81369
+ * observation.
81370
+ *
81371
+ * This is the LLM-bound counterpart of the {@link messages} getter: it
81372
+ * runs the same detect + compact + project pipeline, then fingerprints
81373
+ * the result and logs how much of the prefix survived since the last
81374
+ * call. A stable prefix length equal to the previous message count means
81375
+ * the provider prompt cache should hit; a smaller value means an early
81376
+ * message mutated (compaction summary, micro-compaction truncation, or a
81377
+ * projection repair) and the cache broke from that index.
81378
+ *
81379
+ * Behavior is otherwise identical to the getter - this is observation
81380
+ * only, it does not alter the messages returned.
81381
+ */
81382
+ messagesForLLM() {
81383
+ this.agent.microCompaction.detect();
81384
+ const messages = project(this.agent.microCompaction.compact(this.history));
81385
+ this.observePrefixStability(messages);
81386
+ return messages;
81387
+ }
81388
+ /**
81389
+ * Compare the projected messages against the last LLM-bound batch and
81390
+ * log the stable-prefix length. Pure observation: no state that affects
81391
+ * message content is mutated, only the fingerprint baseline used by the
81392
+ * next call's comparison.
81393
+ */
81394
+ observePrefixStability(messages) {
81395
+ const prev = this.lastSentFingerprints;
81396
+ const stable = stablePrefixLength(prev, messages);
81397
+ this.lastSentFingerprints = messages.map(messageFingerprint);
81398
+ if (prev.length === 0) return;
81399
+ const appended = messages.length - prev.length;
81400
+ if (stable >= prev.length) return;
81401
+ this.agent.log.debug("prefix-stability: provider prompt cache prefix broke", {
81402
+ stablePrefixLength: stable,
81403
+ prevMessageCount: prev.length,
81404
+ currentMessageCount: messages.length,
81405
+ appendedSinceLast: appended,
81406
+ breakIndex: stable
81407
+ });
81408
+ }
80983
81409
  appendLoopEvent(event) {
80984
81410
  this.agent.records.logRecord({
80985
81411
  type: "context.append_loop_event",
@@ -81616,6 +82042,8 @@ function buildGoalReminder(goal) {
81616
82042
  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
82043
  lines.push("");
81618
82044
  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.");
82045
+ lines.push("");
82046
+ 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
82047
  return lines.join("\n");
81620
82048
  }
81621
82049
  function maxBudgetFraction(goal) {
@@ -97329,6 +97757,13 @@ function buildGraderPrompt(objective, criteria, output) {
97329
97757
  "## Agent Output",
97330
97758
  output || "(no output captured)",
97331
97759
  "",
97760
+ "The Agent Output section contains the following optional parts, in order:",
97761
+ "1. The agent's natural-language summary of what was done this turn.",
97762
+ "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.",
97763
+ "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.",
97764
+ "",
97765
+ "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.",
97766
+ "",
97332
97767
  "Evaluate each dimension independently against the acceptance criteria, then decide overall PASS/FAIL.",
97333
97768
  "When FAIL, list every specific issue with an actionable fix direction so the agent knows exactly what to address next.",
97334
97769
  "Respond with JSON:",
@@ -98019,6 +98454,11 @@ const GOAL_CONTINUATION_ORIGIN = {
98019
98454
  kind: "system_trigger",
98020
98455
  name: "goal_continuation"
98021
98456
  };
98457
+ 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.";
98458
+ const GOAL_BUDGET_STEER_ORIGIN = {
98459
+ kind: "system_trigger",
98460
+ name: "goal_budget_steer"
98461
+ };
98022
98462
  var TurnFlow = class {
98023
98463
  agent;
98024
98464
  steerBuffer = [];
@@ -98183,6 +98623,14 @@ var TurnFlow = class {
98183
98623
  }
98184
98624
  }
98185
98625
  await this.agent.goal.incrementTurn();
98626
+ const budgetSnapshot = this.agent.goal.getGoal().goal;
98627
+ if (budgetSnapshot !== null && budgetSnapshot.status === "active" && (budgetSnapshot.budget.overBudget || isBudgetNearExhaustion(budgetSnapshot.budget, .2))) {
98628
+ turnInput = [{
98629
+ type: "text",
98630
+ text: GOAL_BUDGET_STEER_PROMPT
98631
+ }];
98632
+ turnOrigin = GOAL_BUDGET_STEER_ORIGIN;
98633
+ }
98186
98634
  const end = await this.runOneTurn(turnId, turnInput, turnOrigin, signal, false);
98187
98635
  if (end.event.reason === "cancelled") {
98188
98636
  await this.agent.goal.pauseOnInterrupt({ reason: "Paused after interruption" });
@@ -98374,7 +98822,7 @@ var TurnFlow = class {
98374
98822
  turnId: String(turnId),
98375
98823
  signal,
98376
98824
  llm: this.agent.llm,
98377
- buildMessages: () => this.agent.context.messages,
98825
+ buildMessages: () => this.agent.context.messagesForLLM(),
98378
98826
  dispatchEvent: this.buildDispatchEvent(turnId),
98379
98827
  tools: this.agent.tools.loopTools,
98380
98828
  log: this.agent.log,
@@ -99509,6 +99957,9 @@ var Agent = class {
99509
99957
  if (status === "paused") return this.goal.pauseGoal({}, "user");
99510
99958
  return this.goal.resumeGoal({}, "user");
99511
99959
  },
99960
+ updateGoalObjective: async (payload) => {
99961
+ return this.goal.updateObjective({ objective: payload.objective }, "user");
99962
+ },
99512
99963
  cancelGoal: async () => {
99513
99964
  return this.goal.cancelGoal("user");
99514
99965
  },
@@ -119565,6 +120016,9 @@ var SessionAPIImpl = class {
119565
120016
  updateGoalStatus({ agentId, ...payload }) {
119566
120017
  return this.getAgent(agentId).updateGoalStatus(payload);
119567
120018
  }
120019
+ updateGoalObjective({ agentId, ...payload }) {
120020
+ return this.getAgent(agentId).updateGoalObjective(payload);
120021
+ }
119568
120022
  cancelGoal({ agentId, ...payload }) {
119569
120023
  return this.getAgent(agentId).cancelGoal(payload);
119570
120024
  }
@@ -121307,6 +121761,9 @@ var ScreamCore = class {
121307
121761
  updateGoalStatus({ sessionId, ...payload }) {
121308
121762
  return this.sessionApi(sessionId).updateGoalStatus(payload);
121309
121763
  }
121764
+ updateGoalObjective({ sessionId, ...payload }) {
121765
+ return this.sessionApi(sessionId).updateGoalObjective(payload);
121766
+ }
121310
121767
  cancelGoal({ sessionId, ...payload }) {
121311
121768
  return this.sessionApi(sessionId).cancelGoal(payload);
121312
121769
  }
@@ -121760,6 +122217,13 @@ var SDKRpcClient = class {
121760
122217
  status: input.status
121761
122218
  });
121762
122219
  }
122220
+ async updateGoalObjective(input) {
122221
+ return (await this.getRpc()).updateGoalObjective({
122222
+ sessionId: input.sessionId,
122223
+ agentId: this.interactiveAgentId,
122224
+ objective: input.objective
122225
+ });
122226
+ }
121763
122227
  async cancelGoal(input) {
121764
122228
  return (await this.getRpc()).cancelGoal({
121765
122229
  sessionId: input.sessionId,
@@ -122520,6 +122984,13 @@ var Session = class {
122520
122984
  status
122521
122985
  });
122522
122986
  }
122987
+ async updateGoalObjective(objective) {
122988
+ this.ensureOpen();
122989
+ return this.rpc.updateGoalObjective({
122990
+ sessionId: this.id,
122991
+ objective
122992
+ });
122993
+ }
122523
122994
  async cancelGoal() {
122524
122995
  this.ensureOpen();
122525
122996
  return this.rpc.cancelGoal({ sessionId: this.id });
@@ -122979,7 +123450,7 @@ function optionalBuildString(value) {
122979
123450
  return typeof value === "string" && value.length > 0 ? value : void 0;
122980
123451
  }
122981
123452
  const SCREAM_BUILD_INFO = {
122982
- version: optionalBuildString("0.10.7"),
123453
+ version: optionalBuildString("0.10.8"),
122983
123454
  channel: optionalBuildString(""),
122984
123455
  commit: optionalBuildString(""),
122985
123456
  buildTarget: optionalBuildString("darwin-arm64")
@@ -126197,9 +126668,15 @@ function formatGoalDuration(ms) {
126197
126668
  const seconds = totalSeconds % 60;
126198
126669
  return seconds > 0 ? `${minutes}m${seconds}s` : `${minutes}m`;
126199
126670
  }
126200
- /** Build the footer goal badge: `GOAL 3m · 7 turns`. */
126201
- function formatGoalBadge(wallClockMs, turnsUsed) {
126202
- return `GOAL ${formatGoalDuration(wallClockMs)} · ${turnsUsed} turns`;
126671
+ /** Build the footer goal badge: `GOAL 3m · 7 turns`.
126672
+ *
126673
+ * The TUI only receives `goal.updated` events on state changes, so we keep a
126674
+ * local base timestamp and add the elapsed time since the last snapshot to
126675
+ * produce a live wall-clock reading between sparse events.
126676
+ */
126677
+ function formatGoalBadge(goal) {
126678
+ const elapsedSinceSnapshot = Date.now() - goal.wallClockBaseAt;
126679
+ return `GOAL ${formatGoalDuration(goal.wallClockMs + Math.max(0, elapsedSinceSnapshot))} · ${goal.turnsUsed} turns`;
126203
126680
  }
126204
126681
  const CONTEXT_WARNING_PERCENT_THRESHOLD = 60;
126205
126682
  const CONTEXT_ERROR_PERCENT_THRESHOLD = 90;
@@ -126295,15 +126772,17 @@ var FooterComponent = class {
126295
126772
  this.onGitStatusChange = onGitStatusChange;
126296
126773
  this.gitCacheWorkDir = state.workDir;
126297
126774
  this.gitCache = createGitStatusCache(state.workDir, { onChange: this.onGitStatusChange });
126775
+ this.#restartStatusTimer(state.streamingPhase, state.goalActive);
126298
126776
  }
126299
126777
  setState(state) {
126300
126778
  const previousPhase = this.state?.streamingPhase;
126779
+ const previousGoalActive = this.state?.goalActive;
126301
126780
  if (state.workDir !== this.gitCacheWorkDir) {
126302
126781
  this.gitCacheWorkDir = state.workDir;
126303
126782
  this.gitCache = createGitStatusCache(state.workDir, { onChange: this.onGitStatusChange });
126304
126783
  }
126305
126784
  this.state = state;
126306
- if (state.streamingPhase !== previousPhase) this.#restartStatusTimer(state.streamingPhase);
126785
+ if (state.streamingPhase !== previousPhase || state.goalActive !== previousGoalActive) this.#restartStatusTimer(state.streamingPhase, state.goalActive);
126307
126786
  }
126308
126787
  setColors(colors) {
126309
126788
  this.colors = colors;
@@ -126331,9 +126810,9 @@ var FooterComponent = class {
126331
126810
  dispose() {
126332
126811
  this.#stopStatusTimer();
126333
126812
  }
126334
- #restartStatusTimer(phase) {
126813
+ #restartStatusTimer(phase, goalActive) {
126335
126814
  this.#stopStatusTimer();
126336
- if (phase === "idle") return;
126815
+ if (phase === "idle" && !goalActive) return;
126337
126816
  const intervalMs = 1e3 / 60;
126338
126817
  this.statusTimer = setInterval(() => {
126339
126818
  this.ui.requestRender();
@@ -126354,8 +126833,7 @@ var FooterComponent = class {
126354
126833
  }
126355
126834
  if (state.wolfpackMode) left.push(chalk.hex(colors.wolfpackMode).bold(t("badge.wolfpack")));
126356
126835
  if (state.goalActive && state.goal) {
126357
- const g = state.goal;
126358
- const goalLabel = formatGoalBadge(g.wallClockMs, g.turnsUsed);
126836
+ const goalLabel = formatGoalBadge(state.goal);
126359
126837
  left.push(chalk.hex(colors.primary).bold(goalLabel));
126360
126838
  }
126361
126839
  const model = shortenModel(modelDisplayName(state));
@@ -126731,10 +127209,63 @@ function createThemeStyles(colors) {
126731
127209
  //#endregion
126732
127210
  //#region src/tui/theme/pi-tui-theme.ts
126733
127211
  const HEADING_HASH_PREFIX = /^((?:\u001B\[[0-9;]*m)*)#{1,6}[ \t]+/;
127212
+ /**
127213
+ * Map cli-highlight syntax tokens onto the ColorPalette so code blocks follow
127214
+ * the active theme instead of cli-highlight's built-in colors. cli-highlight
127215
+ * takes one formatter function per token; tokens not listed here fall back to
127216
+ * its DEFAULT_THEME.
127217
+ */
127218
+ function createCodeHighlightTheme(colors) {
127219
+ const keyword = chalk.hex(colors.primary);
127220
+ const str = chalk.hex(colors.success);
127221
+ const comment = chalk.hex(colors.textDim);
127222
+ const num = chalk.hex(colors.warning);
127223
+ const fn = chalk.hex(colors.primary);
127224
+ const cls = chalk.hex(colors.accent);
127225
+ const text = chalk.hex(colors.text);
127226
+ return {
127227
+ keyword,
127228
+ built_in: fn,
127229
+ type: cls,
127230
+ literal: num,
127231
+ number: num,
127232
+ regexp: str,
127233
+ string: str,
127234
+ subst: str,
127235
+ symbol: num,
127236
+ class: cls,
127237
+ function: fn,
127238
+ title: fn,
127239
+ params: text,
127240
+ comment,
127241
+ doctag: comment,
127242
+ meta: chalk.hex(colors.textMuted),
127243
+ "meta-keyword": keyword,
127244
+ "meta-string": str,
127245
+ section: keyword,
127246
+ tag: cls,
127247
+ name: fn,
127248
+ "builtin-name": fn,
127249
+ attr: num,
127250
+ attribute: num,
127251
+ variable: text,
127252
+ bullet: num,
127253
+ code: str,
127254
+ emphasis: (s) => chalk.italic(s),
127255
+ strong: (s) => chalk.bold(s),
127256
+ formula: text,
127257
+ link: chalk.hex(colors.mdLink),
127258
+ quote: chalk.hex(colors.mdQuote),
127259
+ addition: chalk.hex(colors.diffAdded),
127260
+ deletion: chalk.hex(colors.diffRemoved),
127261
+ default: text
127262
+ };
127263
+ }
126734
127264
  function createMarkdownTheme(colors) {
126735
127265
  const stripHash = (text) => text.replace(HEADING_HASH_PREFIX, "$1");
126736
127266
  const muted = chalk.hex(colors.textMuted);
126737
127267
  const border = chalk.hex(colors.border);
127268
+ const codeTheme = createCodeHighlightTheme(colors);
126738
127269
  return {
126739
127270
  heading: (text) => chalk.bold.hex(colors.text)(stripHash(text)),
126740
127271
  link: (text) => chalk.hex(colors.mdLink)(text),
@@ -126756,7 +127287,8 @@ function createMarkdownTheme(colors) {
126756
127287
  try {
126757
127288
  return highlight(code, {
126758
127289
  language,
126759
- ignoreIllegals: true
127290
+ ignoreIllegals: true,
127291
+ theme: codeTheme
126760
127292
  }).split("\n");
126761
127293
  } catch {
126762
127294
  return code.split("\n");
@@ -128185,6 +128717,10 @@ function buildEmptyGoalLines(colors) {
128185
128717
  value(t("goalpanel.no_goal")),
128186
128718
  "",
128187
128719
  `${muted("/goal")} ${value(t("goalpanel.goal_placeholder"))} ${muted(t("goalpanel.create_goal"))}`,
128720
+ `${muted("/goal setup")} ${muted(t("goalpanel.setup_goal"))}`,
128721
+ `${muted("/goal update")} ${value("<new objective>")} ${muted(t("goalpanel.update_goal"))}`,
128722
+ `${muted("/goal status")} ${muted(t("goalpanel.status_goal"))}`,
128723
+ `${muted("/goal replace")} ${value("<new objective>")} ${muted(t("goalpanel.replace_goal"))}`,
128188
128724
  `${muted("/goal pause")} ${muted(t("goalpanel.pause_goal"))}`,
128189
128725
  `${muted("/goal resume")} ${muted(t("goalpanel.resume_goal"))}`,
128190
128726
  `${muted("/goaloff")} ${muted(t("goalpanel.cancel_goal"))}`
@@ -128247,6 +128783,19 @@ function parseGoalCommand(rawArgs) {
128247
128783
  const tokens = args.split(/\s+/);
128248
128784
  const first = tokens[0];
128249
128785
  if (first !== void 0 && CONTROL_SUBCOMMANDS.has(first) && tokens.length === 1) return { kind: first };
128786
+ if (first === "setup") return { kind: "setup" };
128787
+ if (first === "update") {
128788
+ const objective = tokens.slice(1).join(" ").trim();
128789
+ if (objective.length === 0) return {
128790
+ kind: "error",
128791
+ severity: "hint",
128792
+ message: t("goal.need_desc")
128793
+ };
128794
+ return {
128795
+ kind: "update",
128796
+ objective
128797
+ };
128798
+ }
128250
128799
  let index = 0;
128251
128800
  let replace = false;
128252
128801
  if (tokens[index] === "replace") {
@@ -128276,6 +128825,9 @@ async function handleGoalCommand(host, args) {
128276
128825
  case "status":
128277
128826
  await showGoalStatus(host);
128278
128827
  return;
128828
+ case "setup":
128829
+ await guidedGoalSetup(host);
128830
+ return;
128279
128831
  case "pause":
128280
128832
  await pauseGoal(host);
128281
128833
  return;
@@ -128285,6 +128837,9 @@ async function handleGoalCommand(host, args) {
128285
128837
  case "off":
128286
128838
  await handleGoalOffCommand(host);
128287
128839
  return;
128840
+ case "update":
128841
+ await updateGoalObjective(host, parsed);
128842
+ return;
128288
128843
  case "create":
128289
128844
  await createGoal(host, parsed);
128290
128845
  return;
@@ -128302,8 +128857,51 @@ async function createGoal(host, parsed) {
128302
128857
  }
128303
128858
  await showGoalConfigWizard(host, session, parsed.objective, parsed.replace);
128304
128859
  }
128860
+ 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.";
128861
+ /**
128862
+ * Guided goal creation: collect a brief task description, refine it via the
128863
+ * LLM into a single objective sentence, let the user confirm/edit, then enter
128864
+ * the standard configuration wizard. Falls back to the raw description if the
128865
+ * LLM call fails.
128866
+ */
128867
+ async function guidedGoalSetup(host) {
128868
+ const session = host.session;
128869
+ if (session === void 0) {
128870
+ host.showError(t("error.no_session"));
128871
+ return;
128872
+ }
128873
+ if (detectGoalConflict(host.state.appState, "enable_goal") === "goal_active") {
128874
+ host.showNotice(t("goal.storm_breaker"), t("goal.conflict_loop"));
128875
+ return;
128876
+ }
128877
+ const { TextInputDialogComponent } = await import("./text-input-dialog-B_rgO4qn.mjs");
128878
+ const initialDesc = await promptText(host, TextInputDialogComponent, {
128879
+ title: t("goal.setup_title_initial"),
128880
+ subtitle: t("goal.setup_desc_hint"),
128881
+ placeholder: t("goal.setup_desc_placeholder"),
128882
+ allowEmpty: false
128883
+ });
128884
+ if (initialDesc === void 0) return;
128885
+ host.showStatus(t("goal.setup_refining"));
128886
+ let objective;
128887
+ try {
128888
+ objective = (await session.generateText(GOAL_REFINER_SYSTEM_PROMPT, initialDesc)).trim();
128889
+ if (objective.length === 0) objective = initialDesc;
128890
+ } catch {
128891
+ objective = initialDesc;
128892
+ }
128893
+ const confirmed = await promptText(host, TextInputDialogComponent, {
128894
+ title: t("goal.setup_title_confirm"),
128895
+ subtitle: t("goal.setup_confirm_hint"),
128896
+ placeholder: objective,
128897
+ initialValue: objective,
128898
+ allowEmpty: true
128899
+ });
128900
+ if (confirmed === void 0) return;
128901
+ await showGoalConfigWizard(host, session, confirmed.trim() || objective, false);
128902
+ }
128305
128903
  async function showGoalConfigWizard(host, session, objective, replace) {
128306
- const { TextInputDialogComponent } = await import("./text-input-dialog-DqBy9bEe.mjs");
128904
+ const { TextInputDialogComponent } = await import("./text-input-dialog-B_rgO4qn.mjs");
128307
128905
  const turnInput = await promptNumber(host, TextInputDialogComponent, {
128308
128906
  title: t("goal.wizard_title", { objective }),
128309
128907
  subtitle: t("goal.budget_turns_hint"),
@@ -128373,6 +128971,27 @@ function promptNumber(host, TextInputDialogComponent, opts) {
128373
128971
  host.mountEditorReplacement(dialog);
128374
128972
  });
128375
128973
  }
128974
+ /** Prompt user for free-form text. Returns undefined on cancel. */
128975
+ function promptText(host, TextInputDialogComponent, opts) {
128976
+ return new Promise((resolve) => {
128977
+ const dialog = new TextInputDialogComponent((result) => {
128978
+ host.restoreEditor();
128979
+ if (result.kind !== "ok") {
128980
+ resolve(void 0);
128981
+ return;
128982
+ }
128983
+ resolve(result.value.trim());
128984
+ }, {
128985
+ title: opts.title,
128986
+ subtitle: opts.subtitle,
128987
+ placeholder: opts.placeholder,
128988
+ initialValue: opts.initialValue,
128989
+ allowEmpty: opts.allowEmpty,
128990
+ colors: host.state.theme.colors
128991
+ });
128992
+ host.mountEditorReplacement(dialog);
128993
+ });
128994
+ }
128376
128995
  async function pauseGoal(host) {
128377
128996
  const session = host.session;
128378
128997
  if (session === void 0) {
@@ -128413,6 +129032,24 @@ async function resumeGoal(host) {
128413
129032
  host.showError(t("goal.resume_failed", { msg: message }));
128414
129033
  }
128415
129034
  }
129035
+ async function updateGoalObjective(host, parsed) {
129036
+ const session = host.session;
129037
+ if (session === void 0) {
129038
+ host.showError(t("error.no_session"));
129039
+ return;
129040
+ }
129041
+ try {
129042
+ if ((await session.getGoal()).goal === null) {
129043
+ host.showStatus(t("goal.no_active"));
129044
+ return;
129045
+ }
129046
+ await session.updateGoalObjective(parsed.objective);
129047
+ host.showStatus(t("goal.updated", { objective: parsed.objective }));
129048
+ } catch (error) {
129049
+ const message = error instanceof Error ? error.message : String(error);
129050
+ host.showError(t("goal.update_failed", { msg: message }));
129051
+ }
129052
+ }
128416
129053
  async function handleGoalOffCommand(host) {
128417
129054
  const session = host.session;
128418
129055
  if (session === void 0) {
@@ -129442,6 +130079,12 @@ function easeSpeedRatio(ratio) {
129442
130079
  }
129443
130080
  //#endregion
129444
130081
  //#region src/tui/components/messages/thinking.ts
130082
+ /** gpt-5 reasoning summaries contain empty HTML comment padding sentinels
130083
+ * like `<!-- -->`. Strip them to keep the thinking display clean. */
130084
+ const EMPTY_COMMENT_RE = /<!--\s*-->/g;
130085
+ function filterThinkingNoise(text) {
130086
+ return text.replace(EMPTY_COMMENT_RE, "");
130087
+ }
129445
130088
  var ThinkingComponent = class {
129446
130089
  text;
129447
130090
  color;
@@ -129480,7 +130123,7 @@ var ThinkingComponent = class {
129480
130123
  this.textComponent.setText(this.styled(trimmed));
129481
130124
  }
129482
130125
  styled(text) {
129483
- return chalk.hex(this.color).italic(text);
130126
+ return chalk.hex(this.color).italic(filterThinkingNoise(text));
129484
130127
  }
129485
130128
  finalize() {
129486
130129
  if (this.mode === "finalized") return;
@@ -129657,6 +130300,47 @@ function makeDiffStyles(colors) {
129657
130300
  meta: (s) => chalk.hex(colors.diffMeta)(s)
129658
130301
  };
129659
130302
  }
130303
+ const ANSI_RE = /\u001B\[[0-9;]*m/;
130304
+ /** Visualize leading whitespace: tabs as `->`, leading spaces as `·`. */
130305
+ function visualizeIndent(line) {
130306
+ let i = 0;
130307
+ let visual = "";
130308
+ while (i < line.length) {
130309
+ const ch = line[i];
130310
+ if (ch === " ") {
130311
+ visual += "->";
130312
+ i++;
130313
+ continue;
130314
+ }
130315
+ if (ch === " ") {
130316
+ visual += "·";
130317
+ i++;
130318
+ continue;
130319
+ }
130320
+ break;
130321
+ }
130322
+ return {
130323
+ text: visual + line.slice(i),
130324
+ indentEnd: visual.length
130325
+ };
130326
+ }
130327
+ /**
130328
+ * Render a diff line's code with leading-whitespace visualization (tabs as
130329
+ * `->`, spaces as `·`, dimmed) and optional syntax highlighting. Highlighting
130330
+ * runs on the raw code first; indent visualization then runs on the
130331
+ * highlighted string - leading whitespace carries no token color, so it is
130332
+ * still plain and can be replaced and dimmed without disturbing the tokens.
130333
+ * When highlighting is off (streaming) or produced no token colors, the code
130334
+ * part is colored with the diff line color instead.
130335
+ */
130336
+ function renderDiffCode(code, colorFn, highlight, lang) {
130337
+ const { text, indentEnd } = visualizeIndent(highlight ? highlightLines(code, lang)[0] ?? code : code);
130338
+ const indent = text.slice(0, indentEnd);
130339
+ const rest = text.slice(indentEnd);
130340
+ const dimIndent = indent.length > 0 ? chalk.dim(indent) : indent;
130341
+ if (highlight && ANSI_RE.test(rest)) return dimIndent + rest;
130342
+ return dimIndent + colorFn(rest);
130343
+ }
129660
130344
  /**
129661
130345
  * Compute word-level diff between two single lines and highlight the changed
129662
130346
  * words with `chalk.inverse()`. Only the first removed/added part has its
@@ -129763,6 +130447,8 @@ function computeDiffLines(oldText, newText, oldStart = 1, newStart = 1, isIncomp
129763
130447
  }
129764
130448
  function renderDiffLines(oldText, newText, path, colors, isIncomplete = false, oldStart, newStart, maxLines) {
129765
130449
  const s = makeDiffStyles(colors);
130450
+ const lang = langFromPath(path);
130451
+ const doHighlight = !isIncomplete && lang !== void 0;
129766
130452
  const changedLines = computeDiffLines(oldText, newText, oldStart ?? 1, newStart ?? 1, isIncomplete).filter((l) => l.kind !== "context");
129767
130453
  const added = changedLines.filter((l) => l.kind === "add").length;
129768
130454
  const removed = changedLines.filter((l) => l.kind === "delete").length;
@@ -129787,7 +130473,7 @@ function renderDiffLines(oldText, newText, path, colors, isIncomplete = false, o
129787
130473
  const line = shown[i];
129788
130474
  const marker = line.kind === "add" ? "+" : "-";
129789
130475
  const color = line.kind === "add" ? s.add : s.del;
129790
- output.push(s.gutter(String(line.lineNum).padStart(4) + " ") + color(marker + " " + line.code));
130476
+ output.push(s.gutter(String(line.lineNum).padStart(4) + " ") + color(`${marker} `) + renderDiffCode(line.code, color, doHighlight, lang));
129791
130477
  i += 1;
129792
130478
  }
129793
130479
  const hidden = changedLines.length - shown.length;
@@ -129838,11 +130524,11 @@ function buildClusters(diffLines, contextLines) {
129838
130524
  removedCount: removed
129839
130525
  };
129840
130526
  }
129841
- function formatDiffRow(line, s) {
130527
+ function formatDiffRow(line, s, doHighlight, lang) {
129842
130528
  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;
130529
+ if (line.kind === "add") return gutter + s.add("+ ") + renderDiffCode(line.code, s.add, doHighlight, lang);
130530
+ if (line.kind === "delete") return gutter + s.del("- ") + renderDiffCode(line.code, s.del, doHighlight, lang);
130531
+ return gutter + " " + renderDiffCode(line.code, (x) => x, doHighlight, lang);
129846
130532
  }
129847
130533
  /**
129848
130534
  * Render a diff with surrounding context, eliding unchanged middle
@@ -129855,6 +130541,8 @@ function formatDiffRow(line, s) {
129855
130541
  */
129856
130542
  function renderDiffLinesClustered(oldText, newText, path, colors, opts = {}) {
129857
130543
  const s = makeDiffStyles(colors);
130544
+ const lang = langFromPath(path);
130545
+ const doHighlight = !(opts.isIncomplete ?? false) && lang !== void 0;
129858
130546
  const contextLines = opts.contextLines ?? 3;
129859
130547
  const maxLines = opts.maxLines;
129860
130548
  const diffLines = computeDiffLines(oldText, newText, 1, 1, opts.isIncomplete ?? false);
@@ -129911,7 +130599,7 @@ function renderDiffLinesClustered(oldText, newText, path, colors, opts = {}) {
129911
130599
  i += 2;
129912
130600
  continue;
129913
130601
  }
129914
- output.push(formatDiffRow(line, s));
130602
+ output.push(formatDiffRow(line, s, doHighlight, lang));
129915
130603
  body++;
129916
130604
  if (line.kind !== "context") shownChanges++;
129917
130605
  prevEnd = i;
@@ -138652,7 +139340,8 @@ var SessionEventHandler = class {
138652
139340
  goal: {
138653
139341
  objective: snapshot.objective,
138654
139342
  turnsUsed: snapshot.turnsUsed ?? 0,
138655
- wallClockMs: snapshot.wallClockMs ?? 0
139343
+ wallClockMs: snapshot.wallClockMs ?? 0,
139344
+ wallClockBaseAt: Date.now()
138656
139345
  },
138657
139346
  goalActive: snapshot.status === "active"
138658
139347
  });
@@ -144729,7 +145418,8 @@ var SessionManager = class {
144729
145418
  goal: goal ? {
144730
145419
  objective: goal.objective,
144731
145420
  turnsUsed: goal.turnsUsed ?? 0,
144732
- wallClockMs: goal.wallClockMs ?? 0
145421
+ wallClockMs: goal.wallClockMs ?? 0,
145422
+ wallClockBaseAt: Date.now()
144733
145423
  } : null,
144734
145424
  goalActive: goal?.status === "active",
144735
145425
  goalContinuationCount: 0