scream-code 0.12.4 → 0.12.6

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-DHhRrX3G.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-BI6Q02oM.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";
@@ -54,7 +54,7 @@ import { AsyncLocalStorage } from "node:async_hooks";
54
54
  import { Command, Option } from "commander";
55
55
  import { createInterface } from "node:readline/promises";
56
56
  import chalk, { chalkStderr } from "chalk";
57
- import { CombinedAutocompleteProvider, Container, Editor, FlexSpacer, Image, Input, Key, Markdown, ProcessTerminal, Spacer, TUI, Text, decodeKittyPrintable, deleteAllKittyImages, fuzzyFilter, fuzzyMatch, getCapabilities, getImageDimensions, isKeyRelease, matchesKey, setTightMode, sliceByColumn, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@liutod-scream/pi-tui";
57
+ import { CombinedAutocompleteProvider, Container, Editor, HStack, Image, Input, Key, Markdown, ProcessTerminal, ScrollView, Spacer, Text, TuiAltScreen, VStack, decodeKittyPrintable, deleteAllKittyImages, fuzzyFilter, fuzzyMatch, getCapabilities, getImageDimensions, isKeyRelease, matchesKey, sliceByColumn, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@liutod-scream/pi-tui";
58
58
  import { highlight, supportsLanguage } from "cli-highlight";
59
59
  import { diffWords } from "diff";
60
60
  import { gt, valid } from "semver";
@@ -53309,7 +53309,8 @@ function createMemoryMemo(partial) {
53309
53309
  extractionSource: partial.extractionSource,
53310
53310
  recordedAt: partial.recordedAt ?? Date.now(),
53311
53311
  projectDir: partial.projectDir ?? "",
53312
- tags: normalizedTags(partial.tags)
53312
+ tags: normalizedTags(partial.tags),
53313
+ note: partial.note
53313
53314
  };
53314
53315
  }
53315
53316
  function normalizedTags(value) {
@@ -53329,7 +53330,8 @@ function toSummary(memo) {
53329
53330
  extractionSource: memo.extractionSource,
53330
53331
  recordedAt: memo.recordedAt,
53331
53332
  projectDir: memo.projectDir,
53332
- tags: memo.tags
53333
+ tags: memo.tags,
53334
+ note: memo.note
53333
53335
  };
53334
53336
  }
53335
53337
  //#endregion
@@ -54214,6 +54216,7 @@ function parseMemoryMemos(text) {
54214
54216
  if (userNeed.length === 0) continue;
54215
54217
  const rawTags = parsed["tags"];
54216
54218
  const tags = Array.isArray(rawTags) ? normalizeTags(rawTags) : void 0;
54219
+ const note = typeof parsed["note"] === "string" ? parsed["note"].trim() : void 0;
54217
54220
  memos.push(createMemoryMemo({
54218
54221
  userNeed,
54219
54222
  approach: typeof parsed["approach"] === "string" ? parsed["approach"].trim() : "",
@@ -54221,6 +54224,7 @@ function parseMemoryMemos(text) {
54221
54224
  whatFailed: typeof parsed["whatFailed"] === "string" ? parsed["whatFailed"].trim() : "none",
54222
54225
  whatWorked: typeof parsed["whatWorked"] === "string" ? parsed["whatWorked"].trim() : "none",
54223
54226
  tags,
54227
+ note: note && note.length > 0 ? note : void 0,
54224
54228
  extractionSource: "compaction",
54225
54229
  sourceSessionId: "",
54226
54230
  sourceSessionTitle: ""
@@ -54259,7 +54263,8 @@ function buildExitExtractionPrompt(sessionId, messageCount, sampleText) {
54259
54263
  "outcome": "<最终结果,如'完成'、'部分完成'、'失败:原因'>",
54260
54264
  "whatFailed": "<踩坑记录:试了但不行的路,无则填 'none'>",
54261
54265
  "whatWorked": "<成功经验:最终奏效的关键动作,无则填 'none'>",
54262
- "tags": ["<标签1>", "<标签2>", "<标签3>"]
54266
+ "tags": ["<标签1>", "<标签2>", "<标签3>"],
54267
+ "note": "<可选:给 AI 的补充说明/建议——让未来理解这条经验更容易,如'这类任务先查接口可用性再写代码'。无则省略>"
54263
54268
  }
54264
54269
  \`\`\`
54265
54270
 
@@ -54267,9 +54272,10 @@ function buildExitExtractionPrompt(sessionId, messageCount, sampleText) {
54267
54272
  - tags 是 3-5 个语义标签,概括任务领域/技术栈/动作类型,例如 ["react", "auth", "部署"]
54268
54273
  - whatFailed 记录重要的错误尝试,帮助未来避免重蹈覆辙
54269
54274
  - whatWorked 记录最终成功的关键动作,帮助未来复用经验
54275
+ - note 是可选字段,写一句能让未来 AI 更快理解/复用这条经验的说明或建议(软性提示,非强制规则)
54270
54276
  - 跳过未完成的工作,除非其中包含有价值的踩坑经验
54271
54277
  - 将紧密相关的子任务合并为一条记录
54272
- - 严格遵守字段名和 JSON 格式,不要添加额外字段
54278
+ - 严格遵守字段名和 JSON 格式(除 note 外不要添加额外字段)
54273
54279
 
54274
54280
  如果没有已完成的任务闭环,输出:
54275
54281
  \`\`\`memory-memo
@@ -54964,7 +54970,8 @@ const MemoryWriteInputSchema = z.object({
54964
54970
  outcome: z.string().min(1).describe("Final outcome, e.g. \"完成\", \"部分完成\", \"失败\"."),
54965
54971
  whatFailed: z.string().optional().describe("Dead ends tried — things that did not work. Use \"none\" if nothing notable."),
54966
54972
  whatWorked: z.string().optional().describe("What ultimately worked — key actions that led to success. Use \"none\" if nothing notable."),
54967
- tags: z.array(z.string()).optional().describe("3-5 semantic tags summarizing the task domain, tech stack, or action type (e.g. [\"react\", \"auth\", \"部署\"]).")
54973
+ tags: z.array(z.string()).optional().describe("3-5 semantic tags summarizing the task domain, tech stack, or action type (e.g. [\"react\", \"auth\", \"部署\"])."),
54974
+ note: z.string().optional().describe("Optional advisory note that helps future AI understand/reuse this memo faster — a soft suggestion, not a user-enforced rule.")
54968
54975
  });
54969
54976
  /**
54970
54977
  * Lets the model actively write a new memory memo to the global store.
@@ -54997,6 +55004,7 @@ var MemoryWriteTool = class {
54997
55004
  const whatFailed = stripMemoryTags(args.whatFailed ?? "");
54998
55005
  const whatWorked = stripMemoryTags(args.whatWorked ?? "");
54999
55006
  const tags = normalizeTags(args.tags !== void 0 && args.tags.length > 0 ? args.tags : generateTags(`${userNeed} ${approach}`));
55007
+ const note = args.note ? stripMemoryTags(args.note).trim() : void 0;
55000
55008
  const memo = createMemoryMemo({
55001
55009
  sourceSessionId: sessionId,
55002
55010
  sourceSessionTitle,
@@ -55006,6 +55014,7 @@ var MemoryWriteTool = class {
55006
55014
  whatFailed: whatFailed.length === 0 ? "none" : whatFailed,
55007
55015
  whatWorked: whatWorked.length === 0 ? "none" : whatWorked,
55008
55016
  tags,
55017
+ note: note && note.length > 0 ? note : void 0,
55009
55018
  extractionSource: "manual",
55010
55019
  projectDir: this.agent.config.cwd
55011
55020
  });
@@ -72330,7 +72339,7 @@ z.object({
72330
72339
  numMatches: z.number().int().nonnegative().optional(),
72331
72340
  appliedLimit: z.number().int().nonnegative().optional()
72332
72341
  });
72333
- const DEFAULT_TIMEOUT_MS = 2e4;
72342
+ const DEFAULT_TIMEOUT_MS$1 = 2e4;
72334
72343
  const SIGTERM_GRACE_MS$1 = 5e3;
72335
72344
  const MAX_OUTPUT_BYTES = 10 * 1024 * 1024;
72336
72345
  const RG_MAX_COLUMNS = 500;
@@ -72443,7 +72452,7 @@ var GrepTool = class {
72443
72452
  if (bufferTruncated || timedOut) stdoutText = omitIncompleteTrailingRecord(stdoutText, mode);
72444
72453
  if (timedOut && stdoutText.trim() === "") return {
72445
72454
  isError: true,
72446
- output: `Grep timed out after ${String(DEFAULT_TIMEOUT_MS / 1e3)}s. Try a more specific path or pattern.`
72455
+ output: `Grep timed out after ${String(DEFAULT_TIMEOUT_MS$1 / 1e3)}s. Try a more specific path or pattern.`
72447
72456
  };
72448
72457
  if (signal.aborted) return {
72449
72458
  isError: true,
@@ -72483,7 +72492,7 @@ var GrepTool = class {
72483
72492
  else messages.push(paginationNotice);
72484
72493
  }
72485
72494
  if (bufferTruncated) messages.push(`[stdout truncated at ${String(MAX_OUTPUT_BYTES)} bytes; incomplete trailing line omitted]`);
72486
- if (timedOut) messages.push(`Grep timed out after ${String(DEFAULT_TIMEOUT_MS / 1e3)}s; partial results returned`);
72495
+ if (timedOut) messages.push(`Grep timed out after ${String(DEFAULT_TIMEOUT_MS$1 / 1e3)}s; partial results returned`);
72487
72496
  const contentIncludesLineNumbers = mode === "content" && args["-n"] !== false;
72488
72497
  const contentBody = limited.map((line) => formatDisplayLine(line, mode, this.workspace.workspaceDir, pathClass, contentIncludesLineNumbers)).join("\n");
72489
72498
  const visibleBody = orderedLines.length === 0 && filteredSensitive.size > 0 ? "No non-sensitive matches found" : contentBody;
@@ -72600,7 +72609,7 @@ async function runRipgrepOnce(jian, rgArgs, signal) {
72600
72609
  const timeoutHandle = setTimeout(() => {
72601
72610
  timedOut = true;
72602
72611
  killProc();
72603
- }, DEFAULT_TIMEOUT_MS);
72612
+ }, DEFAULT_TIMEOUT_MS$1);
72604
72613
  let exitCode = 0;
72605
72614
  let stdoutText = "";
72606
72615
  let stderrText = "";
@@ -74711,6 +74720,466 @@ function truncateUtf8$1(input, maxBytes) {
74711
74720
  return `${input.slice(0, low)}${suffix}`;
74712
74721
  }
74713
74722
  //#endregion
74723
+ //#region ../../packages/agent-core/src/tools/builtin/python/python.ts
74724
+ const PY_DONE_MARKER = "__SCREAM_PY_DONE__";
74725
+ const DEFAULT_TIMEOUT_MS = 6e4;
74726
+ const MAX_TIMEOUT_MS = 5 * 6e4;
74727
+ const KERNEL_START_TIMEOUT_MS = 3e4;
74728
+ const PythonInputSchema = z.object({
74729
+ code: z.string().min(1).describe("Python code to execute in the persistent kernel. Variables, imports, and loaded data persist across calls, unlike Bash. State is kept for the whole session while the /rlm mode is enabled."),
74730
+ timeout: z.number().int().positive().max(MAX_TIMEOUT_MS / 1e3).optional().describe("Execution timeout in seconds (default 60, max 300).")
74731
+ });
74732
+ const RLM_BOOTSTRAP_PY = `import json, os, sys, tempfile, time, itertools, pickle
74733
+ _RLM_ID = itertools.count(1)
74734
+ _SNAP = __SNAP_PATH__
74735
+
74736
+ def _host_request(method, payload, timeout=120):
74737
+ rid = next(_RLM_ID)
74738
+ sys.stdout.write(json.dumps({"type": "host_request", "id": rid, "method": method, "payload": payload}) + "\\n")
74739
+ sys.stdout.flush()
74740
+ reply_file = os.path.join(tempfile.gettempdir(), "scream-rlm-" + str(os.getpid()) + "-" + str(rid) + ".json")
74741
+ deadline = time.time() + timeout
74742
+ while time.time() < deadline:
74743
+ if os.path.exists(reply_file):
74744
+ try:
74745
+ with open(reply_file, "r", encoding="utf-8") as f:
74746
+ reply = json.load(f)
74747
+ os.remove(reply_file)
74748
+ except Exception:
74749
+ time.sleep(0.1)
74750
+ continue
74751
+ if "error" in reply:
74752
+ raise RuntimeError(reply["error"])
74753
+ return reply.get("result")
74754
+ time.sleep(0.1)
74755
+ raise RuntimeError("host bridge timeout")
74756
+
74757
+ def rlm(task, name="subagent", **meta):
74758
+ payload = {"task": task, "name": name}
74759
+ payload.update(meta)
74760
+ return _host_request("rlm.run", payload)
74761
+
74762
+ def rlm_wait(handle, timeout=120):
74763
+ return _host_request("rlm.result", {"id": handle, "timeout": timeout}, timeout=timeout)
74764
+
74765
+ def _snapshot(path=_SNAP):
74766
+ saved = {}
74767
+ for k, v in list(globals().items()):
74768
+ if k.startswith("_") or callable(v) or isinstance(v, type(sys)):
74769
+ continue
74770
+ try:
74771
+ pickle.dumps(v)
74772
+ saved[k] = v
74773
+ except Exception:
74774
+ pass
74775
+ try:
74776
+ with open(path, "wb") as f:
74777
+ pickle.dump(saved, f)
74778
+ return len(saved)
74779
+ except Exception:
74780
+ return 0
74781
+
74782
+ def _restore(path=_SNAP):
74783
+ if not os.path.exists(path):
74784
+ return 0
74785
+ try:
74786
+ with open(path, "rb") as f:
74787
+ saved = pickle.load(f)
74788
+ except Exception:
74789
+ return 0
74790
+ for k, v in saved.items():
74791
+ globals()[k] = v
74792
+ return len(saved)
74793
+
74794
+ _restore()
74795
+ `;
74796
+ /** Builds the single-line bootstrap exec for this tool instance, embedding
74797
+ * the snapshot path so each PythonTool has its own persistent state file. */
74798
+ function buildRlmBootstrap(snapshotPath) {
74799
+ const py = RLM_BOOTSTRAP_PY.replace("__SNAP_PATH__", JSON.stringify(snapshotPath));
74800
+ return `exec(__import__('base64').b64decode('${Buffer.from(py).toString("base64")}').decode())\nprint("__SCREAM_BOOT_DONE__")`;
74801
+ }
74802
+ /**
74803
+ * Executes Python code in a persistent interactive kernel (`python3 -u -i`
74804
+ * over a pipe). The kernel process is lazily started on first use and lives
74805
+ * for the lifetime of this tool instance (the /rlm session), so variables,
74806
+ * imports, and loaded data survive across calls — unlike the stateless Bash
74807
+ * tool. Runs under the normal permission mode like any other tool; the code
74808
+ * can read/write files, so it is gated exactly like a mutating tool.
74809
+ */
74810
+ var PythonTool = class PythonTool {
74811
+ cwd;
74812
+ options;
74813
+ name = "python";
74814
+ description;
74815
+ parameters = toInputJsonSchema(PythonInputSchema);
74816
+ kernel;
74817
+ kernelBusy = false;
74818
+ /** Accumulated stderr from the kernel (tracebacks land here). */
74819
+ kernelStderr = "";
74820
+ /** Bytes of kernelStderr consumed by the last execution (race-safe drain). */
74821
+ kernelStderrOffset = 0;
74822
+ /** Per-instance snapshot file so RLM state survives kernel restarts. */
74823
+ snapshotPath;
74824
+ hostHandlers;
74825
+ constructor(cwd, options = {}) {
74826
+ this.cwd = cwd;
74827
+ this.options = options;
74828
+ this.hostHandlers = options.hostHandlers;
74829
+ this.snapshotPath = options.snapshotPath ?? join(tmpdir(), `scream-rlm-state-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}.pkl`);
74830
+ this.description = "Execute Python code in a persistent kernel. Variables, imports, and loaded data persist across calls (unlike Bash) — ideal for data analysis and multi-step processing. Run shell commands with the Bash tool instead. In RLM mode the kernel also provides `rlm(task, name=\"subagent\")` to spawn a subagent (returns a handle immediately) and `rlm_wait(handle, timeout)` to await its final summary. Multi-line code (def/for/if) is fully supported. Code runs under the current permission mode; mutating operations follow the same approval rules as other tools.";
74831
+ }
74832
+ dispose() {
74833
+ if (this.hostHandlers !== void 0) this.hostHandlers["__dispose__"]?.({}).catch(() => {});
74834
+ this.kernel?.kill("SIGKILL");
74835
+ this.kernel = void 0;
74836
+ this.kernelBusy = false;
74837
+ this.kernelStderr = "";
74838
+ this.kernelStderrOffset = 0;
74839
+ unlink(this.snapshotPath).catch(() => {});
74840
+ }
74841
+ /** Lazily starts the persistent kernel. The banner and REPL prompts are
74842
+ * emitted on stderr (not stdout), so we drain stderr continuously and drop
74843
+ * the startup banner; stdout only carries print() output. */
74844
+ async ensureKernel() {
74845
+ if (this.kernel !== void 0 && this.kernel.exitCode === null) return this.kernel;
74846
+ this.kernelBusy = false;
74847
+ this.kernelStderr = "";
74848
+ this.kernelStderrOffset = 0;
74849
+ const proc = await this.spawnKernel();
74850
+ this.kernel = proc;
74851
+ let spawnError;
74852
+ proc.on("error", (error) => {
74853
+ spawnError = error;
74854
+ });
74855
+ proc.stdin?.on("error", () => {});
74856
+ proc.stdout?.on("error", () => {});
74857
+ proc.stderr?.on("error", () => {});
74858
+ this.drainStderr(proc);
74859
+ if (this.hostHandlers !== void 0) {
74860
+ proc.stdin.write(`${buildRlmBootstrap(this.snapshotPath)}\n`);
74861
+ if (!(await this.readUntilMarker(proc.stdout, "__SCREAM_BOOT_DONE__", KERNEL_START_TIMEOUT_MS)).found) {
74862
+ proc.kill("SIGKILL");
74863
+ this.kernel = void 0;
74864
+ throw new Error(`Python kernel failed to start: ${spawnError?.message ?? "bootstrap did not complete"}`);
74865
+ }
74866
+ }
74867
+ return proc;
74868
+ }
74869
+ /** Candidate python commands, platform-first order. Exposed for tests:
74870
+ * POSIX prefers `python3`, Windows prefers `python`. Both fall back to the
74871
+ * other on ENOENT so a machine with either interpreter works. */
74872
+ static pythonCandidates(platform = process.platform) {
74873
+ return platform === "win32" ? ["python", "python3"] : ["python3", "python"];
74874
+ }
74875
+ /**
74876
+ * Spawns the kernel, resolving the python command per platform.
74877
+ *
74878
+ * The command name differs across platforms: POSIX ships `python3`; Windows
74879
+ * commonly exposes only `python` (python3 may or may not exist as an alias).
74880
+ * We prefer the platform-default name first and fall back to the other on
74881
+ * ENOENT, so a Windows install with either `python` or `python3` works.
74882
+ */
74883
+ async spawnKernel() {
74884
+ const candidates = PythonTool.pythonCandidates();
74885
+ let lastError;
74886
+ for (const command of candidates) try {
74887
+ return await this.trySpawn(command);
74888
+ } catch (error) {
74889
+ lastError = error;
74890
+ if (error.code !== "ENOENT") throw error;
74891
+ }
74892
+ throw lastError ?? /* @__PURE__ */ new Error(`No python interpreter found (tried: ${candidates.join(", ")})`);
74893
+ }
74894
+ trySpawn(command) {
74895
+ return new Promise((resolve, reject) => {
74896
+ const proc = spawn(command, ["-u", "-i"], {
74897
+ cwd: this.cwd,
74898
+ stdio: [
74899
+ "pipe",
74900
+ "pipe",
74901
+ "pipe"
74902
+ ]
74903
+ });
74904
+ proc.once("error", (error) => {
74905
+ reject(error);
74906
+ });
74907
+ proc.once("spawn", () => {
74908
+ proc.off("error", reject);
74909
+ resolve(proc);
74910
+ });
74911
+ });
74912
+ }
74913
+ drainStderr(proc) {
74914
+ const decoder = new StringDecoder("utf8");
74915
+ proc.stderr?.on("data", (chunk) => {
74916
+ this.kernelStderr += decoder.write(chunk);
74917
+ });
74918
+ }
74919
+ /**
74920
+ * Reads stdout until `marker` appears (or the deadline passes). Uses the
74921
+ * 'data' event instead of an async iterator: a `for await` that returns
74922
+ * early destroys the stream, which breaks the next call on the same
74923
+ * persistent kernel. `'data'` keeps the stream alive across calls.
74924
+ */
74925
+ async readUntilMarker(stream, marker, timeoutMs) {
74926
+ if (stream === null) return {
74927
+ lines: [],
74928
+ found: false
74929
+ };
74930
+ return new Promise((resolve) => {
74931
+ const lines = [];
74932
+ let buffer = "";
74933
+ const decoder = new StringDecoder("utf8");
74934
+ const cleanup = () => {
74935
+ clearTimeout(timer);
74936
+ stream.off("data", onData);
74937
+ stream.off("end", onEnd);
74938
+ stream.off("close", onEnd);
74939
+ stream.off("error", onEnd);
74940
+ };
74941
+ const finish = (found) => {
74942
+ cleanup();
74943
+ resolve({
74944
+ lines,
74945
+ found
74946
+ });
74947
+ };
74948
+ const onEnd = () => finish(false);
74949
+ const onData = (chunk) => {
74950
+ buffer += decoder.write(chunk);
74951
+ const parts = buffer.split("\n");
74952
+ buffer = parts.pop() ?? "";
74953
+ for (const line of parts) {
74954
+ if (line.includes(marker)) return finish(true);
74955
+ if (line.includes("\"host_request\"")) {
74956
+ this.handleHostRequest(line, stream).catch(() => {});
74957
+ continue;
74958
+ }
74959
+ lines.push(line);
74960
+ }
74961
+ if (buffer.includes(marker)) {
74962
+ lines.push(buffer);
74963
+ buffer = "";
74964
+ return finish(true);
74965
+ }
74966
+ };
74967
+ const timer = setTimeout(() => finish(false), timeoutMs);
74968
+ stream.on("data", onData);
74969
+ stream.on("end", onEnd);
74970
+ stream.on("close", onEnd);
74971
+ stream.on("error", onEnd);
74972
+ });
74973
+ }
74974
+ /**
74975
+ * Handles a kernel `host_request` line: invokes the registered handler and
74976
+ * writes the reply to the kernel's expected reply file (the kernel polls
74977
+ * that file — replies must not go over stdin, which is used for code input
74978
+ * and would race with the REPL). Fire-and-forget from the read loop.
74979
+ */
74980
+ /** Writes a bridge reply to the kernel's expected reply file. Uses the
74981
+ * pid captured when the request was received: if the kernel is restarted
74982
+ * (timeout kill) while a bridge handler is still running, the late reply
74983
+ * must still land in the file the originating kernel is polling. */
74984
+ writeHostReply(pid, id, body) {
74985
+ if (pid === void 0) return;
74986
+ writeFile(join(tmpdir(), `scream-rlm-${pid}-${id}.json`), JSON.stringify({
74987
+ type: "host_reply",
74988
+ id,
74989
+ ...body
74990
+ }), "utf8").catch(() => {});
74991
+ }
74992
+ async handleHostRequest(line, stream) {
74993
+ const replyPid = this.kernel?.pid;
74994
+ if (replyPid === void 0) return;
74995
+ let parsed;
74996
+ try {
74997
+ parsed = JSON.parse(line);
74998
+ } catch {
74999
+ return;
75000
+ }
75001
+ const { id, method, payload } = parsed;
75002
+ if (typeof id !== "number" || typeof method !== "string" || payload === void 0) return;
75003
+ const handler = this.hostHandlers?.[method];
75004
+ if (handler === void 0) {
75005
+ this.writeHostReply(replyPid, id, { error: `no handler for ${method}` });
75006
+ return;
75007
+ }
75008
+ try {
75009
+ const result = await handler(payload);
75010
+ this.writeHostReply(replyPid, id, { result });
75011
+ } catch (error) {
75012
+ this.writeHostReply(replyPid, id, { error: error instanceof Error ? error.message : String(error) });
75013
+ }
75014
+ }
75015
+ resolveExecution(args) {
75016
+ return {
75017
+ description: `Python: ${args.code.length > 50 ? `${args.code.slice(0, 50)}…` : args.code}`,
75018
+ display: {
75019
+ kind: "command",
75020
+ command: args.code,
75021
+ cwd: this.cwd
75022
+ },
75023
+ approvalRule: this.name,
75024
+ execute: (ctx) => this.execution(args, ctx)
75025
+ };
75026
+ }
75027
+ async execution(args, _ctx) {
75028
+ if (this.kernelBusy) return { output: "The Python kernel is busy executing a previous call. Wait for it to finish, or stop it with TaskStop." };
75029
+ this.kernelBusy = true;
75030
+ try {
75031
+ const proc = await this.ensureKernel();
75032
+ const timeoutMs = (args.timeout ?? DEFAULT_TIMEOUT_MS / 1e3) * 1e3;
75033
+ const wrapperPy = `import ast as _ast, base64 as _b
75034
+ __c = _b.b64decode('${Buffer.from(args.code, "utf8").toString("base64")}').decode()\ntry:
75035
+ try:
75036
+ __r = eval(__c, globals())
75037
+ if __r is not None:
75038
+ print(repr(__r))
75039
+ except SyntaxError:
75040
+ __tree = _ast.parse(__c)
75041
+ __tail = __tree.body[-1] if __tree.body else None
75042
+ if isinstance(__tail, _ast.Expr) and not any(
75043
+ isinstance(n, (_ast.ClassDef, _ast.FunctionDef, _ast.AsyncFunctionDef))
75044
+ for n in _ast.walk(__tail)):
75045
+ __lines = __c.split("\\n")
75046
+ __head = "\\n".join(__lines[: __tail.lineno - 1] + [__lines[__tail.lineno - 1][: __tail.col_offset]])
75047
+ if __head.strip():
75048
+ exec(__head, globals())
75049
+ __r = eval(_ast.unparse(__tail.value), globals())
75050
+ if __r is not None:
75051
+ print(repr(__r))
75052
+ else:
75053
+ exec(__c, globals())
75054
+ except BaseException as __e:
75055
+ # Show the user’s own source lines (the raw REPL traceback
75056
+ # only exposes the base64 wrapper line, which is useless for
75057
+ # debugging), then re-raise so the kernel prints the real stack
75058
+ # and the tool flags the call as an error.
75059
+ print("Traceback (most recent call last):")
75060
+ for __i, __ln in enumerate(__c.split("\\n"), 1):
75061
+ print(f" File \\"<user_code>\\", line {__i}")
75062
+ print(f" {__ln}")
75063
+ print(f"{type(__e).__name__}: {__e}")
75064
+ raise`;
75065
+ const wrapperB64 = Buffer.from(wrapperPy, "utf8").toString("base64");
75066
+ const codeWithDone = this.hostHandlers !== void 0 ? `exec(__import__('base64').b64decode('${wrapperB64}').decode(), globals())\nprint('${PY_DONE_MARKER}')\n_snapshot()\n` : `exec(__import__('base64').b64decode('${wrapperB64}').decode(), globals())\nprint('${PY_DONE_MARKER}')\n`;
75067
+ if (!proc.stdin.write(codeWithDone)) await new Promise((resolve) => {
75068
+ const onError = () => resolve();
75069
+ proc.stdin.once("close", onError);
75070
+ proc.stdin.once("error", onError);
75071
+ proc.stdin.once("drain", () => {
75072
+ proc.stdin.off("close", onError);
75073
+ proc.stdin.off("error", onError);
75074
+ resolve();
75075
+ });
75076
+ });
75077
+ const { lines, found } = await this.readUntilMarker(proc.stdout, PY_DONE_MARKER, timeoutMs);
75078
+ const output = lines.filter((line) => !line.includes(PY_DONE_MARKER) && !line.trimStart().startsWith(">>> ") && !line.trimStart().startsWith("... ")).join("\n").trim();
75079
+ await this.drainStderrQuiet(proc);
75080
+ const stderr = this.kernelStderr.slice(this.kernelStderrOffset).split("\n").filter((line) => !line.trimStart().startsWith(">>> ") && !line.trimStart().startsWith("... ") && !line.trimStart().startsWith("Python ") && !line.trimStart().startsWith("Type \"help\"")).join("\n").trim();
75081
+ this.kernelStderrOffset = this.kernelStderr.length;
75082
+ const merged = [output, stderr].filter((part) => part.length > 0).join("\n");
75083
+ if (!found) {
75084
+ const preInterruptOffset = this.kernelStderrOffset;
75085
+ if (process.platform === "win32" ? true : await this.interruptKernel(proc, 1500)) {
75086
+ proc.kill("SIGKILL");
75087
+ this.kernel = void 0;
75088
+ return {
75089
+ isError: true,
75090
+ output: `Python execution timed out after ${Math.round(timeoutMs / 1e3)}s (kernel restarted).\n${merged}`
75091
+ };
75092
+ }
75093
+ if (!await this.syncKernel(proc)) {
75094
+ proc.kill("SIGKILL");
75095
+ this.kernel = void 0;
75096
+ return {
75097
+ isError: true,
75098
+ output: `Python execution timed out after ${Math.round(timeoutMs / 1e3)}s (kernel restarted).\n${merged}`
75099
+ };
75100
+ }
75101
+ await this.drainStderrQuiet(proc);
75102
+ const interruptStderr = this.kernelStderr.slice(preInterruptOffset).split("\n").filter((line) => !line.trimStart().startsWith(">>> ") && !line.trimStart().startsWith("... ") && !line.trimStart().startsWith("Python ") && !line.trimStart().startsWith("Type \"help\"")).join("\n").trim();
75103
+ this.kernelStderrOffset = this.kernelStderr.length;
75104
+ const timedOutMerged = [merged, interruptStderr].filter((part) => part.length > 0).join("\n");
75105
+ return {
75106
+ isError: true,
75107
+ output: `Python execution timed out after ${Math.round(timeoutMs / 1e3)}s (kernel interrupted; state preserved).\n${timedOutMerged}`
75108
+ };
75109
+ }
75110
+ return {
75111
+ isError: merged.includes("Traceback") || merged.toLowerCase().includes("error:"),
75112
+ output: merged.length > 0 ? merged : "(no output)"
75113
+ };
75114
+ } finally {
75115
+ this.kernelBusy = false;
75116
+ }
75117
+ }
75118
+ /**
75119
+ * Sends SIGINT to the kernel and waits (up to `graceMs`) for it to exit.
75120
+ * Returns true when the process exited — meaning the kernel is gone and a
75121
+ * fresh one must be started; false when it is still alive after the signal,
75122
+ * which for `python3 -i` means the REPL interrupted the running statement
75123
+ * and is ready again with state intact.
75124
+ *
75125
+ * `proc.killed` is NOT used as an exit check: it flips true the first time
75126
+ * `kill()` is called, so on a second timeout it would falsely report the
75127
+ * process as exited. Only `exitCode`/`signalCode` reflect a real exit.
75128
+ */
75129
+ interruptKernel(proc, graceMs) {
75130
+ if (proc.exitCode !== null || proc.signalCode !== null) return Promise.resolve(true);
75131
+ return new Promise((resolve) => {
75132
+ const exited = () => resolve(true);
75133
+ const onExit = () => {
75134
+ clearTimeout(timer);
75135
+ exited();
75136
+ };
75137
+ const timer = setTimeout(() => {
75138
+ proc.off("exit", onExit);
75139
+ resolve(false);
75140
+ }, graceMs);
75141
+ proc.once("exit", onExit);
75142
+ proc.kill("SIGINT");
75143
+ });
75144
+ }
75145
+ /**
75146
+ * Round-trips a sync marker through the (interrupted) kernel: writes
75147
+ * `print('__SCREAM_SYNC__')` and waits for it to appear on stdout. Any
75148
+ * residual queued output (a DONE marker written before the timeout, a
75149
+ * partially-flushed traceback) is consumed by the same read loop, so the
75150
+ * next execution starts from a clean marker state. Returns true when the
75151
+ * marker came back — the REPL is idle and reusable.
75152
+ */
75153
+ async syncKernel(proc, timeoutMs = 3e3) {
75154
+ const SYNC_MARKER = "__SCREAM_SYNC__";
75155
+ if (!proc.stdin.write(`print('${SYNC_MARKER}')\n`)) await new Promise((resolve) => {
75156
+ const onError = () => resolve();
75157
+ proc.stdin.once("close", onError);
75158
+ proc.stdin.once("error", onError);
75159
+ proc.stdin.once("drain", () => {
75160
+ proc.stdin.off("close", onError);
75161
+ proc.stdin.off("error", onError);
75162
+ resolve();
75163
+ });
75164
+ });
75165
+ const { found } = await this.readUntilMarker(proc.stdout, SYNC_MARKER, timeoutMs);
75166
+ return found;
75167
+ }
75168
+ /** Waits (bounded) for stderr to go quiet so tracebacks settle before the
75169
+ * execution snapshot is committed. */
75170
+ async drainStderrQuiet(proc, quietMs = 40, capMs = 400) {
75171
+ const start = Date.now();
75172
+ let quietSince = Date.now();
75173
+ while (Date.now() - start < capMs) {
75174
+ const before = this.kernelStderr.length;
75175
+ await new Promise((resolve) => setTimeout(resolve, 10));
75176
+ if (this.kernelStderr.length === before) {
75177
+ if (Date.now() - quietSince >= quietMs) return;
75178
+ } else quietSince = Date.now();
75179
+ }
75180
+ }
75181
+ };
75182
+ //#endregion
74714
75183
  //#region ../../packages/agent-core/src/tools/builtin/shell/bash.md
74715
75184
  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";
74716
75185
  //#endregion
@@ -75926,7 +76395,7 @@ function stripContextMetadata(message) {
75926
76395
  }
75927
76396
  //#endregion
75928
76397
  //#region ../../packages/agent-core/src/agent/compaction/compaction-instruction.md
75929
- var compaction_instruction_default = "\n--- This message is a direct task, not part of the above conversation ---\n\nYou are now given a task to compact this conversation context according to specific priorities and output requirements.\n\nOutput text only. DO NOT CALL ANY TOOLS. Calling tools will be rejected and fails the task. You already have all the information you need in the conversation history. You have only one chance.\n\nThe goal of compaction is to keep essential code patterns, technical details, and architectural decisions for continuing development without losing context after the above messages are cleared work.\n\n{{ customInstruction }}\n\n<!-- Memory Memo Extraction (PRIORITY — do not skip) -->\n\n## 任务经验提取\n\nAFTER completing the compaction summary below, scan the messages being compacted for **completed task loops**. A task loop is \"completed\" when:\n- The user made a clear request or asked a specific question\n- You provided a solution or answer\n- The outcome is clear (success, partial success, or failure)\n\nFor each completed task loop found, output a structured experience record **at the very end of your response**:\n\n```memory-memo\n{\n \"userNeed\": \"<the user's need or goal, one sentence>\",\n \"approach\": \"<what was done — the approach taken, 2-4 sentences>\",\n \"outcome\": \"<final result, e.g. '完成', '部分完成', '失败: reason'>\",\n \"whatFailed\": \"<dead ends tried — things that didn't work, or 'none'>\",\n \"whatWorked\": \"<key actions that ultimately worked, or 'none'>\",\n \"tags\": [\"<tag1>\", \"<tag2>\", \"<tag3>\"]\n}\n```\n\nGuidelines:\n- Record important failed attempts in \"whatFailed\" to help avoid repeating mistakes.\n- Record key successful actions in \"whatWorked\" to help reuse effective approaches.\n- Include 3-5 semantic \"tags\" summarizing the task domain, tech stack, or action type (e.g. [\"react\", \"auth\", \"部署\"]).\n- Skip in-progress work unless it contains a valuable error+fix experience.\n- Merge closely related sub-tasks into a single record.\n- Use the exact field names and JSON format shown above.\n\nIf no completed task loops are found in the compacted messages, output:\n```memory-memo\n{\"none\": true}\n```\n\n<!-- Compression Priorities (in order) -->\n\n1. **Current Task State**: What is being worked on RIGHT NOW\n2. **Errors & Solutions**: All encountered errors and their resolutions\n3. **Code Evolution**: Final working versions only (remove intermediate attempts)\n4. **System Context**: Project structure, dependencies, environment setup\n5. **Design Decisions**: Architectural choices and their rationale\n6. **TODO Items**: Unfinished tasks and known issues\n\n<!-- Required Output Structure -->\n\n## Current Focus\n\n[What we're working on now]\n\n## Environment\n\n- [Key setup/config points]\n- ...\n\n## Completed Tasks\n\n- [Task]: [Brief outcome]\n- ...\n\n## Active Issues\n\n- [Issue]: [Status/Next steps]\n- ...\n\n## Code State\n\n### [Critical file name]\n\n[Brief description of the file's purpose and current state]\n\n```\n[The latest version of critical code snippets in this file, <20 lines]\n```\n\n### [Critical file name]\n\n- [Useful classes/methods/functions]: [Brief description/usage]\n- ...\n\n<!-- Omit non-critical code, intermediate attempts, and resolved errors -->\n\n## Important Context\n\n- [Any crucial information not covered above]\n- ...\n\n## All User Messages\n\n- [Detailed non tool use user message]\n- ...\n";
76398
+ var compaction_instruction_default = "\n--- This message is a direct task, not part of the above conversation ---\n\nYou are now given a task to compact this conversation context according to specific priorities and output requirements.\n\nOutput text only. DO NOT CALL ANY TOOLS. Calling tools will be rejected and fails the task. You already have all the information you need in the conversation history. You have only one chance.\n\nThe goal of compaction is to keep essential code patterns, technical details, and architectural decisions for continuing development without losing context after the above messages are cleared work.\n\n{{ customInstruction }}\n\n<!-- Memory Memo Extraction (PRIORITY — do not skip) -->\n\n## 任务经验提取\n\nAFTER completing the compaction summary below, scan the messages being compacted for **completed task loops**. A task loop is \"completed\" when:\n- The user made a clear request or asked a specific question\n- You provided a solution or answer\n- The outcome is clear (success, partial success, or failure)\n\nFor each completed task loop found, output a structured experience record **at the very end of your response**:\n\n```memory-memo\n{\n \"userNeed\": \"<the user's need or goal, one sentence>\",\n \"approach\": \"<what was done — the approach taken, 2-4 sentences>\",\n \"outcome\": \"<final result, e.g. '完成', '部分完成', '失败: reason'>\",\n \"whatFailed\": \"<dead ends tried — things that didn't work, or 'none'>\",\n \"whatWorked\": \"<key actions that ultimately worked, or 'none'>\",\n \"tags\": [\"<tag1>\", \"<tag2>\", \"<tag3>\"],\n \"note\": \"<optional: a soft, AI-readable note/suggestion that makes the record easier for future turns to understand — e.g. '这类任务先查接口可用性再写代码'. Omit if none>\"\n}\n```\n\nGuidelines:\n- Record important failed attempts in \"whatFailed\" to help avoid repeating mistakes.\n- Record key successful actions in \"whatWorked\" to help reuse effective approaches.\n- Include 3-5 semantic \"tags\" summarizing the task domain, tech stack, or action type (e.g. [\"react\", \"auth\", \"部署\"]).\n- \"note\" is optional: a one-sentence advisory note that helps future AI understand/reuse the record faster. It is a soft suggestion, not a user-enforced rule.\n- Skip in-progress work unless it contains a valuable error+fix experience.\n- Merge closely related sub-tasks into a single record.\n- Use the exact field names and JSON format shown above (no extra fields beyond \"note\").\n\nIf no completed task loops are found in the compacted messages, output:\n```memory-memo\n{\"none\": true}\n```\n\n<!-- Compression Priorities (in order) -->\n\n1. **Current Task State**: What is being worked on RIGHT NOW\n2. **Errors & Solutions**: All encountered errors and their resolutions\n3. **Code Evolution**: Final working versions only (remove intermediate attempts)\n4. **System Context**: Project structure, dependencies, environment setup\n5. **Design Decisions**: Architectural choices and their rationale\n6. **TODO Items**: Unfinished tasks and known issues\n\n<!-- Required Output Structure -->\n\n## Current Focus\n\n[What we're working on now]\n\n## Environment\n\n- [Key setup/config points]\n- ...\n\n## Completed Tasks\n\n- [Task]: [Brief outcome]\n- ...\n\n## Active Issues\n\n- [Issue]: [Status/Next steps]\n- ...\n\n## Code State\n\n### [Critical file name]\n\n[Brief description of the file's purpose and current state]\n\n```\n[The latest version of critical code snippets in this file, <20 lines]\n```\n\n### [Critical file name]\n\n- [Useful classes/methods/functions]: [Brief description/usage]\n- ...\n\n<!-- Omit non-critical code, intermediate attempts, and resolved errors -->\n\n## Important Context\n\n- [Any crucial information not covered above]\n- ...\n\n## All User Messages\n\n- [Detailed non tool use user message]\n- ...\n\n## Skill candidates\n\nScan the compressed messages for reusable processes (project-specific build\nsteps, recurring debugging patterns, or tool workflows). If any is genuinely\nworth capturing as a reusable skill, output at the very end, at most one:\n\n[[skill-candidate: <name>|<one-line purpose>|<evidence>]]\n\nOmit if none.\n";
75930
76399
  //#endregion
75931
76400
  //#region ../../packages/agent-core/src/agent/compaction/compaction-update-instruction.md
75932
76401
  var compaction_update_instruction_default = "\n--- This message is a direct task, not part of the above conversation ---\n\nYou are now given a task to UPDATE an existing compaction summary with new messages that came in after the last compaction.\n\nA previous compaction summary already exists at the top of the conversation (the first assistant message). You must NOT discard it. Instead, merge the new information into the existing structure, keeping all previously captured state intact unless explicitly contradicted or resolved by the new messages.\n\nOutput text only. DO NOT CALL ANY TOOLS. Calling tools will be rejected and fails the task. You have only one chance.\n\n{{ customInstruction }}\n\n<!-- Previous Summary Reference -->\n\nThe first assistant message above is the PREVIOUS summary. Treat it as the source of truth for everything that happened before the new messages. Do not restate it verbatim — produce an updated, merged summary.\n\n<!-- Memory Memo Extraction (PRIORITY — do not skip) -->\n\n## 任务经验提取\n\nAFTER completing the updated summary below, scan ONLY the new messages being compacted (not those already covered by the previous summary) for **completed task loops**. A task loop is \"completed\" when:\n- The user made a clear request or asked a specific question\n- You provided a solution or answer\n- The outcome is clear (success, partial success, or failure)\n\nFor each completed task loop found, output a structured experience record **at the very end of your response**:\n\n```memory-memo\n{\n \"userNeed\": \"<the user's need or goal, one sentence>\",\n \"approach\": \"<what was done — the approach taken, 2-4 sentences>\",\n \"outcome\": \"<final result, e.g. '完成', '部分完成', '失败: reason'>\",\n \"whatFailed\": \"<dead ends tried — things that didn't work, or 'none'>\",\n \"whatWorked\": \"<key actions that ultimately worked, or 'none'>\",\n \"tags\": [\"<tag1>\", \"<tag2>\", \"<tag3>\"]\n}\n```\n\nGuidelines:\n- Record important failed attempts in \"whatFailed\" to help avoid repeating mistakes.\n- Record key successful actions in \"whatWorked\" to help reuse effective approaches.\n- Include 3-5 semantic \"tags\" summarizing the task domain, tech stack, or action type.\n- Skip in-progress work unless it contains a valuable error+fix experience.\n- Merge closely related sub-tasks into a single record.\n- Use the exact field names and JSON format shown above.\n\nIf no completed task loops are found in the new compacted messages, output:\n```memory-memo\n{\"none\": true}\n```\n\n<!-- Update Rules -->\n\n1. Preserve the section structure of the previous summary (Current Focus, Environment, Completed Tasks, Active Issues, Code State, Important Context, All User Messages).\n2. Move newly-completed tasks from \"Current Focus\" / \"Active Issues\" into \"Completed Tasks\".\n3. Update \"Current Focus\" to reflect what is being worked on RIGHT NOW.\n4. Append new user messages to \"All User Messages\" — do not repeat those already captured.\n5. Refresh \"Code State\" code snippets only if newer versions exist in the new messages.\n6. Drop resolved issues from \"Active Issues\"; add newly-discovered ones.\n7. Do not invent new information. If the new messages say nothing about a section, carry the previous content forward unchanged.\n\n<!-- Required Output Structure -->\n\n## Current Focus\n\n[What we're working on now]\n\n## Environment\n\n- [Key setup/config points]\n- ...\n\n## Completed Tasks\n\n- [Task]: [Brief outcome]\n- ...\n\n## Active Issues\n\n- [Issue]: [Status/Next steps]\n- ...\n\n## Code State\n\n### [Critical file name]\n\n[Brief description of the file's purpose and current state]\n\n```\n[The latest version of critical code snippets in this file, <20 lines]\n```\n\n## Important Context\n\n- [Any crucial information not covered above]\n- ...\n\n## All User Messages\n\n- [Detailed non tool use user message]\n- ...\n";
@@ -76551,6 +77020,7 @@ var FullCompaction = class {
76551
77020
  this.lowWaterMark = Math.floor(this.effectiveTokenCount * 1.1);
76552
77021
  await this.extractAndStoreMemos(processedSummary);
76553
77022
  this.triggerPostCompactHook(data, result);
77023
+ this.detectSkillCandidates(processedSummary);
76554
77024
  this.consecutiveCompactionFailures = 0;
76555
77025
  this._shouldInjectSessionSummary = true;
76556
77026
  } catch (error) {
@@ -76641,6 +77111,33 @@ var FullCompaction = class {
76641
77111
  });
76642
77112
  }
76643
77113
  /**
77114
+ * Detects [[skill-candidate: <name>|<purpose>|<evidence>]] markers in the
77115
+ * compaction summary and emits a `skill_candidate` event for each one (at
77116
+ * most one per compact). Best-effort and fully isolated: a parse failure or
77117
+ * a missing marker never affects compaction or memory extraction — this
77118
+ * method only reads the summary and emits an event.
77119
+ */
77120
+ detectSkillCandidates(summary) {
77121
+ try {
77122
+ const matches = summary.matchAll(/\[\[skill-candidate:\s*([^|\]]+)\|([^|\]]+)\|([^|\]]+)\]\]/g);
77123
+ for (const m of matches) {
77124
+ const name = m[1].trim();
77125
+ if (name.length === 0) continue;
77126
+ this.agent.emitEvent({
77127
+ type: "skill_candidate",
77128
+ candidate: {
77129
+ name,
77130
+ purpose: m[2].trim(),
77131
+ evidence: m[3].trim()
77132
+ }
77133
+ });
77134
+ break;
77135
+ }
77136
+ } catch (error) {
77137
+ this.agent.log.warn("Skill candidate detection failed", { error: String(error) });
77138
+ }
77139
+ }
77140
+ /**
76644
77141
  * Append the current todo list and file operations as markdown sections to
76645
77142
  * the compaction summary so active tasks and file context survive
76646
77143
  * compression. Without this, both are lost after compaction because the
@@ -82111,6 +82608,12 @@ function restoreAgentRecord(agent, input) {
82111
82608
  case "wolfpack.exit":
82112
82609
  agent.wolfpackMode.exit();
82113
82610
  return;
82611
+ case "rlm.enter":
82612
+ agent.restoreRlm(true);
82613
+ return;
82614
+ case "rlm.exit":
82615
+ agent.restoreRlm(false);
82616
+ return;
82114
82617
  case "goal.create":
82115
82618
  agent.goal.restoreCreate(input);
82116
82619
  return;
@@ -95160,6 +95663,70 @@ function createGoalGrader(agent) {
95160
95663
  };
95161
95664
  };
95162
95665
  }
95666
+ /**
95667
+ * Host bridge handlers for the /rlm python kernel: `rlm.run` spawns a
95668
+ * subagent (reusing the subagent host) and returns its handle id;
95669
+ * `rlm.result` waits for that subagent's final summary. Handles are kept in
95670
+ * a closure map for the lifetime of the ToolManager (one per session).
95671
+ *
95672
+ * Recursion guard: a subagent may only spawn its own rlm() children if its
95673
+ * depth is below the cap. Depth is carried on each agent instance (root = 0,
95674
+ * every spawned subagent = parent + 1). The cap is per-agent
95675
+ * (setRlmMaxDepth / /rlm-max-depth); it is `Infinity` by default — unlimited
95676
+ * recursion — and a positive integer when the user opts into a limit.
95677
+ */
95678
+ function createRlmHostHandlers(agent) {
95679
+ const handles = /* @__PURE__ */ new Map();
95680
+ return {
95681
+ "rlm.run": async (payload) => {
95682
+ const host = agent.subagentHost;
95683
+ if (host === void 0) throw new Error("subagent host unavailable");
95684
+ if (agent.getRlmDepth() >= agent.getRlmMaxDepth()) throw new Error(`RLM recursion depth limit reached (depth ${agent.getRlmDepth()}, max ${agent.getRlmMaxDepth()}). Nested rlm() subagents are not allowed.`);
95685
+ const task = String(payload["task"] ?? "");
95686
+ const name = String(payload["name"] ?? "subagent").slice(0, 64);
95687
+ const controller = new AbortController();
95688
+ const handle = await host.spawn("coder", {
95689
+ parentToolCallId: `rlm_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
95690
+ prompt: task,
95691
+ description: `rlm subagent: ${name}`,
95692
+ runInBackground: false,
95693
+ signal: controller.signal
95694
+ });
95695
+ const entry = {
95696
+ completion: handle.completion,
95697
+ name,
95698
+ controller
95699
+ };
95700
+ handles.set(handle.agentId, entry);
95701
+ handle.completion.then((result) => {
95702
+ entry.result = result;
95703
+ }, () => {
95704
+ entry.result = void 0;
95705
+ });
95706
+ return {
95707
+ id: handle.agentId,
95708
+ name
95709
+ };
95710
+ },
95711
+ "rlm.result": async (payload) => {
95712
+ const id = String(payload["id"]);
95713
+ const entry = handles.get(id);
95714
+ if (entry === void 0) throw new Error(`unknown rlm handle: ${id}`);
95715
+ if (entry.result !== void 0) {
95716
+ handles.delete(id);
95717
+ return { result: entry.result };
95718
+ }
95719
+ const result = (await entry.completion).result;
95720
+ handles.delete(id);
95721
+ return { result };
95722
+ },
95723
+ __dispose__: async () => {
95724
+ for (const { controller } of handles.values()) controller.abort(/* @__PURE__ */ new Error("rlm kernel disposed"));
95725
+ handles.clear();
95726
+ return {};
95727
+ }
95728
+ };
95729
+ }
95163
95730
  var ToolManager = class {
95164
95731
  agent;
95165
95732
  builtinTools = /* @__PURE__ */ new Map();
@@ -95417,6 +95984,12 @@ var ToolManager = class {
95417
95984
  } })
95418
95985
  });
95419
95986
  }
95987
+ getActiveTools() {
95988
+ return [...this.enabledTools];
95989
+ }
95990
+ getBuiltinTool(name) {
95991
+ return this.builtinTools.get(name);
95992
+ }
95420
95993
  setActiveTools(names) {
95421
95994
  this.agent.records.logRecord({
95422
95995
  type: "tools.set_active_tools",
@@ -95477,6 +96050,7 @@ var ToolManager = class {
95477
96050
  allowBackground,
95478
96051
  availableTools: this.enabledTools
95479
96052
  }),
96053
+ new PythonTool(cwd, { hostHandlers: createRlmHostHandlers(this.agent) }),
95480
96054
  (modelCapabilities.image_in || modelCapabilities.video_in) && new ReadMediaFileTool(jian, workspace, modelCapabilities, videoUploader),
95481
96055
  new EnterPlanModeTool(this.agent),
95482
96056
  new ExitPlanModeTool(this.agent),
@@ -97095,6 +97669,96 @@ var Agent = class {
97095
97669
  }
97096
97670
  })();
97097
97671
  }
97672
+ rlmEnabled = false;
97673
+ /** RLM recursion depth: 0 for the root agent, +1 per spawned subagent. */
97674
+ rlmDepth = 0;
97675
+ /** Maximum allowed RLM recursion depth. `Infinity` (the default) means
97676
+ * unlimited recursion. Set to a positive integer via /rlm-max-depth to
97677
+ * cap the chain (e.g. 1 = root may spawn children, children may not spawn
97678
+ * grandchildren); 0 is treated as unlimited as well. */
97679
+ rlmMaxDepth = Infinity;
97680
+ getRlmDepth() {
97681
+ return this.rlmDepth;
97682
+ }
97683
+ setRlmDepth(depth) {
97684
+ this.rlmDepth = Math.max(0, depth);
97685
+ }
97686
+ getRlmMaxDepth() {
97687
+ return this.rlmMaxDepth;
97688
+ }
97689
+ setRlmMaxDepth(maxDepth) {
97690
+ if (!Number.isFinite(maxDepth) || maxDepth <= 0) {
97691
+ this.rlmMaxDepth = Infinity;
97692
+ return;
97693
+ }
97694
+ this.rlmMaxDepth = Math.trunc(maxDepth);
97695
+ }
97696
+ /** Disposes the persistent python kernel (if any) and resets RLM mode.
97697
+ * Called on session close so no orphaned kernel process is left behind.
97698
+ * Only touches state when RLM was actually enabled — a session that never
97699
+ * used /rlm must not emit a spurious setActiveTools record (which would
97700
+ * pollute the wire log and, on a subagent that does not re-apply a profile
97701
+ * on resume, could strip MCP access patterns). */
97702
+ disposeRlm() {
97703
+ if (!this.rlmEnabled) return;
97704
+ this.rlmEnabled = false;
97705
+ const withoutPython = this.tools.getActiveTools().filter((name) => name !== "python");
97706
+ this.tools.setActiveTools(withoutPython);
97707
+ this.tools.getBuiltinTool("python")?.dispose?.();
97708
+ this.records.logRecord({ type: "rlm.exit" });
97709
+ }
97710
+ /**
97711
+ * Enables or disables the /rlm persistent-python mode. On enable, the
97712
+ * `python` tool is added to the active tools; on disable it is removed and
97713
+ * its kernel is disposed. Default state (disabled) leaves the tool list and
97714
+ * default behaviour untouched.
97715
+ */
97716
+ setRlmEnabled(enabled) {
97717
+ this.rlmEnabled = enabled;
97718
+ const current = this.tools.getActiveTools();
97719
+ if (enabled) {
97720
+ if (!current.includes("python")) this.tools.setActiveTools([...current, "python"]);
97721
+ } else {
97722
+ const withoutPython = current.filter((name) => name !== "python");
97723
+ if (withoutPython.length !== current.length) this.tools.setActiveTools(withoutPython);
97724
+ this.tools.getBuiltinTool("python")?.dispose?.();
97725
+ }
97726
+ this.records.logRecord({ type: enabled ? "rlm.enter" : "rlm.exit" });
97727
+ this.emitStatusUpdated();
97728
+ }
97729
+ /** Returns whether RLM (persistent python mode) is currently enabled. */
97730
+ getRlmEnabled() {
97731
+ return this.rlmEnabled;
97732
+ }
97733
+ /**
97734
+ * Lets a subagent inherit RLM mode from its parent at spawn time: mounts
97735
+ * the python tool (so the child can keep recursing) and records rlm.enter,
97736
+ * mirroring setRlmEnabled(true). The rlm.enter record keeps enter/exit
97737
+ * symmetric: the child's disposeRlm (triggered by session close) emits
97738
+ * rlm.exit, so a subagent's records must contain the matching enter or the
97739
+ * pair is unbalanced. A status event is NOT emitted here — the child's own
97740
+ * turn is about to start and the parent's record already represents this
97741
+ * recursion chain. Non-RLM parents never call this.
97742
+ */
97743
+ inheritRlm() {
97744
+ this.rlmEnabled = true;
97745
+ const current = this.tools.getActiveTools();
97746
+ if (!current.includes("python")) this.tools.setActiveTools([...current, "python"]);
97747
+ this.records.logRecord({ type: "rlm.enter" });
97748
+ }
97749
+ /** Restores RLM mode from a persisted record during replay. Does not log
97750
+ * a new record and does not emit a status update (both are suppressed while
97751
+ * records are restoring). */
97752
+ restoreRlm(enabled) {
97753
+ this.rlmEnabled = enabled;
97754
+ const current = this.tools.getActiveTools();
97755
+ if (enabled) {
97756
+ if (!current.includes("python")) this.tools.setActiveTools([...current, "python"]);
97757
+ } else {
97758
+ const withoutPython = current.filter((name) => name !== "python");
97759
+ if (withoutPython.length !== current.length) this.tools.setActiveTools(withoutPython);
97760
+ }
97761
+ }
97098
97762
  initKnowledgeStore(store) {
97099
97763
  if (store === void 0) return Promise.resolve();
97100
97764
  return (async () => {
@@ -97262,6 +97926,12 @@ var Agent = class {
97262
97926
  setActiveTools: (payload) => {
97263
97927
  this.tools.setActiveTools(payload.names);
97264
97928
  },
97929
+ setRlmEnabled: (payload) => {
97930
+ this.setRlmEnabled(payload.enabled);
97931
+ },
97932
+ setRlmMaxDepth: (payload) => {
97933
+ this.setRlmMaxDepth(payload.maxDepth);
97934
+ },
97265
97935
  stopBackground: (payload) => {
97266
97936
  this.background.stop(payload.taskId, payload.reason);
97267
97937
  },
@@ -97335,6 +98005,9 @@ var Agent = class {
97335
98005
  },
97336
98006
  getWolfpackMode: () => {
97337
98007
  return this.wolfpackMode.isActive;
98008
+ },
98009
+ getRlmEnabled: () => {
98010
+ return this.rlmEnabled;
97338
98011
  }
97339
98012
  };
97340
98013
  }
@@ -97459,6 +98132,7 @@ var Agent = class {
97459
98132
  planMode: this.planMode.isActive,
97460
98133
  planStrategy: this.planMode.isActive ? this.planMode.strategy : void 0,
97461
98134
  wolfpackMode: this.wolfpackMode.isActive,
98135
+ rlmEnabled: this.rlmEnabled,
97462
98136
  permission: this.permission.mode,
97463
98137
  usage
97464
98138
  });
@@ -101779,6 +102453,8 @@ var SessionSubagentHost = class {
101779
102453
  type: "sub",
101780
102454
  generate: parent.rawGenerate
101781
102455
  }, void 0, this.ownerAgentId);
102456
+ agent.setRlmDepth(parent.getRlmDepth() + 1);
102457
+ agent.setRlmMaxDepth(parent.getRlmMaxDepth());
101782
102458
  const controller = new AbortController();
101783
102459
  const unlinkAbortSignal = linkAbortSignal(options.signal, controller);
101784
102460
  this.activeChildren.set(id, {
@@ -101946,6 +102622,7 @@ var SessionSubagentHost = class {
101946
102622
  });
101947
102623
  const context = await prepareSystemPromptContext(child.jian);
101948
102624
  child.useProfile(profile, context);
102625
+ if (parent.getRlmEnabled()) child.inheritRlm();
101949
102626
  }
101950
102627
  resolveModelBinding(profileName) {
101951
102628
  const bindings = this.modelBindings?.();
@@ -102127,6 +102804,7 @@ var Session$1 = class {
102127
102804
  try {
102128
102805
  for (const agent of this.agents.values()) if (agent.turn.hasActiveTurn) agent.turn.cancel();
102129
102806
  await Promise.allSettled(Array.from(this.agents.values(), async (agent) => agent.cron?.stop()));
102807
+ for (const agent of this.agents.values()) agent.disposeRlm?.();
102130
102808
  await this.stopBackgroundTasksOnExit();
102131
102809
  await this.flushMetadata();
102132
102810
  await this.triggerSessionEnd("exit");
@@ -117314,6 +117992,12 @@ var SessionAPIImpl = class {
117314
117992
  setActiveTools({ agentId, ...payload }) {
117315
117993
  return this.getAgent(agentId).setActiveTools(payload);
117316
117994
  }
117995
+ setRlmEnabled({ agentId, ...payload }) {
117996
+ return this.getAgent(agentId).setRlmEnabled(payload);
117997
+ }
117998
+ setRlmMaxDepth({ agentId, ...payload }) {
117999
+ return this.getAgent(agentId).setRlmMaxDepth(payload);
118000
+ }
117317
118001
  stopBackground({ agentId, ...payload }) {
117318
118002
  return this.getAgent(agentId).stopBackground(payload);
117319
118003
  }
@@ -117387,6 +118071,9 @@ var SessionAPIImpl = class {
117387
118071
  getWolfpackMode({ agentId }) {
117388
118072
  return this.getAgent(agentId).getWolfpackMode({});
117389
118073
  }
118074
+ getRlmEnabled({ agentId }) {
118075
+ return this.getAgent(agentId).getRlmEnabled({});
118076
+ }
117390
118077
  getAgent(agentId) {
117391
118078
  const agent = this.session.agents.get(agentId);
117392
118079
  if (agent === void 0) throw new ScreamError(ErrorCodes.AGENT_NOT_FOUND, `Agent "${agentId}" was not found`);
@@ -119063,6 +119750,12 @@ var ScreamCore = class {
119063
119750
  setActiveTools({ sessionId, ...payload }) {
119064
119751
  return this.sessionApi(sessionId).setActiveTools(payload);
119065
119752
  }
119753
+ setRlmEnabled({ sessionId, ...payload }) {
119754
+ return this.sessionApi(sessionId).setRlmEnabled(payload);
119755
+ }
119756
+ setRlmMaxDepth({ sessionId, ...payload }) {
119757
+ return this.sessionApi(sessionId).setRlmMaxDepth(payload);
119758
+ }
119066
119759
  stopBackground({ sessionId, ...payload }) {
119067
119760
  return this.sessionApi(sessionId).stopBackground(payload);
119068
119761
  }
@@ -119135,6 +119828,9 @@ var ScreamCore = class {
119135
119828
  getWolfpackMode({ sessionId, ...payload }) {
119136
119829
  return this.sessionApi(sessionId).getWolfpackMode(payload);
119137
119830
  }
119831
+ getRlmEnabled({ sessionId, ...payload }) {
119832
+ return this.sessionApi(sessionId).getRlmEnabled(payload);
119833
+ }
119138
119834
  updateSessionMetadata({ sessionId, ...payload }) {
119139
119835
  return this.sessionApi(sessionId).updateSessionMetadata(payload);
119140
119836
  }
@@ -119773,7 +120469,7 @@ var SDKRpcClient = class {
119773
120469
  async getStatus(input) {
119774
120470
  const rpc = await this.getRpc();
119775
120471
  const agentId = this.interactiveAgentId;
119776
- const [config, context, permission, plan, usage, wolfpackMode] = await Promise.all([
120472
+ const [config, context, permission, plan, usage, wolfpackMode, rlmEnabled] = await Promise.all([
119777
120473
  rpc.getConfig({
119778
120474
  sessionId: input.sessionId,
119779
120475
  agentId
@@ -119797,6 +120493,10 @@ var SDKRpcClient = class {
119797
120493
  rpc.getWolfpackMode({
119798
120494
  sessionId: input.sessionId,
119799
120495
  agentId
120496
+ }),
120497
+ rpc.getRlmEnabled({
120498
+ sessionId: input.sessionId,
120499
+ agentId
119800
120500
  })
119801
120501
  ]);
119802
120502
  const maxContextTokens = config.modelCapabilities?.max_context_tokens ?? 0;
@@ -119810,6 +120510,7 @@ var SDKRpcClient = class {
119810
120510
  planMode: plan !== null,
119811
120511
  planStrategy: plan?.strategy,
119812
120512
  wolfpackMode,
120513
+ rlmEnabled,
119813
120514
  contextTokens,
119814
120515
  maxContextTokens,
119815
120516
  contextUsage,
@@ -119854,6 +120555,20 @@ var SDKRpcClient = class {
119854
120555
  taskId: input.taskId
119855
120556
  });
119856
120557
  }
120558
+ async setRlmEnabled(input) {
120559
+ return (await this.getRpc()).setRlmEnabled({
120560
+ sessionId: input.sessionId,
120561
+ agentId: this.interactiveAgentId,
120562
+ enabled: input.enabled
120563
+ });
120564
+ }
120565
+ async setRlmMaxDepth(input) {
120566
+ return (await this.getRpc()).setRlmMaxDepth({
120567
+ sessionId: input.sessionId,
120568
+ agentId: this.interactiveAgentId,
120569
+ maxDepth: input.maxDepth
120570
+ });
120571
+ }
119857
120572
  async stopBackgroundTask(input) {
119858
120573
  return (await this.getRpc()).stopBackground({
119859
120574
  sessionId: input.sessionId,
@@ -120279,6 +120994,27 @@ var Session = class {
120279
120994
  * for unknown or already-terminal task ids are no-ops at the core
120280
120995
  * level — this method does not throw in those cases.
120281
120996
  */
120997
+ /**
120998
+ * Enables or disables the /rlm persistent-python mode. When enabled, the
120999
+ * `python` tool (persistent kernel) is added to the active tools; when
121000
+ * disabled it is removed and its kernel is disposed. Default is disabled.
121001
+ */
121002
+ async setRlmEnabled(enabled) {
121003
+ this.ensureOpen();
121004
+ await this.rpc.setRlmEnabled({
121005
+ sessionId: this.id,
121006
+ enabled
121007
+ });
121008
+ }
121009
+ /** Sets the maximum RLM recursion depth (default 1). Depth 1 means the root
121010
+ * kernel may spawn rlm() children but they cannot spawn grandchildren. */
121011
+ async setRlmMaxDepth(maxDepth) {
121012
+ this.ensureOpen();
121013
+ await this.rpc.setRlmMaxDepth({
121014
+ sessionId: this.id,
121015
+ maxDepth
121016
+ });
121017
+ }
120282
121018
  async stopBackgroundTask(taskId, options = {}) {
120283
121019
  this.ensureOpen();
120284
121020
  const trimmedTaskId = normalizeRequiredString(taskId, "Task id cannot be empty", ErrorCodes.BACKGROUND_TASK_ID_EMPTY);
@@ -120898,7 +121634,7 @@ function optionalBuildString(value) {
120898
121634
  return typeof value === "string" && value.length > 0 ? value : void 0;
120899
121635
  }
120900
121636
  const SCREAM_BUILD_INFO = {
120901
- version: optionalBuildString("0.12.4"),
121637
+ version: optionalBuildString("0.12.6"),
120902
121638
  channel: optionalBuildString(""),
120903
121639
  commit: optionalBuildString(""),
120904
121640
  buildTarget: optionalBuildString("darwin-arm64")
@@ -121899,28 +122635,43 @@ const BUILTIN_SLASH_COMMANDS = [
121899
122635
  name: "ask",
121900
122636
  aliases: ["ask"],
121901
122637
  description: "registry.ask_desc",
121902
- priority: 122,
122638
+ priority: 123,
122639
+ availability: "always"
122640
+ },
122641
+ {
122642
+ name: "rlm",
122643
+ aliases: ["rlm"],
122644
+ description: "registry.rlm_desc",
122645
+ priority: 121,
122646
+ availability: "always"
122647
+ },
122648
+ {
122649
+ name: "rlm-max-depth",
122650
+ aliases: [],
122651
+ description: "registry.rlm_max_depth_desc",
122652
+ argumentHint: "[N]",
122653
+ priority: 119,
121903
122654
  availability: "always"
121904
122655
  },
121905
122656
  {
121906
122657
  name: "wolfpack",
121907
122658
  aliases: ["wp"],
121908
122659
  description: "registry.wolfpack_desc",
121909
- priority: 123,
122660
+ priority: 122,
121910
122661
  availability: "always"
121911
122662
  },
121912
122663
  {
121913
122664
  name: "sessions",
121914
122665
  aliases: ["resume"],
121915
122666
  description: "registry.sessions_desc",
121916
- priority: 122
122667
+ priority: 119
121917
122668
  },
121918
122669
  {
121919
122670
  name: "goal",
121920
122671
  aliases: ["goaloff"],
121921
122672
  description: "registry.goal_desc",
121922
122673
  argumentHint: "[objective]",
121923
- priority: 122,
122674
+ priority: 120,
121924
122675
  availability: (args) => {
121925
122676
  const trimmed = args.trim();
121926
122677
  return trimmed === "" || trimmed === "status" || trimmed === "pause" || trimmed === "off" ? "always" : "idle-only";
@@ -123242,7 +123993,7 @@ async function handleConnectCommand(host, args) {
123242
123993
  ok: false,
123243
123994
  label: "Failed to load catalog."
123244
123995
  });
123245
- host.showError(`Failed to fetch catalog: ${formatErrorMessage(error)}`);
123996
+ host.showError(t("auth.catalog_fetch_failed", { error: formatErrorMessage(error) }));
123246
123997
  }
123247
123998
  }
123248
123999
  }
@@ -124391,6 +125142,7 @@ var FooterComponent = class {
124391
125142
  left.push(chalk.hex(isFusion ? colors.fusionPlanMode : colors.planMode).bold(isFusion ? t("badge.fusion") : t("badge.plan")));
124392
125143
  }
124393
125144
  if (state.wolfpackMode) left.push(chalk.hex(colors.wolfpackMode).bold(t("badge.wolfpack")));
125145
+ if (state.rlmEnabled) left.push(chalk.hex(colors.rlmMode).bold(t("badge.rlm")));
124394
125146
  if (state.goalActive && state.goal) {
124395
125147
  const goalLabel = formatGoalBadge(state.goal);
124396
125148
  left.push(chalk.hex(colors.primary).bold(goalLabel));
@@ -124679,6 +125431,7 @@ const darkColors = {
124679
125431
  planMode: dark.cyan400,
124680
125432
  fusionPlanMode: dark.amber500,
124681
125433
  wolfpackMode: "#C084FC",
125434
+ rlmMode: "#FFD700",
124682
125435
  text: dark.gray100,
124683
125436
  textStrong: dark.gray50,
124684
125437
  textDim: dark.gray500,
@@ -124710,6 +125463,7 @@ const lightColors = {
124710
125463
  planMode: light.cyan800,
124711
125464
  fusionPlanMode: light.amber700,
124712
125465
  wolfpackMode: "#7C3AED",
125466
+ rlmMode: "#A16207",
124713
125467
  text: light.gray900,
124714
125468
  textStrong: light.gray900,
124715
125469
  textDim: light.gray700,
@@ -125509,6 +126263,61 @@ async function handleAskCommand(host, args) {
125509
126263
  host.setAppState({ permissionMode: "ask" });
125510
126264
  }
125511
126265
  }
126266
+ async function handleRlmCommand(host, args) {
126267
+ const session = host.session;
126268
+ if (session === void 0) {
126269
+ host.showError(getNoActiveSessionMessage());
126270
+ return;
126271
+ }
126272
+ const subcmd = args.trim().toLowerCase();
126273
+ let enabled;
126274
+ if (subcmd.length === 0) enabled = !host.state.appState.rlmEnabled;
126275
+ else if (subcmd === "on") enabled = true;
126276
+ else if (subcmd === "off") enabled = false;
126277
+ else {
126278
+ host.showError(`Unknown rlm subcommand: ${subcmd}`);
126279
+ return;
126280
+ }
126281
+ try {
126282
+ await session.setRlmEnabled(enabled);
126283
+ host.setAppState({ rlmEnabled: enabled });
126284
+ if (enabled) {
126285
+ host.showNotice(t("config.rlm_on"));
126286
+ return;
126287
+ }
126288
+ host.showNotice(t("config.rlm_off"));
126289
+ } catch (error) {
126290
+ const msg = formatErrorMessage(error);
126291
+ host.showError(`Failed to set rlm mode: ${msg}`);
126292
+ }
126293
+ }
126294
+ /** /rlm-max-depth [N] — query or set the maximum RLM recursion depth.
126295
+ * N=0 or no limit means unlimited (the default). */
126296
+ async function handleRlmMaxDepthCommand(host, args) {
126297
+ const session = host.session;
126298
+ if (session === void 0) {
126299
+ host.showError(getNoActiveSessionMessage());
126300
+ return;
126301
+ }
126302
+ const arg = args.trim();
126303
+ if (arg.length === 0) {
126304
+ host.showStatus("RLM max depth: run /rlm-max-depth <N> (0 or blank = unlimited) to set it.");
126305
+ return;
126306
+ }
126307
+ const parsed = Number(arg);
126308
+ if (!Number.isInteger(parsed) || parsed < 0) {
126309
+ host.showError(`Invalid rlm-max-depth: ${arg} (expected a non-negative integer, 0 = unlimited).`);
126310
+ return;
126311
+ }
126312
+ try {
126313
+ await session.setRlmMaxDepth(parsed);
126314
+ if (parsed === 0) host.showStatus("RLM recursion depth set to unlimited.", host.state.theme.colors.success);
126315
+ else host.showStatus(`RLM max depth set to ${parsed}.`, host.state.theme.colors.success);
126316
+ } catch (error) {
126317
+ const msg = formatErrorMessage(error);
126318
+ host.showError(`Failed to set rlm max depth: ${msg}`);
126319
+ }
126320
+ }
125512
126321
  async function handleAutoCommand(host, args) {
125513
126322
  const session = host.session;
125514
126323
  if (session === void 0) {
@@ -126639,7 +127448,7 @@ async function guidedGoalSetup(host) {
126639
127448
  host.showNotice(t("goal.storm_breaker"), t("goal.conflict_loop"));
126640
127449
  return;
126641
127450
  }
126642
- const { TextInputDialogComponent } = await import("./text-input-dialog-K1S_ir1m.mjs");
127451
+ const { TextInputDialogComponent } = await import("./text-input-dialog-BViNsv5f.mjs");
126643
127452
  const initialDesc = await promptText(host, TextInputDialogComponent, {
126644
127453
  title: t("goal.setup_title_initial"),
126645
127454
  subtitle: t("goal.setup_desc_hint"),
@@ -126660,7 +127469,7 @@ async function guidedGoalSetup(host) {
126660
127469
  await showGoalConfigWizard(host, session, confirmed.trim() || objective, false);
126661
127470
  }
126662
127471
  async function showGoalConfigWizard(host, session, objective, replace) {
126663
- const { TextInputDialogComponent } = await import("./text-input-dialog-K1S_ir1m.mjs");
127472
+ const { TextInputDialogComponent } = await import("./text-input-dialog-BViNsv5f.mjs");
126664
127473
  const turnInput = await promptNumber(host, TextInputDialogComponent, {
126665
127474
  title: t("goal.wizard_title", { objective }),
126666
127475
  subtitle: t("goal.budget_turns_hint"),
@@ -127472,7 +128281,7 @@ var AssistantMessageComponent = class {
127472
128281
  const age = Date.now() - (this.fadeStartMs ?? 0);
127473
128282
  this.cachedWidth = void 0;
127474
128283
  this.cachedLines = void 0;
127475
- this.ui?.requestComponentRender(this);
128284
+ this.ui?.requestRender();
127476
128285
  if (age >= 1200) this.stopFade();
127477
128286
  }, FADE_TICK_MS);
127478
128287
  this.ui.requestRender();
@@ -127966,7 +128775,7 @@ var ThinkingComponent = class {
127966
128775
  if (this.ui === void 0 || this.spinnerInterval !== void 0) return;
127967
128776
  this.spinnerInterval = setInterval(() => {
127968
128777
  this.spinnerFrame = (this.spinnerFrame + 1) % BRAILLE_SPINNER_FRAMES.length;
127969
- this.ui?.requestComponentRender(this);
128778
+ this.ui?.requestRender();
127970
128779
  }, 80);
127971
128780
  }
127972
128781
  stopSpinner() {
@@ -132408,7 +133217,7 @@ var MoonLoader = class extends Text {
132408
133217
  const frame = BRAILLE_SPINNER_FRAMES[this.currentFrame];
132409
133218
  const coloredFrame = this.colorFn ? this.colorFn(frame) : frame;
132410
133219
  this.setText(this.label ? `${coloredFrame} ${this.label}` : coloredFrame);
132411
- this.ui.requestComponentRender(this);
133220
+ this.ui.requestRender();
132412
133221
  }
132413
133222
  };
132414
133223
  //#endregion
@@ -132544,9 +133353,6 @@ function getFallbackSkillMarketplace() {
132544
133353
  }
132545
133354
  //#endregion
132546
133355
  //#region src/tui/utils/component-capabilities.ts
132547
- function isExpandable(obj) {
132548
- return typeof obj === "object" && obj !== null && "setExpanded" in obj && typeof obj.setExpanded === "function";
132549
- }
132550
133356
  function isPlanExpandable(obj) {
132551
133357
  return typeof obj === "object" && obj !== null && "setPlanExpanded" in obj && typeof obj.setPlanExpanded === "function";
132552
133358
  }
@@ -135345,6 +136151,12 @@ async function handleBuiltInSlashCommand(host, name, args) {
135345
136151
  case "ask":
135346
136152
  await handleAskCommand(host, args);
135347
136153
  return;
136154
+ case "rlm":
136155
+ await handleRlmCommand(host, args);
136156
+ return;
136157
+ case "rlm-max-depth":
136158
+ await handleRlmMaxDepthCommand(host, args);
136159
+ return;
135348
136160
  case "auto":
135349
136161
  await handleAutoCommand(host, args);
135350
136162
  return;
@@ -135420,6 +136232,17 @@ var AuthFlowController = class {
135420
136232
  availableModels: config.models ?? {},
135421
136233
  availableProviders: config.providers ?? {}
135422
136234
  });
136235
+ this.prefetchCatalogCache();
136236
+ }
136237
+ /**
136238
+ * Best-effort background catalog refresh. Fetches the remote catalog and,
136239
+ * on success, updates the on-disk cache. On failure it stays silent: the
136240
+ * existing cache (or built-in) keeps working. Never throws.
136241
+ */
136242
+ async prefetchCatalogCache() {
136243
+ try {
136244
+ saveCatalogCache(await fetchCatalog(DEFAULT_CATALOG_URL), resolveScreamHome());
136245
+ } catch {}
135423
136246
  }
135424
136247
  async activateModelAfterLogin(model, thinkingLevel) {
135425
136248
  const { host } = this;
@@ -136885,6 +137708,9 @@ var SessionEventHandler = class {
136885
137708
  case "compaction.cancelled":
136886
137709
  this.handleCompactionCancel(event, sendQueued);
136887
137710
  break;
137711
+ case "skill_candidate":
137712
+ this.handleSkillCandidate(event);
137713
+ break;
136888
137714
  case "subagent.spawned":
136889
137715
  this.handleSubagentSpawned(event);
136890
137716
  break;
@@ -137219,6 +138045,7 @@ var SessionEventHandler = class {
137219
138045
  if (event.maxContextTokens !== void 0) patch.maxContextTokens = event.maxContextTokens;
137220
138046
  if (event.planMode !== void 0) patch.planMode = event.planMode ? event.planStrategy === "fusion" ? "fusionplan" : "plan" : "off";
137221
138047
  if (event.wolfpackMode !== void 0) patch.wolfpackMode = event.wolfpackMode;
138048
+ if (event.rlmEnabled !== void 0) patch.rlmEnabled = event.rlmEnabled;
137222
138049
  if (event.permission !== void 0) patch.permissionMode = event.permission;
137223
138050
  if (Object.keys(patch).length > 0) this.host.setAppState(patch);
137224
138051
  }
@@ -137391,6 +138218,28 @@ var SessionEventHandler = class {
137391
138218
  this.lastCompactionTrigger = void 0;
137392
138219
  this.finishCompaction(sendQueued);
137393
138220
  }
138221
+ /** Candidates already prompted this session, to avoid repeating the request. */
138222
+ promptedSkillCandidates = /* @__PURE__ */ new Set();
138223
+ /**
138224
+ * Handles a `skill_candidate` event emitted after compaction detected a
138225
+ * reusable process in the summary. Best-effort and non-fatal: if the session
138226
+ * is unavailable or the prompt fails, nothing happens — the user simply sees
138227
+ * no confirmation. When it works, the model runs an AskUserQuestion dialog
138228
+ * (确定/忽略) so the user can confirm or dismiss with a single keystroke,
138229
+ * then generates the skill via MakeSkillPlanTool on confirmation.
138230
+ */
138231
+ handleSkillCandidate(event) {
138232
+ try {
138233
+ const { name, purpose } = event.candidate;
138234
+ if (name.length === 0) return;
138235
+ if (this.promptedSkillCandidates.has(name)) return;
138236
+ this.promptedSkillCandidates.add(name);
138237
+ const session = this.host.session;
138238
+ if (session === void 0) return;
138239
+ const request = `检测到可复用过程「${name}」${purpose ? `(${purpose})` : ""}。请用 AskUserQuestion 工具询问用户是否将其保存为可复用技能(选项:生成 / 忽略)。用户选择「生成」后,用 MakeSkillPlanTool 生成该技能;选择「忽略」则直接说明已跳过。`;
138240
+ session.prompt(request).catch(() => {});
138241
+ } catch {}
138242
+ }
137394
138243
  finishCompaction(sendQueued) {
137395
138244
  if (!this.host.streamingUI.hasActiveTurn()) {
137396
138245
  this.host.setAppState({
@@ -139749,9 +140598,8 @@ var TasksBrowserController = class {
139749
140598
  colors: state.theme.colors,
139750
140599
  ...this.buildCallbacks()
139751
140600
  }, state.terminal);
139752
- const savedChildren = [...state.ui.children];
139753
- state.ui.clear();
139754
- state.ui.addChild(component);
140601
+ const savedLayoutRoot = state.layoutRoot;
140602
+ state.ui.setLayoutRoot(component);
139755
140603
  state.ui.setFocus(component);
139756
140604
  state.ui.requestRender(true);
139757
140605
  const pollTimer = setInterval(() => {
@@ -139759,7 +140607,7 @@ var TasksBrowserController = class {
139759
140607
  }, 1e3);
139760
140608
  this.host.setTasksBrowser({
139761
140609
  component,
139762
- savedChildren,
140610
+ savedLayoutRoot,
139763
140611
  filter,
139764
140612
  selectedTaskId,
139765
140613
  tailOutput: void 0,
@@ -139779,8 +140627,7 @@ var TasksBrowserController = class {
139779
140627
  if (browser.viewer !== void 0) this.closeOutputViewer();
139780
140628
  if (browser.pollTimer !== void 0) clearInterval(browser.pollTimer);
139781
140629
  if (browser.flashTimer !== void 0) clearTimeout(browser.flashTimer);
139782
- state.ui.clear();
139783
- for (const child of browser.savedChildren) state.ui.addChild(child);
140630
+ state.ui.setLayoutRoot(browser.savedLayoutRoot);
139784
140631
  this.host.setTasksBrowser(void 0);
139785
140632
  state.ui.setFocus(state.editor);
139786
140633
  state.ui.requestRender(true);
@@ -139950,9 +140797,8 @@ var TasksBrowserController = class {
139950
140797
  this.closeOutputViewer();
139951
140798
  }
139952
140799
  }, state.terminal);
139953
- const savedBrowserChildren = [...state.ui.children];
139954
- state.ui.clear();
139955
- state.ui.addChild(viewer);
140800
+ const savedBrowserLayout = state.layoutRoot;
140801
+ state.ui.setLayoutRoot(viewer);
139956
140802
  state.ui.setFocus(viewer);
139957
140803
  state.ui.requestRender(true);
139958
140804
  const pollTimer = setInterval(() => {
@@ -139960,7 +140806,7 @@ var TasksBrowserController = class {
139960
140806
  }, 1e3);
139961
140807
  browser.viewer = {
139962
140808
  component: viewer,
139963
- savedChildren: savedBrowserChildren,
140809
+ savedLayoutRoot: savedBrowserLayout,
139964
140810
  taskId,
139965
140811
  output,
139966
140812
  refreshId: 0,
@@ -140016,8 +140862,7 @@ var TasksBrowserController = class {
140016
140862
  const viewer = browser.viewer;
140017
140863
  clearInterval(viewer.pollTimer);
140018
140864
  browser.viewer = void 0;
140019
- this.host.state.ui.clear();
140020
- for (const child of viewer.savedChildren) this.host.state.ui.addChild(child);
140865
+ this.host.state.ui.setLayoutRoot(viewer.savedLayoutRoot);
140021
140866
  this.host.state.ui.setFocus(browser.component);
140022
140867
  this.host.state.ui.requestRender(true);
140023
140868
  }
@@ -140453,13 +141298,6 @@ var TranscriptController = class TranscriptController {
140453
141298
  toggleToolOutputExpansion() {
140454
141299
  const { state } = this.host;
140455
141300
  state.toolOutputExpanded = !state.toolOutputExpanded;
140456
- const walk = (children) => {
140457
- for (const child of children) {
140458
- if (isExpandable(child)) child.setExpanded(state.toolOutputExpanded);
140459
- if ("children" in child && Array.isArray(child.children)) walk(child.children);
140460
- }
140461
- };
140462
- walk(state.transcriptContainer.children);
140463
141301
  state.ui.requestRender();
140464
141302
  }
140465
141303
  togglePlanExpansion() {
@@ -140508,6 +141346,44 @@ var GutterContainer = class extends Container {
140508
141346
  }
140509
141347
  };
140510
141348
  //#endregion
141349
+ //#region src/tui/components/panes/status-bar-pane.ts
141350
+ /**
141351
+ * A fixed one-line status bar shown directly above the input editor: it
141352
+ * displays the current work phase (idle / waiting / thinking / composing /
141353
+ * tool) with its spinner, pinned to the bottom of the terminal via the dock
141354
+ * VStack. Designed to hold more status widgets later — add children to the
141355
+ * container when needed.
141356
+ */
141357
+ var StatusBarPaneComponent = class extends Container {
141358
+ constructor(options) {
141359
+ super();
141360
+ this.update(options);
141361
+ }
141362
+ /** Re-renders the bar for a new mode/label. The spinner/pulse are the
141363
+ * caller's shared instances (kept alive across mode changes), so this only
141364
+ * swaps which children are visible. */
141365
+ update(options) {
141366
+ this.clear();
141367
+ if (options.mode === "idle") return;
141368
+ if (options.mode === "waiting" || options.mode === "tool" || options.mode === "composing" || options.mode === "thinking") {
141369
+ if (options.pulseWave !== void 0) {
141370
+ if (options.label.length > 0) {
141371
+ const row = new HStack();
141372
+ row.addChild(options.pulseWave);
141373
+ row.addChild(new Text(` ${options.label}`, 1, 0));
141374
+ this.addChild(row);
141375
+ } else this.addChild(options.pulseWave);
141376
+ return;
141377
+ }
141378
+ if (options.spinner !== void 0) {
141379
+ this.addChild(options.spinner);
141380
+ return;
141381
+ }
141382
+ if (options.label.length > 0) this.addChild(new Text(options.label, 1, 0));
141383
+ }
141384
+ }
141385
+ };
141386
+ //#endregion
140511
141387
  //#region src/tui/theme/bundle.ts
140512
141388
  function createScreamTUIThemeBundle(theme, resolvedTheme) {
140513
141389
  const actualTheme = resolvedTheme ?? resolveThemeSync(theme);
@@ -140799,36 +141675,11 @@ var PulseWaveLoader = class extends Text {
140799
141675
  }
140800
141676
  };
140801
141677
  //#endregion
140802
- //#region src/tui/components/panes/activity-pane.ts
140803
- var ActivityPaneComponent = class extends Container {
140804
- constructor(options) {
140805
- super();
140806
- if (options.mode === "waiting" || options.mode === "tool") {
140807
- if (options.spinner !== void 0) {
140808
- this.addChild(new Spacer(1));
140809
- this.addChild(options.spinner);
140810
- }
140811
- if (options.pulseWave !== void 0) {
140812
- this.addChild(new Spacer(1));
140813
- this.addChild(options.pulseWave);
140814
- }
140815
- return;
140816
- }
140817
- if (options.mode === "composing" && options.spinner !== void 0) {
140818
- this.addChild(new Spacer(1));
140819
- this.addChild(options.spinner);
140820
- if (options.pulseWave !== void 0) {
140821
- this.addChild(new Spacer(1));
140822
- this.addChild(options.pulseWave);
140823
- }
140824
- }
140825
- }
140826
- };
140827
- //#endregion
140828
141678
  //#region src/tui/controllers/lifecycle-controller.ts
140829
141679
  var LifecycleController = class LifecycleController {
140830
141680
  host;
140831
141681
  signalCleanupHandlers = [];
141682
+ footerWrap;
140832
141683
  ccConnectPollTimer;
140833
141684
  memoryIdleTimer;
140834
141685
  memoryCountdownTimer;
@@ -140836,6 +141687,9 @@ var LifecycleController = class LifecycleController {
140836
141687
  terminalFocusTrackingDispose;
140837
141688
  terminalThemeTrackingDispose;
140838
141689
  lastActivityMode;
141690
+ /** The active status-bar loader (PulseWaveLoader or MoonLoader). Owned by
141691
+ * this controller; stopped before replacement to avoid leaking timers. */
141692
+ statusBarLoader;
140839
141693
  static MEMORY_IDLE_MS = 900 * 1e3;
140840
141694
  static MEMORY_COUNTDOWN_MS = 15 * 1e3;
140841
141695
  static MEMORY_EXTRACT_COOLDOWN_MS = 600 * 1e3;
@@ -140975,19 +141829,72 @@ var LifecycleController = class LifecycleController {
140975
141829
  buildLayout() {
140976
141830
  const { ui } = this.host.state;
140977
141831
  ui.clear();
140978
- ui.addChild(this.host.state.transcriptContainer);
140979
- ui.addChild(this.host.state.activityContainer);
140980
- ui.addChild(this.host.state.todoPanelContainer);
140981
- ui.addChild(this.host.state.queueContainer);
140982
- ui.addChild(this.host.state.errorBannerContainer);
140983
- ui.addChild(this.host.state.planModeBannerContainer);
140984
- ui.addChild(new FlexSpacer());
140985
- ui.addChild(this.host.state.editorContainer);
141832
+ const scrollContent = new Container();
141833
+ scrollContent.addChild(this.host.state.transcriptContainer);
141834
+ scrollContent.addChild(this.host.state.activityContainer);
141835
+ scrollContent.addChild(this.host.state.queueContainer);
141836
+ const transcriptScrollView = new ScrollView(scrollContent, {
141837
+ follow: "end",
141838
+ primary: true,
141839
+ scrollbar: "auto"
141840
+ });
141841
+ const dock = new VStack([
141842
+ {
141843
+ component: this.host.state.todoPanelContainer,
141844
+ shrink: 1,
141845
+ minSize: 0,
141846
+ visible: (viewport) => viewport.width >= 90
141847
+ },
141848
+ {
141849
+ component: this.host.state.errorBannerContainer,
141850
+ shrink: 1,
141851
+ minSize: 0
141852
+ },
141853
+ {
141854
+ component: this.host.state.planModeBannerContainer,
141855
+ shrink: 1,
141856
+ minSize: 0
141857
+ },
141858
+ {
141859
+ component: this.host.state.statusBarContainer,
141860
+ shrink: 1,
141861
+ minSize: 1,
141862
+ visible: (viewport) => viewport.width >= 60
141863
+ },
141864
+ {
141865
+ component: this.host.state.editorContainer,
141866
+ shrink: 1,
141867
+ minSize: 3
141868
+ },
141869
+ {
141870
+ component: this.ensureFooterWrap(),
141871
+ shrink: 1,
141872
+ minSize: 1
141873
+ }
141874
+ ]);
141875
+ const layoutRoot = new VStack([{
141876
+ component: transcriptScrollView,
141877
+ basis: 0,
141878
+ grow: 1,
141879
+ shrink: 1,
141880
+ minSize: 1
141881
+ }, {
141882
+ component: dock,
141883
+ basis: "auto",
141884
+ grow: 0,
141885
+ shrink: 1,
141886
+ minSize: 1
141887
+ }]);
141888
+ ui.setLayoutRoot(layoutRoot);
141889
+ this.host.state.layoutRoot = layoutRoot;
140986
141890
  }
140987
141891
  mountFooter() {
140988
- const footerWrap = new GutterContainer(1, 1);
140989
- footerWrap.addChild(this.host.state.footer);
140990
- this.host.state.ui.addChild(footerWrap);
141892
+ this.ensureFooterWrap();
141893
+ this.footerWrap.addChild(this.host.state.footer);
141894
+ }
141895
+ ensureFooterWrap() {
141896
+ if (this.footerWrap === void 0) this.footerWrap = new GutterContainer(1, 1);
141897
+ return this.footerWrap;
140991
141898
  }
140992
141899
  refreshTerminalThemeTracking() {
140993
141900
  this.stopTerminalThemeTracking();
@@ -141022,48 +141929,57 @@ var LifecycleController = class LifecycleController {
141022
141929
  this.syncTerminalProgress(this.shouldShowTerminalProgress(effectiveMode));
141023
141930
  if (effectiveMode === this.lastActivityMode) return;
141024
141931
  this.lastActivityMode = effectiveMode;
141932
+ this.updateStatusBar(effectiveMode);
141025
141933
  const { state } = this.host;
141026
141934
  state.activityContainer.clear();
141027
141935
  switch (effectiveMode) {
141028
141936
  case "hidden":
141937
+ case "idle":
141029
141938
  this.stopActivitySpinner();
141030
141939
  this.stopPulseWave();
141031
- state.ui.requestRender();
141032
- return;
141033
- case "waiting": {
141034
- this.stopActivitySpinner();
141035
- const pulseWave = this.ensurePulseWave();
141036
- state.activityContainer.addChild(new ActivityPaneComponent({
141037
- mode: "waiting",
141038
- pulseWave
141039
- }));
141040
141940
  break;
141041
- }
141941
+ case "waiting":
141042
141942
  case "thinking":
141943
+ case "composing":
141944
+ case "tool":
141043
141945
  this.stopActivitySpinner();
141044
141946
  this.stopPulseWave();
141045
141947
  break;
141046
- case "composing": {
141047
- const spinner = this.ensureActivitySpinner("working...", (s) => chalk.hex(state.theme.colors.primary)(s));
141048
- state.activityContainer.addChild(new ActivityPaneComponent({
141049
- mode: "composing",
141050
- spinner
141051
- }));
141052
- break;
141053
- }
141054
- case "tool": {
141055
- this.stopActivitySpinner();
141056
- const pulseWave = this.ensurePulseWave();
141057
- state.activityContainer.addChild(new ActivityPaneComponent({
141058
- mode: "tool",
141059
- pulseWave
141060
- }));
141948
+ }
141949
+ state.ui.requestRender();
141950
+ }
141951
+ /**
141952
+ * Updates the fixed status bar above the editor to reflect the current
141953
+ * work phase. Called only when the phase actually changes. Uses its OWN
141954
+ * spinner/pulse instances (a component can only have one parent, so the
141955
+ * activity pane's instances must not be shared). Recreating a loader on
141956
+ * every call would leak setInterval timers (each PulseWaveLoader.start()
141957
+ * registers one and the old instance is never stopped), so the previous
141958
+ * instance is explicitly stopped before a new one is mounted. Idle leaves
141959
+ * the bar empty.
141960
+ */
141961
+ updateStatusBar(mode) {
141962
+ const { state } = this.host;
141963
+ state.statusBarContainer.clear();
141964
+ this.statusBarLoader?.stop();
141965
+ this.statusBarLoader = void 0;
141966
+ switch (mode) {
141967
+ case "waiting":
141968
+ case "composing":
141969
+ case "tool":
141970
+ {
141971
+ const loader = new PulseWaveLoader(state.ui, state.theme.colors.primary);
141972
+ this.statusBarLoader = loader;
141973
+ state.statusBarContainer.addChild(new StatusBarPaneComponent({
141974
+ mode,
141975
+ label: "",
141976
+ pulseWave: loader
141977
+ }));
141978
+ }
141061
141979
  break;
141062
- }
141980
+ case "thinking": break;
141063
141981
  case "idle":
141064
- this.stopActivitySpinner();
141065
- this.stopPulseWave();
141066
- break;
141982
+ case "hidden": break;
141067
141983
  }
141068
141984
  state.ui.requestRender();
141069
141985
  }
@@ -141086,28 +142002,12 @@ var LifecycleController = class LifecycleController {
141086
142002
  this.host.state.terminal.setProgress(active);
141087
142003
  this.host.state.terminalState.progressActive = active;
141088
142004
  }
141089
- ensureActivitySpinner(label = "", colorFn) {
141090
- if (this.host.state.activitySpinner === null) {
141091
- const instance = new MoonLoader(this.host.state.ui, colorFn, label);
141092
- this.host.state.activitySpinner = { instance };
141093
- return instance;
141094
- }
141095
- this.host.state.activitySpinner.instance.setLabel(label);
141096
- if (colorFn !== void 0) this.host.state.activitySpinner.instance.setColorFn(colorFn);
141097
- return this.host.state.activitySpinner.instance;
141098
- }
141099
142005
  stopActivitySpinner() {
141100
142006
  if (this.host.state.activitySpinner !== null) {
141101
142007
  this.host.state.activitySpinner.instance.stop();
141102
142008
  this.host.state.activitySpinner = null;
141103
142009
  }
141104
142010
  }
141105
- ensurePulseWave() {
141106
- if (this.host.state.pulseWave !== null) return this.host.state.pulseWave;
141107
- const instance = new PulseWaveLoader(this.host.state.ui, this.host.state.theme.colors.primary);
141108
- this.host.state.pulseWave = instance;
141109
- return instance;
141110
- }
141111
142011
  stopPulseWave() {
141112
142012
  if (this.host.state.pulseWave !== null) {
141113
142013
  this.host.state.pulseWave.stop();
@@ -141534,18 +142434,10 @@ var InputController = class InputController {
141534
142434
  this.host = host;
141535
142435
  }
141536
142436
  setupAutocomplete() {
141537
- const visible = this.host.getSlashCommands().filter((cmd) => !(cmd.name.startsWith("skill:") && cmd.source === "builtin"));
141538
- const slashCommands = visible.map((cmd) => cmd);
142437
+ const slashCommands = this.host.getSlashCommands().filter((cmd) => !(cmd.name.startsWith("skill:") && cmd.source === "builtin")).map((cmd) => cmd);
141539
142438
  const { state } = this.host;
141540
142439
  const provider = new FileMentionProvider(slashCommands, state.appState.workDir, state.fdPath, state.gitLsFilesCache);
141541
142440
  state.editor.setAutocompleteProvider(provider);
141542
- const argumentHints = /* @__PURE__ */ new Map();
141543
- for (const cmd of visible) {
141544
- if (cmd.argumentHint === void 0) continue;
141545
- argumentHints.set(cmd.name, cmd.argumentHint);
141546
- for (const alias of cmd.aliases) argumentHints.set(alias, cmd.argumentHint);
141547
- }
141548
- state.editor.setArgumentHints(argumentHints);
141549
142441
  state.editor.onFirstInput = () => {
141550
142442
  this.host.stopWelcomeBreathing();
141551
142443
  this.#permanentlyStopBreathing();
@@ -142840,8 +143732,7 @@ function createTUIState(options) {
142840
143732
  const initialAppState = options.initialAppState;
142841
143733
  const theme = createScreamTUIThemeBundle(initialAppState.theme, options.resolvedTheme);
142842
143734
  const terminal = new ProcessTerminal();
142843
- const ui = new TUI(terminal);
142844
- ui.setClearOnShrink(true);
143735
+ const ui = new TuiAltScreen(terminal);
142845
143736
  const uiAny = ui;
142846
143737
  const originalDoRender = uiAny["doRender"].bind(ui);
142847
143738
  let recovering = false;
@@ -142867,6 +143758,7 @@ function createTUIState(options) {
142867
143758
  };
142868
143759
  const transcriptContainer = new GutterContainer(1, 1);
142869
143760
  const activityContainer = new GutterContainer(1, 1);
143761
+ const statusBarContainer = new GutterContainer(1, 1);
142870
143762
  const todoPanelContainer = new GutterContainer(1, 1);
142871
143763
  const todoPanel = new TodoPanelComponent(theme.colors);
142872
143764
  const queueContainer = new GutterContainer(1, 1);
@@ -142884,8 +143776,10 @@ function createTUIState(options) {
142884
143776
  return {
142885
143777
  ui,
142886
143778
  terminal,
143779
+ layoutRoot: void 0,
142887
143780
  transcriptContainer,
142888
143781
  activityContainer,
143782
+ statusBarContainer,
142889
143783
  todoPanelContainer,
142890
143784
  todoPanel,
142891
143785
  queueContainer,
@@ -143372,9 +144266,12 @@ const PRUNE_EMPTY_SESSION_GRACE_MS = 300 * 1e3;
143372
144266
  function isPrunableEmptySession(summary, now, graceMs = PRUNE_EMPTY_SESSION_GRACE_MS) {
143373
144267
  if (summary.archived) return false;
143374
144268
  if (summary.lastPrompt !== void 0) return false;
143375
- if (summary.title) return false;
144269
+ if (isUntitledTitle(summary.title)) return false;
143376
144270
  return now - summary.updatedAt > graceMs;
143377
144271
  }
144272
+ function isUntitledTitle(title) {
144273
+ return typeof title === "string" && title.trim().length > 0 && title !== "New Session";
144274
+ }
143378
144275
  /**
143379
144276
  * Encapsulates all session lifecycle operations:
143380
144277
  * create / resume / switch / close / sync state / reset runtime.
@@ -143471,6 +144368,7 @@ var SessionManager$1 = class {
143471
144368
  thinkingLevel: status.thinkingLevel,
143472
144369
  planMode: status.planMode ? status.planStrategy === "fusion" ? "fusionplan" : "plan" : "off",
143473
144370
  wolfpackMode: status.wolfpackMode,
144371
+ rlmEnabled: status.rlmEnabled,
143474
144372
  contextTokens: status.contextTokens,
143475
144373
  maxContextTokens: status.maxContextTokens,
143476
144374
  contextUsage: status.contextUsage,
@@ -143789,7 +144687,7 @@ var ApprovalPanelComponent = class extends Container {
143789
144687
  };
143790
144688
  if (this.ui !== void 0) this.intervalId = setInterval(() => {
143791
144689
  this.animFrame = (this.animFrame + 1) % PIXEL_PULSE_FRAMES.length;
143792
- this.ui?.requestComponentRender(this);
144690
+ this.ui?.requestRender();
143793
144691
  }, 100);
143794
144692
  }
143795
144693
  /** Stop the selection pulse. Idempotent; called on submit and on hide. */
@@ -144644,6 +145542,15 @@ var MemoryPickerComponent = class extends Container {
144644
145542
  }
144645
145543
  lines.push("");
144646
145544
  }
145545
+ if (memo.note !== void 0 && memo.note.length > 0) {
145546
+ const label = t("memory.note_label");
145547
+ const wrapped = wrapText(memo.note, contentWidth - visibleWidth(label));
145548
+ for (let i = 0; i < wrapped.length; i++) {
145549
+ const prefix = i === 0 ? `${indent}${label}` : indent + " ".repeat(visibleWidth(label));
145550
+ lines.push(truncateToWidth(chalk.hex(c.textMuted)(prefix + wrapped[i]), width, ELLIPSIS$1));
145551
+ }
145552
+ lines.push("");
145553
+ }
144647
145554
  lines.push(chalk.hex(c.textMuted)(truncateToWidth(t("memory.detail_nav_hint"), width, ELLIPSIS$1)));
144648
145555
  lines.push(chalk.hex(c.primary)("─".repeat(width)));
144649
145556
  return lines;
@@ -145009,7 +145916,7 @@ var QuestionDialogComponent = class extends Container {
145009
145916
  this.answers = Array.from({ length: total }, () => void 0);
145010
145917
  if (this.ui !== void 0) this.intervalId = setInterval(() => {
145011
145918
  this.animFrame = (this.animFrame + 1) % PIXEL_PULSE_FRAMES.length;
145012
- this.ui?.requestComponentRender(this);
145919
+ this.ui?.requestRender();
145013
145920
  }, 100);
145014
145921
  }
145015
145922
  /** Stop the wizard pulse. Idempotent; called on respond and on hide. */
@@ -145721,7 +146628,7 @@ var DialogManager = class {
145721
146628
  }
145722
146629
  openApprovalPreview(panel, block) {
145723
146630
  if (this.approvalPreview !== void 0) return;
145724
- const savedChildren = [...this.host.state.ui.children];
146631
+ const savedLayoutRoot = this.host.state.layoutRoot;
145725
146632
  const viewer = new ApprovalPreviewViewer({
145726
146633
  block,
145727
146634
  colors: this.host.state.theme.colors,
@@ -145729,13 +146636,12 @@ var DialogManager = class {
145729
146636
  this.closeApprovalPreview();
145730
146637
  }
145731
146638
  }, this.host.state.terminal);
145732
- this.host.state.ui.clear();
145733
- this.host.state.ui.addChild(viewer);
146639
+ this.host.state.ui.setLayoutRoot(viewer);
145734
146640
  this.host.state.ui.setFocus(viewer);
145735
146641
  this.host.state.ui.requestRender(true);
145736
146642
  this.approvalPreview = {
145737
146643
  component: viewer,
145738
- savedChildren,
146644
+ savedLayoutRoot,
145739
146645
  panel
145740
146646
  };
145741
146647
  }
@@ -145743,8 +146649,7 @@ var DialogManager = class {
145743
146649
  const preview = this.approvalPreview;
145744
146650
  if (preview === void 0) return;
145745
146651
  this.approvalPreview = void 0;
145746
- this.host.state.ui.clear();
145747
- for (const child of preview.savedChildren) this.host.state.ui.addChild(child);
146652
+ this.host.state.ui.setLayoutRoot(preview.savedLayoutRoot);
145748
146653
  this.host.state.ui.setFocus(preview.panel);
145749
146654
  this.host.state.ui.requestRender(true);
145750
146655
  }
@@ -145814,6 +146719,7 @@ function createInitialAppState(input) {
145814
146719
  goalContinuationCount: 0,
145815
146720
  ccConnectActive: false,
145816
146721
  wolfpackMode: input.cliOptions.wolfpack === true,
146722
+ rlmEnabled: false,
145817
146723
  reconnectAttempt: 0,
145818
146724
  recentSessions: [],
145819
146725
  subagentUsage: {}
@@ -145834,7 +146740,6 @@ var ScreamTUI = class {
145834
146740
  deferUserMessages = false;
145835
146741
  aborted = false;
145836
146742
  isShuttingDown = false;
145837
- tightModeHandler = null;
145838
146743
  reverseRpcDisposers = [];
145839
146744
  startupNotice;
145840
146745
  /** Interval handle for the periodic provider-balance refresh. */
@@ -145933,11 +146838,6 @@ var ScreamTUI = class {
145933
146838
  this.inputController.setupAutocomplete();
145934
146839
  }
145935
146840
  async start() {
145936
- this.tightModeHandler = () => {
145937
- setTightMode((process.stdout.columns ?? 80) < 60);
145938
- };
145939
- this.tightModeHandler();
145940
- process.stdout.on("resize", this.tightModeHandler);
145941
146841
  this.lifecycleController.installSignalHandlers();
145942
146842
  try {
145943
146843
  const shouldReplayHistory = await this.initMainTui();
@@ -146028,11 +146928,6 @@ var ScreamTUI = class {
146028
146928
  async stop(exitCode) {
146029
146929
  if (this.isShuttingDown) return;
146030
146930
  this.isShuttingDown = true;
146031
- if (this.tightModeHandler !== null) {
146032
- process.stdout.off("resize", this.tightModeHandler);
146033
- this.tightModeHandler = null;
146034
- }
146035
- setTightMode(false);
146036
146931
  this.lifecycleController.stopCcConnectPolling();
146037
146932
  if (this.balancePollTimer !== void 0) {
146038
146933
  clearInterval(this.balancePollTimer);
@@ -148641,6 +149536,7 @@ var WebSession = class {
148641
149536
  permission: this.permission,
148642
149537
  planMode: false,
148643
149538
  wolfpackMode: false,
149539
+ rlmEnabled: false,
148644
149540
  contextTokens: 0,
148645
149541
  maxContextTokens: 0,
148646
149542
  contextUsage: 0