scream-code 0.10.6 → 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-pClOx34t.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
  /**
@@ -55788,15 +55788,35 @@ function formatBudget(value, unit) {
55788
55788
  */
55789
55789
  /** Maximum objective length in characters. */
55790
55790
  const MAX_GOAL_OBJECTIVE_LENGTH = 4e3;
55791
+ /** Consecutive markBlocked calls with the same reason required before blocking. */
55792
+ const BLOCKED_STREAK_THRESHOLD = 3;
55791
55793
  /** Maximum number of working notes kept per goal. */
55792
- const MAX_GOAL_NOTES = 30;
55794
+ const MAX_GOAL_NOTES = 60;
55793
55795
  /** Maximum characters per note. */
55794
- const MAX_NOTE_LENGTH = 200;
55796
+ const MAX_NOTE_LENGTH = 400;
55795
55797
  const GOAL_CANCELLED_REMINDER = [
55796
55798
  "The user cancelled the current goal.",
55797
55799
  "Ignore earlier active-goal reminders for that goal.",
55798
55800
  "Handle the next user request normally unless the user starts or resumes a goal."
55799
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
+ }
55800
55820
  const GOAL_COMPLETION_REMINDER_NAME = "goal_completion_summary";
55801
55821
  const GOAL_BLOCKED_REMINDER_NAME = "goal_blocked_reason";
55802
55822
  var GoalMode = class {
@@ -55835,7 +55855,8 @@ var GoalMode = class {
55835
55855
  tokensUsed: 0,
55836
55856
  wallClockMs: 0,
55837
55857
  budgetLimits: {},
55838
- notes: []
55858
+ notes: [],
55859
+ blockedStreak: 0
55839
55860
  };
55840
55861
  this.state = state;
55841
55862
  }
@@ -55855,6 +55876,7 @@ var GoalMode = class {
55855
55876
  state.wallClockResumedAt = void 0;
55856
55877
  }
55857
55878
  if (record.budgetLimits !== void 0) state.budgetLimits = record.budgetLimits;
55879
+ if (record.objective !== void 0) state.objective = record.objective;
55858
55880
  }
55859
55881
  restoreClear(_record) {
55860
55882
  this.state = void 0;
@@ -55887,7 +55909,8 @@ var GoalMode = class {
55887
55909
  wallClockMs: 0,
55888
55910
  wallClockResumedAt: Date.now(),
55889
55911
  budgetLimits: {},
55890
- notes: []
55912
+ notes: [],
55913
+ blockedStreak: 0
55891
55914
  };
55892
55915
  this.persistState(state);
55893
55916
  this.agent.records.logRecord({
@@ -55932,6 +55955,8 @@ var GoalMode = class {
55932
55955
  if (state.status === "active") return this.toSnapshot(state);
55933
55956
  if (state.status !== "paused" && state.status !== "blocked") throw new ScreamError(ErrorCodes.GOAL_NOT_RESUMABLE, `Cannot resume a goal in status "${state.status}"`);
55934
55957
  state.terminalReason = void 0;
55958
+ state.blockedStreak = 0;
55959
+ state.lastBlockedReason = void 0;
55935
55960
  this.applyStatus(state, "active");
55936
55961
  this.persistState(state, { change: {
55937
55962
  kind: "lifecycle",
@@ -55952,6 +55977,23 @@ var GoalMode = class {
55952
55977
  this.appendGoalUpdate({ budgetLimits: state.budgetLimits });
55953
55978
  return this.toSnapshot(state);
55954
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
+ }
55955
55997
  async cancelGoal(actor = "user") {
55956
55998
  const state = this.requireState();
55957
55999
  const snapshot = this.toSnapshot(state);
@@ -55965,8 +56007,22 @@ var GoalMode = class {
55965
56007
  async markBlocked(input = {}, actor = "runtime") {
55966
56008
  const state = this.state;
55967
56009
  if (state === void 0 || state.status !== "active") return null;
56010
+ if (actor === "model") {
56011
+ const reason = input.reason ?? "";
56012
+ if (reason === (state.lastBlockedReason ?? "")) state.blockedStreak += 1;
56013
+ else {
56014
+ state.blockedStreak = 1;
56015
+ state.lastBlockedReason = reason;
56016
+ }
56017
+ if (state.blockedStreak < BLOCKED_STREAK_THRESHOLD) {
56018
+ this.persistState(state, { silent: true });
56019
+ return null;
56020
+ }
56021
+ }
55968
56022
  this.applyStatus(state, "blocked");
55969
56023
  state.terminalReason = input.reason;
56024
+ state.blockedStreak = 0;
56025
+ state.lastBlockedReason = void 0;
55970
56026
  this.persistState(state, { change: {
55971
56027
  kind: "lifecycle",
55972
56028
  status: "blocked",
@@ -56021,7 +56077,7 @@ var GoalMode = class {
56021
56077
  content: trimmed,
56022
56078
  time: Date.now()
56023
56079
  });
56024
- 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);
56025
56081
  this.persistState(state, { silent: true });
56026
56082
  return this.toSnapshot(state);
56027
56083
  }
@@ -56168,13 +56224,18 @@ function formatTokens$2(tokens) {
56168
56224
  }
56169
56225
  //#endregion
56170
56226
  //#region ../../packages/agent-core/src/tools/builtin/goal/update-goal.ts
56171
- const UpdateGoalToolInputSchema = z.object({ status: z.enum([
56172
- "active",
56173
- "complete",
56174
- "paused",
56175
- "blocked"
56176
- ]).describe("The lifecycle status to set for the current goal.") }).strict();
56227
+ const UpdateGoalToolInputSchema = z.object({
56228
+ status: z.enum([
56229
+ "active",
56230
+ "complete",
56231
+ "paused",
56232
+ "blocked"
56233
+ ]).describe("The lifecycle status to set for the current goal."),
56234
+ reason: z.string().optional().describe("Optional reason for the status change, especially for blocked.")
56235
+ }).strict();
56177
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;
56178
56239
  function extractRecentOutput(history) {
56179
56240
  const parts = [];
56180
56241
  for (let i = history.length - 1; i >= 0; i--) {
@@ -56187,6 +56248,51 @@ function extractRecentOutput(history) {
56187
56248
  const joined = parts.join("\n\n");
56188
56249
  return joined.length > MAX_GRADER_OUTPUT_CHARS ? `${joined.slice(0, MAX_GRADER_OUTPUT_CHARS)}…` : joined;
56189
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
+ }
56190
56296
  var UpdateGoalTool = class {
56191
56297
  agent;
56192
56298
  grader;
@@ -56209,15 +56315,18 @@ var UpdateGoalTool = class {
56209
56315
  }
56210
56316
  if (args.status === "complete") return this.handleComplete(goal);
56211
56317
  if (args.status === "blocked") {
56212
- const blocked = await goal.markBlocked({}, "model");
56213
- if (blocked !== null) this.agent.context.appendSystemReminder(buildGoalBlockedReasonPrompt(blocked), {
56214
- kind: "system_trigger",
56215
- name: GOAL_BLOCKED_REMINDER_NAME
56216
- });
56217
- return {
56218
- output: "Goal marked blocked.",
56219
- stopTurn: true
56220
- };
56318
+ const blocked = await goal.markBlocked(args.reason !== void 0 ? { reason: args.reason } : {}, "model");
56319
+ if (blocked !== null) {
56320
+ this.agent.context.appendSystemReminder(buildGoalBlockedReasonPrompt(blocked), {
56321
+ kind: "system_trigger",
56322
+ name: GOAL_BLOCKED_REMINDER_NAME
56323
+ });
56324
+ return {
56325
+ output: "Goal marked blocked.",
56326
+ stopTurn: true
56327
+ };
56328
+ }
56329
+ return { output: "Goal remains active. Report the same blocker in subsequent turns to confirm it cannot be resolved. Continue working on the goal in the meantime." };
56221
56330
  }
56222
56331
  await goal.pauseGoal({}, "model");
56223
56332
  return {
@@ -56230,7 +56339,7 @@ var UpdateGoalTool = class {
56230
56339
  async handleComplete(goal) {
56231
56340
  const goalState = goal.getGoal().goal;
56232
56341
  if (!goalState) return { output: "No active goal." };
56233
- const output = extractRecentOutput(this.agent.context.history);
56342
+ const outputWithContext = await appendGitDiffStat(appendGoalNotes(extractRecentOutput(this.agent.context.history), goalState.notes), this.agent.config?.cwd ?? "");
56234
56343
  try {
56235
56344
  await goal.pauseGoal({ reason: "verifying" }, "system");
56236
56345
  } catch (error) {
@@ -56238,7 +56347,7 @@ var UpdateGoalTool = class {
56238
56347
  }
56239
56348
  let rawGrade;
56240
56349
  try {
56241
- rawGrade = await this.grader(goalState.objective, goalState.completionCriterion, output);
56350
+ rawGrade = await this.grader(goalState.objective, goalState.completionCriterion, outputWithContext);
56242
56351
  } catch (error) {
56243
56352
  const resumeError = await resumeAfterGrading(goal);
56244
56353
  if (resumeError !== void 0) return resumeError;
@@ -56314,7 +56423,7 @@ function errorMessage$4(error) {
56314
56423
  }
56315
56424
  //#endregion
56316
56425
  //#region ../../packages/agent-core/src/tools/builtin/goal/write-goal-note.ts
56317
- 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();
56318
56427
  var WriteGoalNoteTool = class {
56319
56428
  agent;
56320
56429
  name = "WriteGoalNote";
@@ -57551,6 +57660,16 @@ function parseMemoryMemos(text) {
57551
57660
  }
57552
57661
  return memos;
57553
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
+ }
57554
57673
  /** System prompt for exit-time extraction — instructs the LLM how to extract. */
57555
57674
  const EXIT_EXTRACTION_SYSTEM_PROMPT = "你是一个任务经验提取助手。任务是从对话记录中识别已完成的任务闭环,提炼出任务经验记录。用对话的主要语言输出(中文对话用中文,英文对话用英文)。只输出指定的 JSON 格式,不要调用任何工具。";
57556
57675
  /** Build the user prompt for exit-time extraction, including a conversation sample. */
@@ -58303,17 +58422,20 @@ var MemoryWriteTool = class {
58303
58422
  };
58304
58423
  const sessionId = this.agent.homedir ? basename$1(dirname$2(dirname$2(this.agent.homedir))) : "unknown";
58305
58424
  const sourceSessionTitle = await this.agent.getSessionTitle();
58306
- const whatFailed = args.whatFailed?.trim();
58307
- const whatWorked = args.whatWorked?.trim();
58308
- 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}`));
58309
58431
  const memo = createMemoryMemo({
58310
58432
  sourceSessionId: sessionId,
58311
58433
  sourceSessionTitle,
58312
- userNeed: args.userNeed,
58313
- approach: args.approach,
58314
- outcome: args.outcome,
58315
- whatFailed: whatFailed === void 0 || whatFailed.length === 0 ? "none" : whatFailed,
58316
- 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,
58317
58439
  tags,
58318
58440
  extractionSource: "manual",
58319
58441
  projectDir: this.agent.config.cwd
@@ -62680,20 +62802,20 @@ function isRecord$6(value) {
62680
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";
62681
62803
  //#endregion
62682
62804
  //#region ../../packages/agent-core/src/skill/builtin/dream.ts
62683
- const PSEUDO_PATH$1 = "builtin://dream";
62684
- const parsed$1 = parseSkillText({
62805
+ const PSEUDO_PATH$2 = "builtin://dream";
62806
+ const parsed$2 = parseSkillText({
62685
62807
  skillMdPath: "/builtin/skills/dream.md",
62686
62808
  skillDirName: "dream",
62687
62809
  source: "builtin",
62688
62810
  text: dream_default
62689
62811
  });
62690
62812
  const DREAM_SKILL = {
62691
- ...parsed$1,
62692
- path: PSEUDO_PATH$1,
62693
- dir: PSEUDO_PATH$1,
62813
+ ...parsed$2,
62814
+ path: PSEUDO_PATH$2,
62815
+ dir: PSEUDO_PATH$2,
62694
62816
  metadata: {
62695
- ...parsed$1.metadata,
62696
- type: parsed$1.metadata.type ?? "inline",
62817
+ ...parsed$2.metadata,
62818
+ type: parsed$2.metadata.type ?? "inline",
62697
62819
  disableModelInvocation: true
62698
62820
  }
62699
62821
  };
@@ -62702,14 +62824,36 @@ const DREAM_SKILL = {
62702
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";
62703
62825
  //#endregion
62704
62826
  //#region ../../packages/agent-core/src/skill/builtin/make-skill.ts
62705
- const PSEUDO_PATH = "builtin://make-skill";
62706
- const parsed = parseSkillText({
62827
+ const PSEUDO_PATH$1 = "builtin://make-skill";
62828
+ const parsed$1 = parseSkillText({
62707
62829
  skillMdPath: "/builtin/skills/make-skill.md",
62708
62830
  skillDirName: "make-skill",
62709
62831
  source: "builtin",
62710
62832
  text: make_skill_default
62711
62833
  });
62712
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 = {
62713
62857
  ...parsed,
62714
62858
  path: PSEUDO_PATH,
62715
62859
  dir: PSEUDO_PATH,
@@ -62724,6 +62868,7 @@ const MAKE_SKILL_SKILL = {
62724
62868
  function registerBuiltinSkills(registry) {
62725
62869
  registry.registerBuiltinSkill(DREAM_SKILL);
62726
62870
  registry.registerBuiltinSkill(MAKE_SKILL_SKILL);
62871
+ registry.registerBuiltinSkill(TOOL_PROMPT_OPTIMIZATION_SKILL);
62727
62872
  }
62728
62873
  //#endregion
62729
62874
  //#region ../../packages/agent-core/src/skill/scanner.ts
@@ -75281,17 +75426,25 @@ var ToolResultBuilder = class {
75281
75426
  maxChars;
75282
75427
  maxTailChars;
75283
75428
  maxLineLength;
75429
+ artifactSink;
75430
+ maxFullOutputChars;
75284
75431
  buffer = [];
75285
75432
  nCharsValue = 0;
75286
75433
  truncationHappened = false;
75287
75434
  headTruncated = false;
75288
75435
  tailBuf = [];
75289
75436
  tailCharsValue = 0;
75437
+ fullOutput;
75438
+ fullOutputChars = 0;
75439
+ totalLinesWritten = 0;
75290
75440
  constructor(options = {}) {
75291
75441
  this.maxChars = options.maxChars ?? DEFAULT_MAX_CHARS;
75292
75442
  this.maxTailChars = options.maxTailChars ?? DEFAULT_TAIL_CHARS;
75293
75443
  this.maxLineLength = options.maxLineLength === void 0 ? DEFAULT_MAX_LINE_LENGTH : options.maxLineLength;
75294
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 = [];
75295
75448
  }
75296
75449
  get nChars() {
75297
75450
  return this.nCharsValue + this.tailCharsValue;
@@ -75301,12 +75454,19 @@ var ToolResultBuilder = class {
75301
75454
  if (!this.headTruncated || this.tailCharsValue === 0) return head;
75302
75455
  this.trimTail();
75303
75456
  const tail = this.tailBuf.join("");
75304
- 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}`;
75305
75460
  }
75306
75461
  write(text) {
75307
75462
  if (text.length === 0) return 0;
75308
75463
  const lines = text.match(/[^\r\n]*(?:\r\n|[\n\r])|[^\r\n]+/g) ?? [];
75309
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
+ }
75310
75470
  let charsWritten = 0;
75311
75471
  for (const originalLine of lines) if (this.nCharsValue < this.maxChars) {
75312
75472
  const remainingChars = this.maxChars - this.nCharsValue;
@@ -75354,11 +75514,36 @@ var ToolResultBuilder = class {
75354
75514
  this.tailBuf.push(trimmed);
75355
75515
  this.tailCharsValue = trimmed.length;
75356
75516
  }
75357
- 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 = {}) {
75358
75541
  let finalMessage = message;
75359
75542
  if (finalMessage.length > 0 && !finalMessage.endsWith(".")) finalMessage += ".";
75360
75543
  if (this.truncationHappened) finalMessage = finalMessage.length === 0 ? TRUNCATION_MESSAGE : `${finalMessage} ${TRUNCATION_MESSAGE}`;
75361
- 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);
75362
75547
  return {
75363
75548
  isError: false,
75364
75549
  output: finalMessage.length > 0 && (this.truncationHappened || output.length === 0) ? output.length === 0 ? finalMessage : output.endsWith("\n") ? `${output}${finalMessage}` : `${output}\n${finalMessage}` : output,
@@ -75367,9 +75552,11 @@ var ToolResultBuilder = class {
75367
75552
  brief: options.brief
75368
75553
  };
75369
75554
  }
75370
- error(message, options = {}) {
75555
+ async error(message, options = {}) {
75371
75556
  const finalMessage = this.truncationHappened ? message.length === 0 ? TRUNCATION_MESSAGE : `${message} ${TRUNCATION_MESSAGE}` : message;
75372
- 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);
75373
75560
  return {
75374
75561
  isError: true,
75375
75562
  output: finalMessage.length === 0 ? output : output.length === 0 ? finalMessage : output.endsWith("\n") ? `${output}${finalMessage}` : `${output}\n${finalMessage}`,
@@ -75379,6 +75566,10 @@ var ToolResultBuilder = class {
75379
75566
  };
75380
75567
  }
75381
75568
  };
75569
+ function countLines(text) {
75570
+ if (text.length === 0) return 0;
75571
+ return text.split("\n").length - (text.endsWith("\n") ? 1 : 0);
75572
+ }
75382
75573
  //#endregion
75383
75574
  //#region ../../packages/agent-core/src/tools/builtin/file/grep.md
75384
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";
@@ -75568,7 +75759,7 @@ var GrepTool = class {
75568
75759
  const combined = visibleBody === "" && messages.length === 0 ? emptyResultMessage : messages.length > 0 ? visibleBody === "" ? messages.join("\n") : `${visibleBody}\n${messages.join("\n")}` : visibleBody;
75569
75760
  const builder = new ToolResultBuilder();
75570
75761
  builder.write(combined);
75571
- const result = builder.ok(sideChannelMessages.join("\n"));
75762
+ const result = await builder.ok(sideChannelMessages.join("\n"));
75572
75763
  const display = buildSearchResultsDisplay(args, limited, mode, contentIncludesLineNumbers);
75573
75764
  if (display === void 0) return result;
75574
75765
  if (result.isError === true) return result;
@@ -77788,6 +77979,30 @@ function truncateUtf8$1(input, maxBytes) {
77788
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";
77789
77980
  //#endregion
77790
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
+ */
77791
78006
  const MS_PER_SECOND = 1e3;
77792
78007
  const DEFAULT_TIMEOUT_S = 60;
77793
78008
  const MAX_TIMEOUT_S = 300;
@@ -78038,7 +78253,11 @@ var BashTool = class {
78038
78253
  killProc();
78039
78254
  }, timeoutMs);
78040
78255
  try {
78041
- 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
+ } });
78042
78261
  const [, exitCode] = await Promise.all([Promise.all([readStreamIntoBuilder(proc.stdout, builder), readStreamIntoBuilder(proc.stderr, builder)]), proc.wait()]);
78043
78262
  if (timedOut) {
78044
78263
  const timeoutLabel = timeoutMs % 1e3 === 0 ? `${String(timeoutMs / 1e3)}s` : `${String(timeoutMs)}ms`;
@@ -79629,7 +79848,8 @@ const DEFAULT_CONFIG = {
79629
79848
  minContentTokens: 100,
79630
79849
  minContextUsageRatio: .5,
79631
79850
  truncatedMarker: "[Old tool result content cleared]",
79632
- uselessMarker: "[Uneventful result elided]"
79851
+ uselessMarker: "[Uneventful result elided]",
79852
+ noMatchesMarker: "[no matches]"
79633
79853
  };
79634
79854
  /**
79635
79855
  * Compute the cutoff index: everything at index < cutoff is eligible for
@@ -79650,30 +79870,107 @@ function computeCutoff(messages, config) {
79650
79870
  }
79651
79871
  return cutoff;
79652
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"]);
79653
79879
  /**
79654
- * Walk the message list and find Read tool calls whose file paths were
79655
- * superseded by a later Read of the same path. Returns a map from the
79656
- * 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).
79657
79943
  *
79658
- * Only considers tool results before the cutoff line newer reads are
79659
- * 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).
79660
79950
  */
79661
79951
  function findSupersededPaths(messages, cutoff) {
79662
79952
  const superseded = /* @__PURE__ */ new Map();
79663
- const readCalls = /* @__PURE__ */ new Map();
79953
+ const latestReadByPath = /* @__PURE__ */ new Map();
79664
79954
  for (let i = 0; i < messages.length; i++) {
79665
79955
  const msg = messages[i];
79666
79956
  if (msg === void 0) continue;
79667
- if (msg.role === "assistant" && msg.toolCalls.length > 0) {
79668
- for (const tc of msg.toolCalls) if (tc.name === "Read" && tc.id !== void 0 && tc.arguments !== void 0) {
79669
- const filePath = typeof tc.arguments === "object" && tc.arguments !== null ? tc.arguments["file_path"] : void 0;
79670
- if (filePath !== void 0) {
79671
- for (const [prevId, prev] of readCalls) if (prev.filePath === filePath && prev.index < cutoff) superseded.set(prevId, filePath);
79672
- readCalls.set(tc.id, {
79673
- filePath,
79674
- index: i
79675
- });
79676
- }
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
+ });
79677
79974
  }
79678
79975
  }
79679
79976
  }
@@ -79727,20 +80024,26 @@ var MicroCompaction = class {
79727
80024
  }
79728
80025
  /**
79729
80026
  * Apply micro-compaction to a message list: replace old tool results
79730
- * before the cutoff line with truncated markers. Read results for files
79731
- * that were re-read later get a supersede marker so the model knows
79732
- * the old content is stale. Tool results explicitly marked useless are
79733
- * elided with a short notice regardless of size, since they carry no
79734
- * 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.
79735
80033
  */
79736
80034
  compact(messages) {
79737
80035
  const config = this.config;
79738
80036
  const superseded = findSupersededPaths(messages, this.cutoff);
80037
+ const toolNames = buildToolCallNameMap(messages);
79739
80038
  const result = [];
79740
80039
  let i = 0;
79741
80040
  for (const msg of messages) {
79742
- const isUseless = i < this.cutoff && msg.role === "tool" && msg.toolCallId !== void 0 && msg.useless === true;
79743
- 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;
79744
80047
  if (isUseless) result.push({
79745
80048
  ...msg,
79746
80049
  content: [{
@@ -79748,8 +80051,16 @@ var MicroCompaction = class {
79748
80051
  text: config.uselessMarker
79749
80052
  }]
79750
80053
  });
80054
+ else if (isZeroMatch) result.push({
80055
+ ...msg,
80056
+ content: [{
80057
+ type: "text",
80058
+ text: config.noMatchesMarker
80059
+ }]
80060
+ });
79751
80061
  else if (isOversizedTruncatable) {
79752
- 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;
79753
80064
  result.push({
79754
80065
  ...msg,
79755
80066
  content: [{
@@ -79774,20 +80085,28 @@ var MicroCompaction = class {
79774
80085
  measureEffect(messages, cutoff) {
79775
80086
  let markerTokenCount;
79776
80087
  let uselessMarkerTokenCount;
80088
+ let noMatchesMarkerTokenCount;
79777
80089
  let truncatedToolResultCount = 0;
79778
80090
  let beforeTokens = 0;
79779
80091
  let afterTokens = 0;
80092
+ const toolNames = buildToolCallNameMap(messages);
79780
80093
  for (let i = 0; i < messages.length && i < cutoff; i++) {
79781
80094
  const message = messages[i];
79782
80095
  if (message?.role !== "tool" || message.toolCallId === void 0) continue;
79783
80096
  const contentTokens = estimateTokensForMessages([message]);
79784
80097
  const isUseless = message.useless === true;
79785
- 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;
79786
80100
  if (isUseless) {
79787
80101
  uselessMarkerTokenCount ??= estimateTokens$1(this.config.uselessMarker);
79788
80102
  truncatedToolResultCount += 1;
79789
80103
  beforeTokens += contentTokens;
79790
80104
  afterTokens += uselessMarkerTokenCount;
80105
+ } else if (isZeroMatch) {
80106
+ noMatchesMarkerTokenCount ??= estimateTokens$1(this.config.noMatchesMarker);
80107
+ truncatedToolResultCount += 1;
80108
+ beforeTokens += contentTokens;
80109
+ afterTokens += noMatchesMarkerTokenCount;
79791
80110
  } else {
79792
80111
  markerTokenCount ??= estimateTokens$1(this.config.truncatedMarker);
79793
80112
  truncatedToolResultCount += 1;
@@ -80796,6 +81115,71 @@ var ConfigState = class {
80796
81115
  }
80797
81116
  };
80798
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
80799
81183
  //#region ../../packages/agent-core/src/agent/context/types.ts
80800
81184
  const USER_PROMPT_ORIGIN = { kind: "user" };
80801
81185
  //#endregion
@@ -80818,6 +81202,18 @@ var ContextMemory = class {
80818
81202
  openSteps = /* @__PURE__ */ new Map();
80819
81203
  pendingToolResultIds = /* @__PURE__ */ new Set();
80820
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 = [];
80821
81217
  constructor(agent) {
80822
81218
  this.agent = agent;
80823
81219
  }
@@ -80867,6 +81263,7 @@ var ContextMemory = class {
80867
81263
  this.openSteps.clear();
80868
81264
  this.pendingToolResultIds.clear();
80869
81265
  this.deferredMessages = [];
81266
+ this.lastSentFingerprints = [];
80870
81267
  this.agent.injection.onContextClear();
80871
81268
  this.agent.emitStatusUpdated();
80872
81269
  }
@@ -80911,6 +81308,19 @@ var ContextMemory = class {
80911
81308
  }
80912
81309
  if (!this.agent.records.restoring && (stoppedAtBoundary || removedUserCount < count)) {}
80913
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
+ */
80914
81324
  applyCompaction(summary) {
80915
81325
  this.agent.records.logRecord({
80916
81326
  type: "context.apply_compaction",
@@ -80954,6 +81364,48 @@ var ContextMemory = class {
80954
81364
  this.agent.microCompaction.detect();
80955
81365
  return project(this.agent.microCompaction.compact(this.history));
80956
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
+ }
80957
81409
  appendLoopEvent(event) {
80958
81410
  this.agent.records.logRecord({
80959
81411
  type: "context.append_loop_event",
@@ -81590,6 +82042,8 @@ function buildGoalReminder(goal) {
81590
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.");
81591
82043
  lines.push("");
81592
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.");
81593
82047
  return lines.join("\n");
81594
82048
  }
81595
82049
  function maxBudgetFraction(goal) {
@@ -97303,6 +97757,13 @@ function buildGraderPrompt(objective, criteria, output) {
97303
97757
  "## Agent Output",
97304
97758
  output || "(no output captured)",
97305
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
+ "",
97306
97767
  "Evaluate each dimension independently against the acceptance criteria, then decide overall PASS/FAIL.",
97307
97768
  "When FAIL, list every specific issue with an actionable fix direction so the agent knows exactly what to address next.",
97308
97769
  "Respond with JSON:",
@@ -97983,14 +98444,21 @@ const GOAL_CONTINUATION_PROMPT = [
97983
98444
  "reassess. Call UpdateGoal with `complete` only when all required work is done, any stated",
97984
98445
  "validation has passed, and there is no useful next action. Do not mark complete after only",
97985
98446
  "producing a plan, summary, first pass, or partial result. If an external condition or required",
97986
- "user input prevents progress, or the objective cannot be completed as stated, call UpdateGoal",
97987
- "with `blocked`. Otherwise keep going use the existing conversation context and your tools,",
97988
- "and do not ask the user for input unless a real blocker prevents progress."
98447
+ "user input prevents progress, call UpdateGoal with `blocked` and include a `reason`. The goal",
98448
+ "will only be marked blocked after you report the same blocker for at least 3 consecutive",
98449
+ "turns, so first try alternative approaches. Otherwise keep going use the existing",
98450
+ "conversation context and your tools, and do not ask the user for input unless a real blocker",
98451
+ "prevents progress."
97989
98452
  ].join(" ");
97990
98453
  const GOAL_CONTINUATION_ORIGIN = {
97991
98454
  kind: "system_trigger",
97992
98455
  name: "goal_continuation"
97993
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
+ };
97994
98462
  var TurnFlow = class {
97995
98463
  agent;
97996
98464
  steerBuffer = [];
@@ -98155,6 +98623,14 @@ var TurnFlow = class {
98155
98623
  }
98156
98624
  }
98157
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
+ }
98158
98634
  const end = await this.runOneTurn(turnId, turnInput, turnOrigin, signal, false);
98159
98635
  if (end.event.reason === "cancelled") {
98160
98636
  await this.agent.goal.pauseOnInterrupt({ reason: "Paused after interruption" });
@@ -98346,7 +98822,7 @@ var TurnFlow = class {
98346
98822
  turnId: String(turnId),
98347
98823
  signal,
98348
98824
  llm: this.agent.llm,
98349
- buildMessages: () => this.agent.context.messages,
98825
+ buildMessages: () => this.agent.context.messagesForLLM(),
98350
98826
  dispatchEvent: this.buildDispatchEvent(turnId),
98351
98827
  tools: this.agent.tools.loopTools,
98352
98828
  log: this.agent.log,
@@ -98875,6 +99351,7 @@ var LtodLLM = class {
98875
99351
  this.obfuscator = config.obfuscator;
98876
99352
  }
98877
99353
  async chat(params) {
99354
+ const chatStartedAt = Date.now();
98878
99355
  let requestStartedAt = Date.now();
98879
99356
  let firstChunkAt;
98880
99357
  let streamEndedAt;
@@ -98894,13 +99371,22 @@ var LtodLLM = class {
98894
99371
  capability: this.capability
98895
99372
  });
98896
99373
  const outboundMessages = this.obfuscator && this.obfuscator.hasSecrets() ? obfuscateMessages(this.obfuscator, params.messages) : params.messages;
98897
- const result = await this.generate(effectiveProvider, this.systemPrompt, [...params.tools], outboundMessages, callbacks, generateOptions(params, {
99374
+ const runGenerate = (messages) => this.generate(effectiveProvider, this.systemPrompt, [...params.tools], messages, callbacks, generateOptions(params, {
98898
99375
  onRequestStart: markRequestStart,
98899
99376
  onStreamEnd: () => {
98900
99377
  markStreamEnd();
98901
99378
  flushPending();
98902
99379
  }
98903
99380
  }));
99381
+ let result;
99382
+ try {
99383
+ result = await runGenerate(outboundMessages);
99384
+ } catch (error) {
99385
+ if (!(error instanceof APIContextOverflowError)) throw error;
99386
+ const strippedMessages = stripMediaFromMessages(outboundMessages);
99387
+ if (strippedMessages === null) throw error;
99388
+ result = await runGenerate(strippedMessages);
99389
+ }
98904
99390
  if (params.onTextPart !== void 0 || params.onThinkPart !== void 0) {
98905
99391
  for (const part of result.message.content) if (part.type === "text" && params.onTextPart !== void 0) await params.onTextPart(part);
98906
99392
  else if (part.type === "think" && params.onThinkPart !== void 0) await params.onThinkPart(part);
@@ -98910,18 +99396,21 @@ var LtodLLM = class {
98910
99396
  providerFinishReason: result.finishReason ?? void 0,
98911
99397
  rawFinishReason: result.rawFinishReason ?? void 0,
98912
99398
  usage: result.usage ?? emptyUsage(),
98913
- streamTiming: firstChunkAt === void 0 ? void 0 : buildStreamTiming(requestStartedAt, firstChunkAt, streamEndedAt)
99399
+ streamTiming: firstChunkAt === void 0 ? void 0 : buildStreamTiming(chatStartedAt, requestStartedAt, firstChunkAt, streamEndedAt)
98914
99400
  };
98915
99401
  }
98916
99402
  isRetryableError(error) {
98917
99403
  return isRetryableGenerateError(error);
98918
99404
  }
98919
99405
  };
98920
- function buildStreamTiming(requestStartedAt, firstChunkAt, streamEndedAt) {
99406
+ function buildStreamTiming(chatStartedAt, requestStartedAt, firstChunkAt, streamEndedAt) {
98921
99407
  const outputEndedAt = streamEndedAt ?? Date.now();
98922
99408
  return {
98923
99409
  firstTokenLatencyMs: Math.max(0, firstChunkAt - requestStartedAt),
98924
- streamDurationMs: Math.max(0, outputEndedAt - firstChunkAt)
99410
+ streamDurationMs: Math.max(0, outputEndedAt - firstChunkAt),
99411
+ requestBuildMs: Math.max(0, requestStartedAt - chatStartedAt),
99412
+ serverFirstTokenMs: Math.max(0, firstChunkAt - requestStartedAt),
99413
+ serverDecodeMs: Math.max(0, outputEndedAt - firstChunkAt)
98925
99414
  };
98926
99415
  }
98927
99416
  function generateOptions(params, hooks) {
@@ -99056,6 +99545,27 @@ function buildLtodCallbacks(params, markStreamOutput, obfuscator) {
99056
99545
  }
99057
99546
  };
99058
99547
  }
99548
+ /**
99549
+ * Remove image parts from messages for context-overflow retry.
99550
+ * Returns null when no media was found (no point retrying without media).
99551
+ */
99552
+ function stripMediaFromMessages(messages) {
99553
+ let hasMedia = false;
99554
+ const stripped = messages.map((msg) => {
99555
+ const newContent = msg.content.filter((part) => {
99556
+ if (part.type !== "text" && part.type !== "think") {
99557
+ hasMedia = true;
99558
+ return false;
99559
+ }
99560
+ return true;
99561
+ });
99562
+ return {
99563
+ ...msg,
99564
+ content: newContent
99565
+ };
99566
+ });
99567
+ return hasMedia ? stripped : null;
99568
+ }
99059
99569
  //#endregion
99060
99570
  //#region ../../packages/agent-core/src/agent/usage/index.ts
99061
99571
  function copyUsage(usage) {
@@ -99447,6 +99957,9 @@ var Agent = class {
99447
99957
  if (status === "paused") return this.goal.pauseGoal({}, "user");
99448
99958
  return this.goal.resumeGoal({}, "user");
99449
99959
  },
99960
+ updateGoalObjective: async (payload) => {
99961
+ return this.goal.updateObjective({ objective: payload.objective }, "user");
99962
+ },
99450
99963
  cancelGoal: async () => {
99451
99964
  return this.goal.cancelGoal("user");
99452
99965
  },
@@ -119503,6 +120016,9 @@ var SessionAPIImpl = class {
119503
120016
  updateGoalStatus({ agentId, ...payload }) {
119504
120017
  return this.getAgent(agentId).updateGoalStatus(payload);
119505
120018
  }
120019
+ updateGoalObjective({ agentId, ...payload }) {
120020
+ return this.getAgent(agentId).updateGoalObjective(payload);
120021
+ }
119506
120022
  cancelGoal({ agentId, ...payload }) {
119507
120023
  return this.getAgent(agentId).cancelGoal(payload);
119508
120024
  }
@@ -121245,6 +121761,9 @@ var ScreamCore = class {
121245
121761
  updateGoalStatus({ sessionId, ...payload }) {
121246
121762
  return this.sessionApi(sessionId).updateGoalStatus(payload);
121247
121763
  }
121764
+ updateGoalObjective({ sessionId, ...payload }) {
121765
+ return this.sessionApi(sessionId).updateGoalObjective(payload);
121766
+ }
121248
121767
  cancelGoal({ sessionId, ...payload }) {
121249
121768
  return this.sessionApi(sessionId).cancelGoal(payload);
121250
121769
  }
@@ -121698,6 +122217,13 @@ var SDKRpcClient = class {
121698
122217
  status: input.status
121699
122218
  });
121700
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
+ }
121701
122227
  async cancelGoal(input) {
121702
122228
  return (await this.getRpc()).cancelGoal({
121703
122229
  sessionId: input.sessionId,
@@ -122458,6 +122984,13 @@ var Session = class {
122458
122984
  status
122459
122985
  });
122460
122986
  }
122987
+ async updateGoalObjective(objective) {
122988
+ this.ensureOpen();
122989
+ return this.rpc.updateGoalObjective({
122990
+ sessionId: this.id,
122991
+ objective
122992
+ });
122993
+ }
122461
122994
  async cancelGoal() {
122462
122995
  this.ensureOpen();
122463
122996
  return this.rpc.cancelGoal({ sessionId: this.id });
@@ -122917,7 +123450,7 @@ function optionalBuildString(value) {
122917
123450
  return typeof value === "string" && value.length > 0 ? value : void 0;
122918
123451
  }
122919
123452
  const SCREAM_BUILD_INFO = {
122920
- version: optionalBuildString("0.10.6"),
123453
+ version: optionalBuildString("0.10.8"),
122921
123454
  channel: optionalBuildString(""),
122922
123455
  commit: optionalBuildString(""),
122923
123456
  buildTarget: optionalBuildString("darwin-arm64")
@@ -123910,6 +124443,7 @@ const BUILTIN_SLASH_COMMANDS = [
123910
124443
  name: "goal",
123911
124444
  aliases: ["goaloff"],
123912
124445
  description: "registry.goal_desc",
124446
+ argumentHint: "[objective]",
123913
124447
  priority: 122,
123914
124448
  availability: (args) => {
123915
124449
  const trimmed = args.trim();
@@ -123920,6 +124454,7 @@ const BUILTIN_SLASH_COMMANDS = [
123920
124454
  name: "memory",
123921
124455
  aliases: ["memo", "mem"],
123922
124456
  description: "registry.memory_desc",
124457
+ argumentHint: "[query]",
123923
124458
  priority: 120,
123924
124459
  availability: "always"
123925
124460
  },
@@ -123927,6 +124462,7 @@ const BUILTIN_SLASH_COMMANDS = [
123927
124462
  name: "knowledge",
123928
124463
  aliases: ["know"],
123929
124464
  description: "registry.knowledge_desc",
124465
+ argumentHint: "[query]",
123930
124466
  priority: 119,
123931
124467
  availability: "always"
123932
124468
  },
@@ -123940,6 +124476,7 @@ const BUILTIN_SLASH_COMMANDS = [
123940
124476
  name: "model",
123941
124477
  aliases: [],
123942
124478
  description: "registry.model_desc",
124479
+ argumentHint: "[alias]",
123943
124480
  priority: 120
123944
124481
  },
123945
124482
  {
@@ -126123,6 +126660,24 @@ function formatContextStatus(usage, tokens, maxTokens) {
126123
126660
  });
126124
126661
  return t("footer.context_short", { pct });
126125
126662
  }
126663
+ /** Format goal wall-clock duration compactly: `3m`, `1m30s`, `45s`. */
126664
+ function formatGoalDuration(ms) {
126665
+ const totalSeconds = Math.floor(ms / 1e3);
126666
+ if (totalSeconds < 60) return `${totalSeconds}s`;
126667
+ const minutes = Math.floor(totalSeconds / 60);
126668
+ const seconds = totalSeconds % 60;
126669
+ return seconds > 0 ? `${minutes}m${seconds}s` : `${minutes}m`;
126670
+ }
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`;
126680
+ }
126126
126681
  const CONTEXT_WARNING_PERCENT_THRESHOLD = 60;
126127
126682
  const CONTEXT_ERROR_PERCENT_THRESHOLD = 90;
126128
126683
  function pickContextColor(usage, colors) {
@@ -126148,7 +126703,7 @@ const SPINNER_FRAMES$1 = [
126148
126703
  "◎",
126149
126704
  "◉"
126150
126705
  ];
126151
- const SPINNER_TICK_MS = 120;
126706
+ const SPINNER_TICK_MS = 60;
126152
126707
  function hexToRgb$2(hex) {
126153
126708
  const v = parseInt(hex.slice(1), 16);
126154
126709
  return [
@@ -126172,7 +126727,7 @@ function lerpGradient(t) {
126172
126727
  }
126173
126728
  function buildStatusLine(streamingPhase, streamingStartTime, reconnectAttempt) {
126174
126729
  if (streamingPhase === "idle") return t("status.idle");
126175
- if (reconnectAttempt > 0) return chalk.hex("#E85454").bold("◎") + " " + chalk.hex("#E85454")(`${t("status.reconnecting")} ${String(reconnectAttempt)}`);
126730
+ if (reconnectAttempt > 0 && streamingPhase === "waiting") return chalk.hex("#E85454").bold("◎") + " " + chalk.hex("#E85454")(`${t("status.reconnecting")} ${String(reconnectAttempt)}`);
126176
126731
  let label;
126177
126732
  if (streamingPhase === "tool") label = t("status.tool");
126178
126733
  else if (streamingPhase === "waiting") label = t("status.waiting");
@@ -126217,15 +126772,17 @@ var FooterComponent = class {
126217
126772
  this.onGitStatusChange = onGitStatusChange;
126218
126773
  this.gitCacheWorkDir = state.workDir;
126219
126774
  this.gitCache = createGitStatusCache(state.workDir, { onChange: this.onGitStatusChange });
126775
+ this.#restartStatusTimer(state.streamingPhase, state.goalActive);
126220
126776
  }
126221
126777
  setState(state) {
126222
126778
  const previousPhase = this.state?.streamingPhase;
126779
+ const previousGoalActive = this.state?.goalActive;
126223
126780
  if (state.workDir !== this.gitCacheWorkDir) {
126224
126781
  this.gitCacheWorkDir = state.workDir;
126225
126782
  this.gitCache = createGitStatusCache(state.workDir, { onChange: this.onGitStatusChange });
126226
126783
  }
126227
126784
  this.state = state;
126228
- if (state.streamingPhase !== previousPhase) this.#restartStatusTimer(state.streamingPhase);
126785
+ if (state.streamingPhase !== previousPhase || state.goalActive !== previousGoalActive) this.#restartStatusTimer(state.streamingPhase, state.goalActive);
126229
126786
  }
126230
126787
  setColors(colors) {
126231
126788
  this.colors = colors;
@@ -126253,10 +126810,10 @@ var FooterComponent = class {
126253
126810
  dispose() {
126254
126811
  this.#stopStatusTimer();
126255
126812
  }
126256
- #restartStatusTimer(phase) {
126813
+ #restartStatusTimer(phase, goalActive) {
126257
126814
  this.#stopStatusTimer();
126258
- if (phase === "idle") return;
126259
- const intervalMs = phase === "thinking" ? 1e3 / 30 : SPINNER_TICK_MS;
126815
+ if (phase === "idle" && !goalActive) return;
126816
+ const intervalMs = 1e3 / 60;
126260
126817
  this.statusTimer = setInterval(() => {
126261
126818
  this.ui.requestRender();
126262
126819
  }, intervalMs);
@@ -126275,7 +126832,10 @@ var FooterComponent = class {
126275
126832
  left.push(chalk.hex(isFusion ? colors.fusionPlanMode : colors.planMode).bold(isFusion ? t("badge.fusion") : t("badge.plan")));
126276
126833
  }
126277
126834
  if (state.wolfpackMode) left.push(chalk.hex(colors.wolfpackMode).bold(t("badge.wolfpack")));
126278
- if (state.goalActive) left.push(chalk.hex(colors.primary).bold(t("badge.goal")));
126835
+ if (state.goalActive && state.goal) {
126836
+ const goalLabel = formatGoalBadge(state.goal);
126837
+ left.push(chalk.hex(colors.primary).bold(goalLabel));
126838
+ }
126279
126839
  const model = shortenModel(modelDisplayName(state));
126280
126840
  if (model) if (state.streamingPhase === "thinking") left.push(shimmerText(model, colors));
126281
126841
  else left.push(chalk.hex(colors.textDim)(model));
@@ -126649,10 +127209,63 @@ function createThemeStyles(colors) {
126649
127209
  //#endregion
126650
127210
  //#region src/tui/theme/pi-tui-theme.ts
126651
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
+ }
126652
127264
  function createMarkdownTheme(colors) {
126653
127265
  const stripHash = (text) => text.replace(HEADING_HASH_PREFIX, "$1");
126654
127266
  const muted = chalk.hex(colors.textMuted);
126655
127267
  const border = chalk.hex(colors.border);
127268
+ const codeTheme = createCodeHighlightTheme(colors);
126656
127269
  return {
126657
127270
  heading: (text) => chalk.bold.hex(colors.text)(stripHash(text)),
126658
127271
  link: (text) => chalk.hex(colors.mdLink)(text),
@@ -126674,7 +127287,8 @@ function createMarkdownTheme(colors) {
126674
127287
  try {
126675
127288
  return highlight(code, {
126676
127289
  language,
126677
- ignoreIllegals: true
127290
+ ignoreIllegals: true,
127291
+ theme: codeTheme
126678
127292
  }).split("\n");
126679
127293
  } catch {
126680
127294
  return code.split("\n");
@@ -128103,6 +128717,10 @@ function buildEmptyGoalLines(colors) {
128103
128717
  value(t("goalpanel.no_goal")),
128104
128718
  "",
128105
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"))}`,
128106
128724
  `${muted("/goal pause")} ${muted(t("goalpanel.pause_goal"))}`,
128107
128725
  `${muted("/goal resume")} ${muted(t("goalpanel.resume_goal"))}`,
128108
128726
  `${muted("/goaloff")} ${muted(t("goalpanel.cancel_goal"))}`
@@ -128165,6 +128783,19 @@ function parseGoalCommand(rawArgs) {
128165
128783
  const tokens = args.split(/\s+/);
128166
128784
  const first = tokens[0];
128167
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
+ }
128168
128799
  let index = 0;
128169
128800
  let replace = false;
128170
128801
  if (tokens[index] === "replace") {
@@ -128194,6 +128825,9 @@ async function handleGoalCommand(host, args) {
128194
128825
  case "status":
128195
128826
  await showGoalStatus(host);
128196
128827
  return;
128828
+ case "setup":
128829
+ await guidedGoalSetup(host);
128830
+ return;
128197
128831
  case "pause":
128198
128832
  await pauseGoal(host);
128199
128833
  return;
@@ -128203,6 +128837,9 @@ async function handleGoalCommand(host, args) {
128203
128837
  case "off":
128204
128838
  await handleGoalOffCommand(host);
128205
128839
  return;
128840
+ case "update":
128841
+ await updateGoalObjective(host, parsed);
128842
+ return;
128206
128843
  case "create":
128207
128844
  await createGoal(host, parsed);
128208
128845
  return;
@@ -128220,8 +128857,51 @@ async function createGoal(host, parsed) {
128220
128857
  }
128221
128858
  await showGoalConfigWizard(host, session, parsed.objective, parsed.replace);
128222
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
+ }
128223
128903
  async function showGoalConfigWizard(host, session, objective, replace) {
128224
- const { TextInputDialogComponent } = await import("./text-input-dialog-zZlNFEk3.mjs");
128904
+ const { TextInputDialogComponent } = await import("./text-input-dialog-B_rgO4qn.mjs");
128225
128905
  const turnInput = await promptNumber(host, TextInputDialogComponent, {
128226
128906
  title: t("goal.wizard_title", { objective }),
128227
128907
  subtitle: t("goal.budget_turns_hint"),
@@ -128291,6 +128971,27 @@ function promptNumber(host, TextInputDialogComponent, opts) {
128291
128971
  host.mountEditorReplacement(dialog);
128292
128972
  });
128293
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
+ }
128294
128995
  async function pauseGoal(host) {
128295
128996
  const session = host.session;
128296
128997
  if (session === void 0) {
@@ -128331,6 +129032,24 @@ async function resumeGoal(host) {
128331
129032
  host.showError(t("goal.resume_failed", { msg: message }));
128332
129033
  }
128333
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
+ }
128334
129053
  async function handleGoalOffCommand(host) {
128335
129054
  const session = host.session;
128336
129055
  if (session === void 0) {
@@ -129360,6 +130079,12 @@ function easeSpeedRatio(ratio) {
129360
130079
  }
129361
130080
  //#endregion
129362
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
+ }
129363
130088
  var ThinkingComponent = class {
129364
130089
  text;
129365
130090
  color;
@@ -129398,7 +130123,7 @@ var ThinkingComponent = class {
129398
130123
  this.textComponent.setText(this.styled(trimmed));
129399
130124
  }
129400
130125
  styled(text) {
129401
- return chalk.hex(this.color).italic(text);
130126
+ return chalk.hex(this.color).italic(filterThinkingNoise(text));
129402
130127
  }
129403
130128
  finalize() {
129404
130129
  if (this.mode === "finalized") return;
@@ -129575,6 +130300,47 @@ function makeDiffStyles(colors) {
129575
130300
  meta: (s) => chalk.hex(colors.diffMeta)(s)
129576
130301
  };
129577
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
+ }
129578
130344
  /**
129579
130345
  * Compute word-level diff between two single lines and highlight the changed
129580
130346
  * words with `chalk.inverse()`. Only the first removed/added part has its
@@ -129681,6 +130447,8 @@ function computeDiffLines(oldText, newText, oldStart = 1, newStart = 1, isIncomp
129681
130447
  }
129682
130448
  function renderDiffLines(oldText, newText, path, colors, isIncomplete = false, oldStart, newStart, maxLines) {
129683
130449
  const s = makeDiffStyles(colors);
130450
+ const lang = langFromPath(path);
130451
+ const doHighlight = !isIncomplete && lang !== void 0;
129684
130452
  const changedLines = computeDiffLines(oldText, newText, oldStart ?? 1, newStart ?? 1, isIncomplete).filter((l) => l.kind !== "context");
129685
130453
  const added = changedLines.filter((l) => l.kind === "add").length;
129686
130454
  const removed = changedLines.filter((l) => l.kind === "delete").length;
@@ -129705,7 +130473,7 @@ function renderDiffLines(oldText, newText, path, colors, isIncomplete = false, o
129705
130473
  const line = shown[i];
129706
130474
  const marker = line.kind === "add" ? "+" : "-";
129707
130475
  const color = line.kind === "add" ? s.add : s.del;
129708
- 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));
129709
130477
  i += 1;
129710
130478
  }
129711
130479
  const hidden = changedLines.length - shown.length;
@@ -129756,11 +130524,11 @@ function buildClusters(diffLines, contextLines) {
129756
130524
  removedCount: removed
129757
130525
  };
129758
130526
  }
129759
- function formatDiffRow(line, s) {
130527
+ function formatDiffRow(line, s, doHighlight, lang) {
129760
130528
  const gutter = s.gutter(String(line.lineNum).padStart(4) + " ");
129761
- if (line.kind === "add") return gutter + s.add("+ " + line.code);
129762
- if (line.kind === "delete") return gutter + s.del("- " + line.code);
129763
- 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);
129764
130532
  }
129765
130533
  /**
129766
130534
  * Render a diff with surrounding context, eliding unchanged middle
@@ -129773,6 +130541,8 @@ function formatDiffRow(line, s) {
129773
130541
  */
129774
130542
  function renderDiffLinesClustered(oldText, newText, path, colors, opts = {}) {
129775
130543
  const s = makeDiffStyles(colors);
130544
+ const lang = langFromPath(path);
130545
+ const doHighlight = !(opts.isIncomplete ?? false) && lang !== void 0;
129776
130546
  const contextLines = opts.contextLines ?? 3;
129777
130547
  const maxLines = opts.maxLines;
129778
130548
  const diffLines = computeDiffLines(oldText, newText, 1, 1, opts.isIncomplete ?? false);
@@ -129829,7 +130599,7 @@ function renderDiffLinesClustered(oldText, newText, path, colors, opts = {}) {
129829
130599
  i += 2;
129830
130600
  continue;
129831
130601
  }
129832
- output.push(formatDiffRow(line, s));
130602
+ output.push(formatDiffRow(line, s, doHighlight, lang));
129833
130603
  body++;
129834
130604
  if (line.kind !== "context") shownChanges++;
129835
130605
  prevEnd = i;
@@ -138266,7 +139036,10 @@ var SessionEventHandler = class {
138266
139036
  pendingApproval: null,
138267
139037
  pendingQuestion: null
138268
139038
  });
138269
- this.host.setAppState({ streamingPhase: "waiting" });
139039
+ this.host.setAppState({
139040
+ streamingPhase: "waiting",
139041
+ reconnectAttempt: 0
139042
+ });
138270
139043
  }
138271
139044
  handleStepCompleted(event) {
138272
139045
  this.host.streamingUI.flushNow();
@@ -138564,7 +139337,12 @@ var SessionEventHandler = class {
138564
139337
  goalActive: false
138565
139338
  });
138566
139339
  else this.host.setAppState({
138567
- goal: snapshot.objective,
139340
+ goal: {
139341
+ objective: snapshot.objective,
139342
+ turnsUsed: snapshot.turnsUsed ?? 0,
139343
+ wallClockMs: snapshot.wallClockMs ?? 0,
139344
+ wallClockBaseAt: Date.now()
139345
+ },
138568
139346
  goalActive: snapshot.status === "active"
138569
139347
  });
138570
139348
  }
@@ -142383,9 +143161,10 @@ var FileMentionProvider = class {
142383
143161
  const name = ac.value ?? ac.name ?? "";
142384
143162
  const desc = ac.description ?? "";
142385
143163
  const resolvedDesc = desc ? t(desc) : "";
143164
+ const aliases = cmd.aliases;
142386
143165
  return {
142387
143166
  value: name,
142388
- label: `/${name}${resolvedDesc ? ` — ${resolvedDesc}` : ""}`
143167
+ label: `/${name}${aliases && aliases.length > 0 ? ` (${aliases.join(", ")})` : ""}${resolvedDesc ? ` — ${resolvedDesc}` : ""}`
142389
143168
  };
142390
143169
  });
142391
143170
  this.inner = new CombinedAutocompleteProvider(slashCommands, workDir, fdPath);
@@ -142740,10 +143519,18 @@ var InputController = class InputController {
142740
143519
  this.host = host;
142741
143520
  }
142742
143521
  setupAutocomplete() {
142743
- const slashCommands = this.host.getSlashCommands().filter((cmd) => !cmd.name.startsWith("skill:")).map((cmd) => cmd);
143522
+ const visible = this.host.getSlashCommands().filter((cmd) => !cmd.name.startsWith("skill:"));
143523
+ const slashCommands = visible.map((cmd) => cmd);
142744
143524
  const { state } = this.host;
142745
143525
  const provider = new FileMentionProvider(slashCommands, state.appState.workDir, state.fdPath, state.gitLsFilesCache);
142746
143526
  state.editor.setAutocompleteProvider(provider);
143527
+ const argumentHints = /* @__PURE__ */ new Map();
143528
+ for (const cmd of visible) {
143529
+ if (cmd.argumentHint === void 0) continue;
143530
+ argumentHints.set(cmd.name, cmd.argumentHint);
143531
+ for (const alias of cmd.aliases) argumentHints.set(alias, cmd.argumentHint);
143532
+ }
143533
+ state.editor.setArgumentHints(argumentHints);
142747
143534
  state.editor.onFirstInput = () => {
142748
143535
  this.host.stopWelcomeBreathing();
142749
143536
  this.#permanentlyStopBreathing();
@@ -144628,7 +145415,12 @@ var SessionManager = class {
144628
145415
  maxContextTokens: status.maxContextTokens,
144629
145416
  contextUsage: status.contextUsage,
144630
145417
  sessionTitle: session.summary?.title ?? null,
144631
- goal: goal?.objective ?? null,
145418
+ goal: goal ? {
145419
+ objective: goal.objective,
145420
+ turnsUsed: goal.turnsUsed ?? 0,
145421
+ wallClockMs: goal.wallClockMs ?? 0,
145422
+ wallClockBaseAt: Date.now()
145423
+ } : null,
144632
145424
  goalActive: goal?.status === "active",
144633
145425
  goalContinuationCount: 0
144634
145426
  });