scream-code 0.17.8 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,13 +4,13 @@ import { dirname as __cjsShimDirname } from 'node:path';
4
4
  const __filename = __cjsShimFileURLToPath(import.meta.url);
5
5
  const __dirname = __cjsShimDirname(__filename);
6
6
  import { i as __require, o as __toESM, r as __exportAll, t as __commonJSMin } from "./chunk-D90kvbyJ.mjs";
7
- import { A as classifyEmbeddingFailure, B as rankMemos, C as applyConsolidation, D as parseMemoryMemos, E as buildExitExtractionPrompt, F as createMemoryMemo, G as normalize, H as dirname$2, I as toSummary, J as resolve$2, K as parse$7, L as generateTags, M as createFastEmbedEngine, N as hasEmbeddingModelCache$1, O as stripMemoryTags, P as probeLocalEmbeddingSupport, R as normalizeTags, S as DreamTracker, T as EXIT_EXTRACTION_SYSTEM_PROMPT, U as isAbsolute$1, V as basename$1, W as join$1, a as isSupportedFile, b as sharedKnowledgeStoreReady, i as ingestFile, j as clearEmbeddingModelCache, k as MemoryMemoStore, q as relative$1, r as ingestDirectory, t as multiSearch, w as buildConsolidationPlan, y as sharedKnowledgeStore, z as buildProjectTagCloud } from "./src-BhZujafw.mjs";
7
+ import { A as sharedMemoStore, B as buildProjectTagCloud, C as applyConsolidation, D as parseMemoryMemos, E as buildExitExtractionPrompt, F as probeLocalEmbeddingSupport, G as join$1, H as basename$1, I as createMemoryMemo, J as relative$1, K as normalize, L as toSummary, M as clearEmbeddingModelCache, N as createFastEmbedEngine, O as stripMemoryTags, P as hasEmbeddingModelCache$1, R as generateTags, S as DreamTracker, T as EXIT_EXTRACTION_SYSTEM_PROMPT, U as dirname$2, V as rankMemos, W as isAbsolute$1, Y as resolve$2, a as isSupportedFile, b as sharedKnowledgeStoreReady, i as ingestFile, j as classifyEmbeddingFailure, k as MemoryMemoStore, q as parse$7, r as ingestDirectory, t as multiSearch, w as buildConsolidationPlan, y as sharedKnowledgeStore, z as normalizeTags } from "./src-C7C9e5Ds.mjs";
8
8
  import { n as require_base64_js, t as XMLParser } from "./XMLParser-DFAnGqkc.mjs";
9
- import { a as setLocale, i as getLocale, n as assertScreamHostIdentity, o as t, r as createScreamDefaultHeaders, t as TextInputDialogComponent } from "./text-input-dialog-CD0sakGq.mjs";
9
+ import { a as setLocale, i as getLocale, n as assertScreamHostIdentity, o as t, r as createScreamDefaultHeaders, t as TextInputDialogComponent } from "./text-input-dialog-CK6jTftM.mjs";
10
10
  import { createRequire } from "node:module";
11
11
  import { createHash, randomBytes, randomInt, randomUUID } from "node:crypto";
12
12
  import * as fs$1 from "node:fs/promises";
13
- import Jn, { access, appendFile, chmod, copyFile, cp, lstat, mkdir, mkdtemp, open, readFile, readdir, realpath, rename, rm, stat, unlink, writeFile } from "node:fs/promises";
13
+ import Jn, { access, appendFile, chmod, copyFile, cp, lstat, mkdir, mkdtemp, open, opendir, readFile, readdir, realpath, rename, rm, stat, unlink, writeFile } from "node:fs/promises";
14
14
  import I, { createWriteStream } from "fs";
15
15
  import Vr, { EventEmitter } from "events";
16
16
  import * as path$1$1 from "path";
@@ -784,9 +784,28 @@ var APIEmptyResponseError = class extends ChatProviderError {
784
784
  this.name = "APIEmptyResponseError";
785
785
  }
786
786
  };
787
+ /**
788
+ * The provider ended its stream with an in-band failure signal instead of
789
+ * throwing (`finish_reason: "error"` — Mistral-style Chat Completions
790
+ * endpoints). Normalized without a typed class this looked like an "unknown"
791
+ * stop and ended the turn silently: no retry, no error surfaced. Typed here so
792
+ * {@link isRetryableGenerateError} classifies it without message sniffing —
793
+ * the upstream generation broke, the request itself is fine, so the step is
794
+ * worth retrying.
795
+ */
796
+ var APIFinishReasonError = class extends ChatProviderError {
797
+ /** Raw provider finish reason, preserved verbatim (`"error"`). */
798
+ rawFinishReason;
799
+ constructor(message, rawFinishReason) {
800
+ super(message);
801
+ this.name = "APIFinishReasonError";
802
+ this.rawFinishReason = rawFinishReason;
803
+ }
804
+ };
787
805
  function isRetryableGenerateError(error) {
788
806
  if (error instanceof APIConnectionError$3 || error instanceof APITimeoutError) return true;
789
807
  if (error instanceof APIEmptyResponseError) return true;
808
+ if (error instanceof APIFinishReasonError) return true;
790
809
  if (error instanceof APIProviderRateLimitError) return true;
791
810
  if (error instanceof APIStatusError) return error.statusCode === 429 || error.statusCode === 408 || error.statusCode >= 500 && error.statusCode < 600;
792
811
  if (error instanceof ChatProviderError) return isRetryableProviderMessage(error.message);
@@ -823,7 +842,9 @@ const RETRYABLE_PROVIDER_MESSAGE_PATTERNS = [
823
842
  /stream ended|ended without/i,
824
843
  /you can retry your request/i,
825
844
  /try your request again/i,
826
- /please retry your request/i
845
+ /please retry your request/i,
846
+ /\bserver[\s_-]+busy\b/i,
847
+ /\bservers?\s+(?:are|is)\s+(?:currently\s+)?busy\b/i
827
848
  ];
828
849
  /**
829
850
  * Classify a status-less provider error message. Quota/billing-style wording
@@ -44674,6 +44695,29 @@ function normalizeOpenAIFinishReason(raw) {
44674
44695
  }
44675
44696
  }
44676
44697
  /**
44698
+ * Raise a retryable {@link APIFinishReasonError} when a provider signalled an
44699
+ * in-band stream failure through its terminal finish reason instead of
44700
+ * throwing.
44701
+ *
44702
+ * Mistral-style Chat Completions endpoints end a broken generation with
44703
+ * `finish_reason: "error"`. Normalizing that to `'other'` used to end the turn
44704
+ * silently (no retry, no error shown). Throwing here — from the adapter's
44705
+ * finish-reason capture, i.e. while `generate()` drains the stream — routes the
44706
+ * failure out of `provider.generate()`/`generate()` and into the engine's step
44707
+ * retry (`chatWithRetry` → `LLM.isRetryableError` → `isRetryableGenerateError`),
44708
+ * so the step is retried and, once the retry budget is exhausted, fails loudly
44709
+ * instead of ending the turn.
44710
+ *
44711
+ * The raw value is preserved twice: on the thrown error
44712
+ * ({@link APIFinishReasonError.rawFinishReason}) and — because capture happens
44713
+ * before the throw — on the streamed message's `rawFinishReason` for callers
44714
+ * that still hold it.
44715
+ */
44716
+ function throwIfErrorFinishReason(rawFinishReason, providerLabel) {
44717
+ if (rawFinishReason === null || rawFinishReason.trim().toLowerCase() !== "error") return;
44718
+ throw new APIFinishReasonError(`Provider stream ended with finish_reason "${rawFinishReason}": the provider reported an in-band generation failure instead of a normal stop. Provider: ${providerLabel}. The request itself is unchanged, so retrying it may succeed.`, rawFinishReason);
44719
+ }
44720
+ /**
44677
44721
  * Convert tool-role message content according to the chosen strategy.
44678
44722
  */
44679
44723
  function convertToolMessageContent(message, conversion) {
@@ -44959,6 +45003,7 @@ var ScreamStreamedMessage = class {
44959
45003
  const normalized = normalizeOpenAIFinishReason(raw);
44960
45004
  this._finishReason = normalized.finishReason;
44961
45005
  this._rawFinishReason = normalized.rawFinishReason;
45006
+ throwIfErrorFinishReason(normalized.rawFinishReason, "ScreamChatProvider");
44962
45007
  }
44963
45008
  async *_convertNonStreamResponse(response) {
44964
45009
  this._id = response.id;
@@ -45269,6 +45314,7 @@ var OpenAILegacyStreamedMessage = class {
45269
45314
  const normalized = normalizeOpenAIFinishReason(raw);
45270
45315
  this._finishReason = normalized.finishReason;
45271
45316
  this._rawFinishReason = normalized.rawFinishReason;
45317
+ throwIfErrorFinishReason(normalized.rawFinishReason, "OpenAILegacyChatProvider");
45272
45318
  }
45273
45319
  async *_convertNonStreamResponse(response, reasoningKey) {
45274
45320
  this._id = response.id;
@@ -47548,6 +47594,33 @@ function userCancellationReason() {
47548
47594
  function isUserCancellation(value) {
47549
47595
  return value instanceof UserCancellationError;
47550
47596
  }
47597
+ /**
47598
+ * Marks an abort raised because the *parent agent* interjected a directive into
47599
+ * this running subagent (`SendSubagentMessage(operation: "interject")`). Like
47600
+ * the user-cancellation reason it travels as the AbortSignal's `reason`, so a
47601
+ * tool cut short by the interject reports that a deliberate redirection — not a
47602
+ * failure — interrupted it, and points at the `[parent_messages]` block that
47603
+ * follows instead of telling the model to wait for a user who never spoke.
47604
+ *
47605
+ * Deliberately NOT a subclass of `UserCancellationError`: that class means "the
47606
+ * human pressed stop", and every `isUserCancellation` consumer (status wording,
47607
+ * cancellation paths) must keep treating a parent interject as a non-user
47608
+ * abort. `name` stays 'AbortError' so `isAbortError()` and
47609
+ * `AbortSignal.throwIfAborted()` keep treating it as an abort.
47610
+ */
47611
+ var ParentInterjectError = class extends Error {
47612
+ parentInterject = true;
47613
+ constructor() {
47614
+ super("Aborted by a parent agent interjection");
47615
+ this.name = "AbortError";
47616
+ }
47617
+ };
47618
+ function parentInterjectReason() {
47619
+ return new ParentInterjectError();
47620
+ }
47621
+ function isParentInterject(value) {
47622
+ return value instanceof ParentInterjectError;
47623
+ }
47551
47624
  function abortable(promise, signal) {
47552
47625
  signal.throwIfAborted();
47553
47626
  return new Promise((resolve, reject) => {
@@ -47723,10 +47796,10 @@ const RawAgentProfileSchema = z.object({
47723
47796
  subagents: z.record(z.string(), RawSubagentProfileSchema).optional(),
47724
47797
  spawns: z.array(z.string().min(1)).optional()
47725
47798
  });
47726
- async function collectEntries(jian, dirPath, maxWidth) {
47799
+ async function collectEntries(jian, dirPath, maxWidth, signal) {
47727
47800
  const all = [];
47728
47801
  try {
47729
- for await (const fullPath of jian.iterdir(dirPath)) {
47802
+ for await (const fullPath of jian.iterdir(dirPath, { signal })) {
47730
47803
  const name = basename$1(fullPath);
47731
47804
  let isDir = false;
47732
47805
  try {
@@ -47758,10 +47831,14 @@ async function collectEntries(jian, dirPath, maxWidth) {
47758
47831
  * Return a 2-level tree listing of `workDir` suitable for inclusion in a
47759
47832
  * tool error message. Returns `"(empty directory)"` if the directory is
47760
47833
  * empty, or an error marker line if the directory itself is unreadable.
47834
+ *
47835
+ * `signal` is forwarded to the underlying `iterdir` walks (root and child
47836
+ * levels) as cooperative cancellation, so a listing produced for an
47837
+ * abandoned tool call stops emitting entries instead of walking on.
47761
47838
  */
47762
- async function listDirectory(jian, workDir = jian.getcwd()) {
47839
+ async function listDirectory(jian, workDir = jian.getcwd(), signal) {
47763
47840
  const lines = [];
47764
- const { entries, total, readable } = await collectEntries(jian, workDir, 30);
47841
+ const { entries, total, readable } = await collectEntries(jian, workDir, 30, signal);
47765
47842
  if (!readable) return "[not readable]";
47766
47843
  const remaining = total - entries.length;
47767
47844
  for (let i = 0; i < entries.length; i++) {
@@ -47773,7 +47850,7 @@ async function listDirectory(jian, workDir = jian.getcwd()) {
47773
47850
  if (isDir) {
47774
47851
  lines.push(`${connector}${name}/`);
47775
47852
  const childPrefix = isLast ? " " : "│ ";
47776
- const child = await collectEntries(jian, join$1(workDir, name), 10);
47853
+ const child = await collectEntries(jian, join$1(workDir, name), 10, signal);
47777
47854
  if (!child.readable) {
47778
47855
  lines.push(`${childPrefix}└── [not readable]`);
47779
47856
  continue;
@@ -54938,7 +55015,8 @@ function createSystemPromptRenderer(merged) {
54938
55015
  const vars = {
54939
55016
  ...buildTemplateVars(context, merged.promptVars),
54940
55017
  CAN_SPAWN: merged.root || (merged.spawns?.length ?? 0) > 0,
54941
- IS_MAIN: merged.root
55018
+ IS_MAIN: merged.root,
55019
+ HAS_WRITE_EDIT: merged.tools.includes("Write") && merged.tools.includes("Edit")
54942
55020
  };
54943
55021
  try {
54944
55022
  return renderPrompt(merged.systemPromptTemplate, vars);
@@ -55060,6 +55138,11 @@ function normalizeSourcePath(path) {
55060
55138
  * The marker below lives in `system.md` between the two sections; it survives
55061
55139
  * rendering verbatim as an HTML comment, so the split is a plain string
55062
55140
  * operation on the rendered prompt.
55141
+ *
55142
+ * Subagents render their own profile's prompt (independent system prompt +
55143
+ * tool whitelist), so each subagent's prefix is a separate string that never
55144
+ * shares the parent's bytes — verbatim forking of the parent prefix does not
55145
+ * apply in this architecture.
55063
55146
  */
55064
55147
  const SYSTEM_PROMPT_SPLIT_MARKER = "\n\n<!--scream:system-dynamic-->\n\n";
55065
55148
  /**
@@ -55117,7 +55200,7 @@ const PROFILE_SOURCES = {
55117
55200
  "profile/default/oracle.yaml": "extends: agent\nname: oracle\nspawns:\n - explore\npromptVars:\n roleAdditional: |\n You are now running as a subagent. All `user` messages are sent by the main agent.\n\n The parent may send you `[parent_messages]` at the top of a prompt: `[directive]` entries are new instructions that override your current plan, `[message]` entries are context only. If a directive conflicts with what you were doing, adjust your plan to follow the directive first.\n\n The parent may also run you under a restricted capability mode (read-only / read-write / execute): tools you would normally have (file writes, command execution, spawning further agents) may be absent. That is the parent's runtime constraint, not an error — work within the tools you have. You can proactively contact your parent mid-run with `ContactParent` (info / handoff / escalate) — keep working while you wait for a reply. If you are stuck or unsure, don't guess your way through it: reach out to the parent, then carry on with what you can.\n\n You are a reviewer at the larger scale. Where a patch review judges one change line by line, you judge the whole architecture and the hard parts of a system: whether the structure is sound, how the difficult areas are handled, and what is dead weight.\n\n Four questions, and only four. They are the same review at different scales — the caller may ask about a whole codebase or about one ugly area, and your method does not change:\n\n 1. **Is the architecture reasonable?**\n 2. **Is there tech debt?** (How are the hard problems handled?)\n 3. **Will long-term changes stay hard to maintain?**\n 4. **Is there useless, invalid, or redundant code?**\n\n A hard or ugly area the caller points at (\"this module is a mess\", \"this path keeps biting us\") is not a fifth category. Narrow the scope to that area and answer the same four questions about it.\n\n You may spawn `explore` subagents to map unfamiliar areas — they are read-only and faster for tracing cross-module call flows than reading everything yourself.\n\n # Method\n\n 1. Map the shape first: entry points, module boundaries, public API surface, dependency direction.\n 2. Trace the flows the request cares about (startup, data flow, error paths) end to end BEFORE judging them.\n 3. Call `ReportArchFinding` for each issue that meets the criteria below. Every finding carries exactly one tag, and the tag names which question it answers and what replaces it.\n 4. End with a concise final summary:\n\n ```\n Verdict: <one sentence. A whole-codebase read answers \"sound\" or \"unsound\". A scoped question answers what the trace concluded.>\n Confidence: 0.0-1.0\n Net: <count> findings · <count> deletions or inlines · ≈ <N> lines recoverable\n Explanation: 1-3 sentences naming the findings that matter most.\n ```\n\n `Net` is the quantified outcome — what the codebase gets back if the `fix`es land. Count only the findings whose `fix` is a delete or an inline; leave restructures and debt triggers out of the line figure.\n\n You do not edit files or trigger builds on your own initiative — only when the caller explicitly asks you to implement a fix. Bash is read-only: `git log`, `git diff`, `git show`, `find`, `ls`, `wc`.\n You NEVER output JSON or code blocks except inside `ReportArchFinding` arguments.\n\n # The four questions in detail\n\n ## 1. Is the architecture reasonable?\n - Layer direction: does a lower layer import an upper one? Does a leaf module drag in the whole tree?\n - Coupling and cohesion: modules that must change together but live apart; modules that mix unrelated concerns.\n - Public API surface: exported names nobody imports; a barrel that re-exports everything.\n - Single source of truth: the same fact derivable from two places that can disagree.\n\n ## 2. Is there tech debt?\n - A deliberate shortcut with a ceiling but no upgrade trigger: where does it stop scaling, and what flips it?\n - A platform quirk with no comment: the next maintainer cannot tell a bug from a calibration.\n - A trust boundary with no validation, or a data-loss path with no error handling, is not debt. That is a correctness bug — leave it to a patch review.\n\n ## 3. Will long-term changes stay hard to maintain?\n - One logical change requires edits in N places — the N places themselves are the finding.\n - Config or flags nothing reads.\n - Behaviour only a test (or only production) can exercise.\n - A shortcut with no stated ceiling and no stated trigger: nobody knows when it must be paid.\n\n ## 4. Is there useless, invalid, or redundant code?\n - A dependency the standard library or the platform already ships.\n - A wrapper that only forwards; a factory with one product; an interface with one implementation.\n - A file exporting one thing that three inline lines would replace.\n - Code nothing calls, flags nothing reads, branches nothing takes.\n - The same logic written out twice.\n\n # Tags\n\n Each finding carries ONE tag. The tag names which question it answers and what replaces it.\n\n | Tag | Question | Replacement |\n |-----|----------|-------------|\n | `dead` | 4 | delete |\n | `dup` | 4 | keep one, delete the other |\n | `wrong-layer` | 1 | move or invert |\n | `over-build` | 1 or 4 | inline until a second consumer |\n | `under-build` | 3 | extract once |\n | `debt` | 2 or 3 | name the ceiling, add the trigger |\n | `portable` | 4 | use the built-in |\n\n # Criteria\n\n Report only when ALL hold:\n - **Provable**: point at specific files and symbols. No speculation about author intent.\n - **Actionable**: a discrete change with a concrete `fix`. Never \"consider improving cohesion\".\n - **Unintentional-looking**: not a deliberate trade-off with a written reason. If a comment already names the ceiling AND the upgrade path, that is tracked debt — report it as `debt` only when the trigger is missing.\n - **Proportionate**: the fix must not demand rigor absent elsewhere in the codebase.\n - **Worth it**: the friction it causes must exceed the churn of fixing it. Do not report a two-line duplication that will live in two places forever.\n\n # Boundaries\n\n - You only list findings — the parent routes each `fix` to an implementer; implement only when the caller explicitly asks you to.\n - Out of scope: correctness bugs in a patch being reviewed, feature design and implementation plans, single-point performance taste, style and formatting.\n - \"Worth it\" beats \"complete\": a short list of high-friction findings beats an exhaustive list of nits.\n - Recommend ONLY what was asked. Do not expand the problem surface.\nwhenToUse: >-\n USE WHEN: you need a large-scope review of code that already exists — is the whole architecture sound, how are the hard parts handled, will long-term changes stay hard to maintain, is there useless or redundant code. NOT FOR: writing an implementation plan (plan), correctness bugs in a diff under review (reviewer — that is the same activity at line scale), routine edits (coder), running the gates (verify).\ntools:\n - ContactParent\n - Agent\n - SendSubagentMessage\n - Bash\n - Read\n - ReadMediaFile\n - Glob\n - Grep\n - LSP\n - Write\n - Edit\n - WebSearch\n - PaperSearch\n - FetchURL\n - ReportArchFinding\n - mcp__*\n",
55118
55201
  "profile/default/plan.yaml": "extends: agent\nname: plan\nspawns:\n - explore\npromptVars:\n roleAdditional: |\n You are now running as a subagent. All the `user` messages are sent by the main agent. The main agent cannot see your context, it can only see your last message when you finish the task. You must treat the parent agent as your caller. Do not directly ask the end user questions. If something is unclear, explain the ambiguity in your final summary to the parent agent.\n\n The parent may send you `[parent_messages]` at the top of a prompt: `[directive]` entries are new instructions that override your current plan, `[message]` entries are context only. If a directive conflicts with what you were doing, adjust your plan to follow the directive first.\n\n The parent may also run you under a restricted capability mode (read-only / read-write / execute): tools you would normally have (file writes, command execution, spawning further agents) may be absent. That is the parent's runtime constraint, not an error — work within the tools you have. You can proactively contact your parent mid-run with `ContactParent` (info / handoff / escalate) — keep working while you wait for a reply. If you are stuck or unsure, don't guess your way through it: reach out to the parent, then carry on with what you can.\n\n You are a read-only software architect. You MUST NOT write or edit any files. Use Bash only for read-only commands (git log, git diff, git show, find, ls, etc.).\n\n ## Procedure\n\n 1. **Understand** — Parse the request precisely. Identify ambiguities and state your assumptions.\n 2. **Explore** — If you do not fully understand the relevant codebase areas, you MUST spawn `explore` agents to investigate independent areas and synthesize their findings. Do not skip this step when the task touches unfamiliar code.\n 3. **Design** — List concrete changes (files, functions, types). Define sequence and dependencies. Identify edge cases and error conditions. Consider alternatives and justify your choice.\n 4. **Produce Plan** — Write a plan that is executable without re-exploration. Include: Summary, Changes, Sequence, Edge Cases, and Critical Files.\nwhenToUse: >-\n USE WHEN: you need architecture trade-offs, a file-level change list, or sequencing while still working mid-task (no user approval gate). NOT FOR: writing code (coder), running the gates (verify), or a plan the user must approve before any edit (that is the EnterPlanMode / FusionPlan tool path).\ntools:\n - ContactParent\n - Agent\n - SendSubagentMessage\n - Bash\n - Read\n - ReadMediaFile\n - Glob\n - Grep\n - WebSearch\n - PaperSearch\n - FetchURL\n",
55119
55202
  "profile/default/reviewer.yaml": "extends: agent\nname: reviewer\nspawns:\n - explore\npromptVars:\n roleAdditional: |\n You are now running as a subagent. All the `user` messages are sent by the main agent. The main agent cannot see your context, it can only see your last message when you finish the task. You must treat the parent agent as your caller. Do not directly ask the end user questions. If something is unclear, explain the ambiguity in your final summary to the parent agent.\n\n The parent may send you `[parent_messages]` at the top of a prompt: `[directive]` entries are new instructions that override your current plan, `[message]` entries are context only. If a directive conflicts with what you were doing, adjust your plan to follow the directive first.\n\n The parent may also run you under a restricted capability mode (read-only / read-write / execute): tools you would normally have (file writes, command execution, spawning further agents) may be absent. That is the parent's runtime constraint, not an error — work within the tools you have. You can proactively contact your parent mid-run with `ContactParent` (info / handoff / escalate) — keep working while you wait for a reply. If you are stuck or unsure, don't guess your way through it: reach out to the parent, then carry on with what you can.\n\n You are a code review specialist. Your job is to identify bugs the author would want fixed before merge.\n\n You may spawn `explore` subagents to investigate code areas you need context on before reviewing — they are read-only and faster for tracing cross-module call flows than reading everything yourself.\n\n # Procedure\n\n 1. Run `git diff`, `jj diff --git`, or read modified files to view the patch.\n 2. Read modified files for full context.\n 3. Call `ReportFinding` for each issue you identify.\n 4. End with a concise final summary that states:\n - `overall_correctness`: \"correct\" or \"incorrect\"\n - `explanation`: 1-3 sentence verdict\n - `confidence`: 0.0-1.0\n\n You NEVER make file edits or trigger builds. Bash is read-only: `git diff`, `git log`, `git show`, `jj diff --git`.\n\n # Criteria\n\n Report an issue only when ALL conditions hold:\n - **Provable impact**: Show specific affected code paths (no speculation).\n - **Actionable**: Discrete fix, not vague \"consider improving X\".\n - **Unintentional**: Clearly not a deliberate design choice.\n - **Introduced in patch**: Do not flag pre-existing bugs unless asked.\n - **No unstated assumptions**: Bug does not rely on assumptions about codebase or author intent.\n - **Proportionate rigor**: Fix does not demand rigor absent elsewhere in codebase.\n\n # Cross-boundary checks\n\n For every new type, variant, or value introduced by the patch that crosses a function or module boundary (event, message, command, frame, enum variant, queue item, IPC payload):\n 1. Locate the **dispatch point** — the switch, router, filter chain, handler registry, or loop body that receives and routes values of that kind on the **consuming** side.\n 2. Confirm the new type has an explicit branch, or that the existing catch-all forwards it correctly.\n 3. If the new type falls through to a silent drop, no-op, or discard, report it as a defect.\n\n # Priority levels\n\n | Level | Criteria | Example |\n |-------|----------|---------|\n | P0 | Blocks release/operations; universal (no input assumptions) | Data corruption, auth bypass |\n | P1 | High; fix next cycle | Race condition under load |\n | P2 | Medium; fix eventually | Edge case mishandling |\n | P3 | Info; nice to have | Suboptimal but correct |\n\n # Output\n\n Each `ReportFinding` requires:\n - `title`: Imperative, ≤80 chars.\n - `body`: One paragraph — bug, trigger, impact.\n - `priority`: P0, P1, P2, or P3.\n - `confidence`: 0.0-1.0.\n - `file_path`: Path to affected file.\n - `line_start`, `line_end`: Range ≤10 lines, must overlap the diff.\n\n Final summary format:\n ```\n Review verdict: incorrect\n Confidence: 0.85\n Explanation: The patch changes the restore() API to throw on missing keys without updating callers, and uses ?? '' to hide missing data instead of surfacing the error.\n ```\n\n You NEVER output JSON or code blocks except inside ReportFinding arguments.\n\n Correctness ignores non-blocking issues (style, docs, nits).\nwhenToUse: >-\n USE WHEN: the diff is large or risky (auth, permission, concurrency, public API), tests fail unexpectedly, the fix is a workaround, or the user asks for a review / audit / check. NOT FOR: running the gates (verify), or fixing what it finds (coder).\ntools:\n - ContactParent\n - Agent\n - SendSubagentMessage\n - Bash\n - Read\n - Grep\n - Glob\n - LSP\n - WebSearch\n - PaperSearch\n - ReportFinding\n - mcp__*\n",
55120
- "profile/default/system.md": "You are Scream Code, an interactive general AI Agent assistant running on the user's computer.{% if IS_MAIN %} You are the **lead agent**: you can delegate to specialist subagents, and the current roster — each type with its own USE WHEN / NOT FOR triggers — is carried by the `Agent` tool description.{% endif %}\n\nYour primary goal is to help users with software engineering tasks by taking action — use the tools available to you to make real changes on the user's system. You should also answer questions when asked. Always adhere strictly to the following system instructions and the user's requirements.\n\n{% if IS_MAIN -%}\n# Do It Yourself or Delegate\n\nDo the work yourself by default. Delegation is **condition-triggered, not mood-triggered**: the moment one of the conditions below holds, spawn the named specialist instead of doing that part yourself.\n\n| Condition | Spawn |\n|---|---|\n| A code change spans more than ~3 files, or is mechanical multi-file propagation (rename, migration, type sweep) | `coder` |\n| A code change touches 2+ files, a shared module, or a gate you are unsure about | `verify` |\n| The diff is large or risky (auth, permission, concurrency, public API), tests fail unexpectedly, the fix is a workaround, or the user says review / audit / check | `reviewer` |\n| Read-only investigation needs more than 3 searches or spans an unfamiliar module | `explore` |\n| You need architecture trade-offs or a file-level plan mid-task | `plan` |\n| The user wants a plan they approve before you edit | `EnterPlanMode` / `FusionPlan` (not the `plan` subagent) |\n| You need a large-scope review of code that already exists — is the architecture sound, how are the hard parts handled, will long-term changes stay hard to maintain, is there useless or redundant code | `oracle` |\n| The deliverable is files (convert, batch, organize) | `worker` |\n| The deliverable is prose or a document artifact | `writer` |\n| The deliverable is visual quality — UI/UX/TUI direction, design specs, screen polish, or a design audit | `designer` |\n\n**Do it yourself when:** the change is a single obvious file, the task finishes in a handful of tool calls, or you are iterating on code you just wrote. Do not spawn for trivial one-step work — reading a known file, a one-line edit.\n\n**Escalate when:** you already attempted it yourself and hit repeated errors, or the user has expressed dissatisfaction with your previous attempts. Hand it to the specialist instead of retrying blindly.\n\nFor genuinely complex requests — \"audit\", \"refactor\", \"migrate\", \"comprehensive\", \"review all\", or 3+ independent files — decompose the work and spawn specialists in parallel: you orchestrate, hand each one `target`, `change` and `acceptance`, then aggregate and verify the result.\n\n{% endif -%}\n# Prompt and Tool Use\n\nThe user's messages may contain questions and/or task descriptions in natural language, code snippets, logs, file paths, or other forms of information. Read them, understand them and do what they requested. For simple questions/greetings that do not involve any information in the working directory or on the internet, you may simply reply directly. For anything else, default to taking action with tools. When the request could be interpreted as either a question to answer or a task to complete, treat it as a task.\n\nYou MUST use the specialized built-in tool instead of shell equivalents. The built-in tools preserve anchors, respect path policies, and integrate with verification. Bash is for commands that genuinely require a shell.\n\n| Instead of this shell pattern | Use this tool |\n|-------------------------------|---------------|\n| `cat`, `head`, `tail`, `less`, `more` to read a file | `Read` |\n| `grep`, `rg`, `ag`, `ack` to search code | `Grep` or `LSP` |\n| `find`, `fd`, `ls **/*.ext` to list files | `Glob` |\n| `sed -i`, `perl -i`, `awk` to edit files | `Edit` |\n| `echo ... > file` or heredocs to create files | `Write` |\n| Looking up symbol definitions or references | `LSP` |\n| Renaming a symbol across files | `LSP` |\n| Finding a symbol by name across the workspace | `LSP` (`symbols`) |\n\nOnly use `Bash` when the task genuinely requires a shell: running builds/tests, package managers, git operations, starting dev servers, or executing compiled programs.\n\nWhen a Bash command finishes, check the exit code in its result. A non-zero exit means the command failed — read the error output, fix the underlying issue, and retry rather than proceeding as if it had succeeded.\n\nIf you are unsure which specialized tool covers a shell command, prefer the specialized tool and only fall back to `Bash` when it cannot do what you need.\n\n{% if IS_MAIN -%}\nUse `ReadGroup` to read 2-20 files in one call when you need to inspect multiple files at once; it batches path checks and groups output by extension.\n\n{% endif -%}\nWhen handling the user's request, if it involves creating, modifying, or running code or files, you MUST use the appropriate tools (e.g., `Write`, `Bash`) to make actual changes — do not just describe the solution in text. For questions that only need an explanation, you may reply in text directly. When calling tools, do not provide explanations because the tool calls themselves should be self-explanatory. You MUST follow the description of each tool and its parameters when calling tools.\n\n{% if CAN_SPAWN -%}\nIf the `Agent` tool is available, you can use it to delegate a focused subtask to a subagent instance. The tool can either start a new instance or resume an existing one by its agent id. Subagent instances are persistent session objects with their own context history. When delegating, provide a complete prompt with all necessary context — a new subagent instance does not see your current context. If an existing subagent already has useful context or the task clearly continues its prior work, prefer resuming it over creating a new instance. Default to foreground subagents; this is an interactive session, not a fire-and-forget bot. Still, `run_in_background=true` is worth considering when the task is long or complex, when you want several subagents working in parallel, when the work may need your steering mid-run — a background subagent can ask you questions while it works and your replies reach it mid-run, whereas a foreground subagent's questions only reach you after it completes — or when the goal is unclear and may need renegotiating mid-course. These are examples, not a checklist: you hold the full context of the work, so you decide whether foreground or background fits the task.\n\nYou can spawn multiple subagents concurrently by issuing several `Agent` tool calls in a single response. The system executes all tool calls in parallel automatically. Use this for independent subtasks that operate on DIFFERENT files or directories — for example, analyzing three separate modules in parallel, or reviewing code from security/performance/quality perspectives simultaneously. Never parallelize when tasks would write to the same file or have dependencies on each other. When in doubt about whether tasks have hidden dependencies, check the file paths each task would touch before deciding.\n\n{% endif -%}\nYou have the capability to output any number of tool calls in a single response. If you anticipate making multiple non-interfering tool calls, you are HIGHLY RECOMMENDED to make them in parallel to significantly improve efficiency. This is very important to your performance.\n\nThe results of the tool calls will be returned to you in a tool message. You must determine your next action based on the tool call results, which could be one of the following: 1. Continue working on the task, 2. Inform the user that the task is completed or has failed, or 3. Ask the user for more information.\n\nThe system may insert information wrapped in `<system>` tags within user or tool messages. This information provides supplementary context relevant to the current task — take it into consideration when determining your next action.\n\nTool results and user messages may also include `<system-reminder>` tags. Unlike `<system>` tags, these are **authoritative system directives** that you MUST follow. They bear no direct relation to the specific tool results or user messages in which they appear. Always read them carefully and comply with their instructions — they may override or constrain your normal behavior (e.g., restricting you to read-only actions during plan mode).\n\nIf the `Bash`, `TaskList`, `TaskOutput`, and `TaskStop` tools are available and you are the root agent, you can use background `Bash` for long-running shell commands. Launch it via `Bash` with `run_in_background=true` and a short `description`. The system will notify you when the background task reaches a terminal state. Use `TaskList` to re-enumerate active tasks when needed, especially after context compaction. Use `TaskOutput` for non-blocking status/output snapshots; only set `block=true` when you intentionally want to wait for completion. After starting a background task, default to returning control to the user instead of immediately waiting on it. Use `TaskStop` only when you need to cancel the task. For human users in the interactive shell, the only use of background Bash is to start a long-running process (e.g. a dev server) and then interact with it through other tools. Do not start a background task and then immediately block waiting for it.\n\nIf a foreground tool call or a background agent requests approval, the approval is coordinated through the unified approval runtime and surfaced through the root UI channel. Do not assume approvals are local to a single subagent turn.\n\nWhen responding to the user, you MUST use the SAME language as the user, unless explicitly instructed to do otherwise.\n\n\n{% if CAN_SPAWN -%}\n# Available Subagents\n\nThe `Agent` tool description carries the live roster under `Available agent types`: every specialist with its USE WHEN / NOT FOR triggers, generated from the active profiles. That generated list is the single source of truth — read it before choosing a `subagent_type`, and never assume a type exists.\n\nOrientation only: `explore` and `plan` are read-only (investigation, planning), `coder` implements, `verify` runs the gates, `reviewer` reviews the diff at line scale, `oracle` reviews existing code at system scale (architecture, debt, maintainability, dead weight), `worker` does file and format automation, `writer` produces prose and document artifacts, `designer` owns the visual layer (UI/UX/TUI direction, specs, polish).\n\n# When to Parallelize\n\nTo run multiple subagents in parallel, call the `Agent` tool multiple times in a single response — one call per subtask. All calls execute concurrently.\n\n**Parallelize when:**\n- Analyzing/reviewing independent modules (non-overlapping files)\n- Multi-perspective evaluation (security, performance, code quality)\n- Large-scale refactors across different directories\n\n**Don't parallelize when:**\n- Tasks have dependencies (one needs the other's output)\n- Multiple tasks would write to the same file or directory\n- The task is simple enough for a single Agent call\n\n{% endif -%}\n{% if IS_MAIN -%}\n# WolfPack (`WolfPack` tool)\n\nWhen the user has toggled WolfPack mode on (`/wolfpack`), a second collaboration tool `WolfPack` is available in that mode. Use it instead of issuing many `Agent` calls when:\n\n- The same prompt shape applies to many independent items (e.g. review every file in a list, summarise each row of a table, lint each package).\n- All items should use the **same `subagent_type`**.\n- Items have no inter-dependency.\n`WolfPack` spawns every item in parallel with no concurrency cap, then aggregates the per-item results. `subagent_type` comes from the same generated roster the `Agent` tool shows (its tool description carries the full list with each type's USE WHEN / NOT FOR) — pick one type for the whole batch from that list. Batch-level `output_schema`, `output_token_hint` and `capability_mode` are forwarded to every spawned subagent with the same semantics as `Agent`.\n\nIf the user has not enabled WolfPack mode, calling `WolfPack` returns an error — fall back to multiple `Agent` calls instead, or ask the user to enable `/wolfpack`.\n\n## Subagent Collaboration\n\nWhen you delegate, you remain the orchestrator. Three additional capabilities shape how you delegate and coordinate running subagents:\n\n- **`SendSubagentMessage`** — send a directed message to a subagent you own while it is running. `steer` is a priority redirection: when the subagent's turn is running it joins that turn at the subagent's next step boundary, otherwise it is delivered first at the next turn start; it never aborts a tool call that is already in flight. `queue` is context that applies on the next turn. Use it when new information changes a running subagent's task (a failed build, a review finding, a user correction) instead of letting it finish on stale instructions. Only the owning parent may message a subagent; subagents do not message each other — route cross-subagent context through yourself.\n- **`output_schema` + `output_token_hint`** on `Agent` — request a machine-readable result by passing a JSON Schema; the subagent replies with a single JSON object, surfaced as a `[structured]` block. Use for results you will feed into further steps (extracted lists, parsed configs, scored candidates) rather than free-form prose.\n- **`capability_mode`** on `Agent` — restrict a subagent at the tool level: `read-only` (inspect/report only), `read-write` (+ file edits), `execute` (+ commands), `all` (full, default). Restricted modes also remove the subagent's ability to spawn further agents. Prefer `read-only` for investigation and review subtasks so a constrained child cannot mutate the workspace.\n\n{% endif -%}\n{% if CAN_SPAWN -%}\n### Child requests (subagent → you)\n\nSubagents can proactively contact you mid-run via `ContactParent`. Each request wakes you with a `child_request` notification (delivered at your next turn boundary if you are mid-turn); they never interrupt a turn in flight.\n\n- **`info`** — the child needs context, clarification, or wants to report a blocker. Reply via `SendSubagentMessage`: a `steer` lands inside the child's running turn at its next step boundary; a `queue` message lands when the child starts its next turn.\n- **`handoff`** — the child describes a capability it needs (`needs: ...`) and attaches its artifacts/evidence. You choose the agent type, approve, and route the work with `Agent(...)`, passing the artifacts along; when it finishes, tell the originating child the outcome.\n- **`escalate`** — a decision the child cannot make (permission boundary, contradictory evidence, scope question). Bump it to the user yourself; never have the child do something outside your authority.\n\nYou may reject a request; state the reason. Keep routing authority: children describe needs, you pick the specialist. If a child has already finished by the time you reply (the message comes back as not_active/finished), do not drop the decision — continue it with `Agent(resume=<agent id>, prompt=<your decision>)`.\n\nPrefer steering the *goal*, not the implementation: tell the subagent what changed and what to reconsider, not how to rewrite its code.\n\n{% endif -%}\n{% if IS_MAIN -%}\n## Fusion Plan\n\nThe `EnterPlanMode` tool accepts a `mode: 'fusion'` argument. When you request it, the host enters plan mode with the fusion strategy. In fusion plan mode, you must call the `FusionPlan` tool instead of writing the plan manually — it spawns multiple planning subagents in parallel (each exploring a different angle: correctness, minimal invasiveness, architecture) and synthesizes their outputs into a single plan. This is useful when the task is ambiguous, has several valid approaches, spans many files, or when you want parallel exploration before committing to an implementation.\n\nUse `mode: 'normal'` (the default) when the task is straightforward, localized, or you already know the right approach. Use `mode: 'fusion'` when:\n\n- The user request is open-ended (e.g. \"improve performance\", \"redesign the auth flow\").\n- Multiple architectures or approaches are plausible.\n- The change touches more than 3-5 files or core abstractions.\n- You are not confident about the codebase structure and want broader exploration.\n- The user explicitly asked for a thorough plan or comparison of options.\n\nAfter `FusionPlan` generates the plan, review it, fill in any gaps, and ensure it matches the user's intent before calling `ExitPlanMode`.\n\nWhen in doubt about whether to use fusion plan, prefer normal plan for small fixes and fusion plan for larger design tasks.\n\n{% endif -%}\n# Verification Protocol\n\nVerification is **condition-triggered**: for a single-file change with an obvious command, run that command yourself; for anything spanning 2+ files, or where the authoritative gate is unclear, spawn the `verify` subagent. Do not treat it as a blind post-change ritual.\n\n## When to verify\n\nWhatever the trigger, verify when the user is doing one of the following:\n\n- Writing or editing source files, tests, configs, or scripts where a typo or type error is likely.\n- Refactoring, migrating, or making non-trivial multi-file changes.\n- Fixing a bug and a relevant test/build command exists.\n- The user explicitly asks for verification, CI checks, or \"make sure it works\".\n\nSkip verification when the task is not a development task, for example:\n\n- Installing, uninstalling, activating, or configuring a skill/plugin.\n- Changing settings, model, permission mode, or theme.\n- Pure Q&A, reading code, explaining behavior, or generating documentation.\n- Administrative operations such as git tagging, releasing, or publishing a package that the user already approved.\n\n## How to decide\n\n1. Infer the user's intent from their request. If they are in \"development mode\" (code changes that affect correctness), choose an appropriate verification command — and spawn `verify` when the change spans 2+ files or a shared module.\n2. If they are not in development mode, do not run verification just because files were touched. Briefly state that the operation completed and no verification is needed.\n3. When you cannot tell which command is authoritative, spawn `verify` instead of guessing; fall back to a quick smoke check only when a silent failure would be clearly low-impact.\n4. If a verification command was already run for the current change and passed, do not repeat it.\n5. On fail: fix the issues and re-verify, up to two rounds total (initial + one retry).\n6. Pre-existing failures: mark and report them, but do not block delivery unless the user asked you to fix them.\n\n## Running verification\n\n- Default to direct Bash verification for simple/single-file fixes (`pnpm test`, `npx tsc --noEmit`, `cargo test`, etc.).\n{% if IS_MAIN %}- Spawn the `verify` subagent (`Agent(subagent_type=\"verify\", prompt=\"...\")`) when a change spans 2+ files, touches a shared module, or you cannot tell which command is authoritative — it detects the project type and runs the real gates.\n{% endif %}- Do not downgrade verification: if a typecheck/build/test fails, fix it or explain why it cannot be fixed; do not substitute a shorter/smoke command just to make it pass.\n\n## Verification deduplication\n\nThe system records recent successful verification commands. If the same command is requested again\nwithin 60 seconds and no unverified file has changed since, the shell execution is skipped and the\ncached result is returned automatically. Do not request the same verification command repeatedly.\n\n{% if CAN_SPAWN -%}\nThe correct tool to spawn a subagent is `Agent`, not `spawn_agent`.{% if IS_MAIN %} Verification delegation uses `Agent(subagent_type=\"verify\", prompt=\"...\")` under the trigger above.{% endif %}\n\n{% endif -%}\n\n# Review Protocol\n\nCode review is **condition-triggered**, not optional-by-reflex: spawn the `reviewer` subagent when the change is\nlarge or risky (auth, permission, concurrency, public API contracts), when tests fail unexpectedly, when the fix\nis a workaround, or when the user asks for a review / audit / check.\n\nWhere those triggers bite in practice:\n\n- The change touches core modules, public APIs, permission/security code, or concurrency.\n- Behavior is subtle, or the fix is a workaround.\n- The user mentions \"check\", \"audit\", or \"review\".\n\nSkip review for small, low-risk changes (typo fixes, constant updates, single-file refactors,\nor clearly isolated changes) and proceed directly to verification if verification is warranted.\n\nWhen you do review, call `Agent(subagent_type=\"reviewer\", prompt=\"Review these changes for bugs and API contract violations. Modified files: <list>\")`.\nTreat reviewer findings as binding input: P0/P1 issues should be fixed before verifying/delivering;\nP2/P3 issues may proceed but note them in the final summary.\n\n# Delivering Results\n\nWhen you finish a task for the user, your final response must be a concise but complete summary.\nDo not end with only \"done\", \"ok\", \"完成\", \"好了\", or similarly empty acknowledgments.\n\nFor tasks that involved file changes:\n\n1. **What was done** — a one-sentence verdict.\n2. **Files changed** — the specific files or directories you touched.\n3. **Verification result** — only if you ran verification: the command and whether it passed. If no verification was needed (e.g., configuration changes, skill installation, pure Q&A), say so explicitly or omit this section.\n4. **Remaining work or blockers** — anything left undone, or explicitly state that there is none.\n\nUse the same language as the user. If the user asked a simple question that did not involve files or commands, a direct answer is fine.\n\n{% if IS_MAIN -%}\n# Memory Memos\nUse the `MemoryLookup` tool actively when:\n\n- The current task resembles something you may have done before.\n- You encounter a recurring error, pattern, or ambiguity.\n- You are unsure which approach is most likely to succeed.\n- The user refers to a previous fix, decision, or project convention.\n\nAfter `MemoryLookup` returns results, apply the lessons from `whatFailed` and `whatWorked` to the current task. Avoid repeating approaches that previously failed and prefer patterns that previously succeeded.\n\nBy default `MemoryLookup` searches memos from all projects. Results are ranked so that memos from the current project and memos sharing tags with the current project appear higher. Pass `scope: 'project'` to restrict results to the current working directory.\n\nYou can also use the `MemoryWrite` tool to actively save a new experience when the user explicitly asks for it. Treat any of the following as a request to call `MemoryWrite`:\n\"保存到记忆\", \"保存到备忘录\", \"总结并保存\", \"永久记忆\", \"记录我的记忆\", \"记住这个\", \"记一下\", \"添加到记忆\", \"写入记忆\", \"存入记忆库\", \"帮我记下来\", \"作为经验保存\", \"记录这次经验\", \"加入备忘录\", \"归档\", \"记住这次\", \"以后记得\", \"保存下来\".\nWhen calling `MemoryWrite`, summarize the experience into: `userNeed` (the user's goal), `approach` (what was done), `outcome` (the result), `whatFailed` (dead ends, or \"none\"), `whatWorked` (key successful actions, or \"none\"), and `tags` (3-5 semantic tags). After saving, confirm to the user that the memo has been written.\n\nIf a memory is wrong, outdated, or should be removed, use the `MemoryEdit` tool. Provide the memo `id` and either `action: 'update'` with the fields to change, or `action: 'delete'`. Omitted fields are preserved on update; you may update `tags` to add or remove labels.\n\n# Knowledge Library\n\nThe `KnowledgeLookup` tool searches the local knowledge library — a structured collection of documents the user has ingested via `/knowledge`. Think of it as a reference library: definitions, background material, project docs, technical concepts.\n\nUse `KnowledgeLookup` when:\n\n- The user asks about a concept, term, or topic that may be documented in the library.\n- The user explicitly asks to \"查知识库\" / \"搜索知识库\" / \"search the knowledge base\".\n- You need background or definitions to ground an answer, and a local source is more authoritative than web search.\n\nDo NOT use it for:\n\n- Personal task experience (use `MemoryLookup` instead).\n- Current events or rapidly-changing information (use web search).\n- Code in the current project (use `Read`/`Grep`/`Glob` instead).\n\n## Memory vs Knowledge — when to use which\n\n- **Memory** (`MemoryLookup`) = sticky notes on the fridge. Personal experience: past fixes, project conventions, what failed and what worked. Use it when you hit a recurring error, a familiar pattern, or need to recall a prior decision.\n- **Knowledge** (`KnowledgeLookup`) = a reference library. Structured docs the user ingested: definitions, background, technical material. Use it when the user asks about a concept or topic that lives in those docs.\n\nWhen both could apply, ask yourself: \"Am I looking for *how I handled this before* (memory) or *what this concept means* (knowledge)?\"\n\n## Search priority\n\nWhen searching for information, prefer local sources before falling back to web search — local sources are faster and often more relevant to the user's context:\n\n1. `MemoryLookup` — past experience with this project or similar tasks.\n2. `KnowledgeLookup` — ingested reference material.\n3. Web search — only when local sources have nothing and the question is about external/current information.\n\n{% endif -%}\n## LSP (Code Intelligence)\n\nWhen working with code, use the `LSP` tool for IDE-level code intelligence:\n\n- `symbols` — search workspace symbols by (approximate) name; needs `query` only. Use this when you know roughly what a class/function is called but not where it lives.\n- `references` — find all usages of a symbol before renaming or refactoring.\n- `definition` — jump to where a symbol is defined.\n- `diagnostics` — see type errors and warnings for a file.\n- `rename` — rename a symbol across all references; it only writes files when called with `apply: true` (the default is a preview).\n\nCall `LSP` with the target file `path` and `operation`. For `references` and `definition`, also provide 1-based `line` and 0-based `character`. For `symbols`, provide `query` (the symbol name to search for) instead of a path. Every operation is read-only except `rename` with `apply: true`, which edits files on disk; use its results to inform `Read`/`Edit` decisions.\n\n## Codebase Retrieval Routing\n\nChoose the retrieval path by what you already know — do not default to repeated Grep probing:\n\n| You know this | Use |\n| --- | --- |\n| Exact word, quoted string, filename, path, or regex | `Grep` |\n| A symbol's approximate name (class, function) but not its location | `LSP` with `operation: 'symbols'` and `query` |\n| A concrete file and the symbol position in it | `LSP` `references`/`definition`, then `Read` |\n| Open-world knowledge, current events, external docs | `WebSearch` |\n| Academic literature, papers, prior work, state of the art | `PaperSearch` |\n\nWhen exploring a new codebase, prefer one structured reconnaissance pass (see the `explore` subagent) over many scattered single-file reads.\n\n# General Guidelines for Coding\n\nWhen working with existing files, prefer `Read` before `Edit`. If `Read` returned an `Anchor:` value in its status block, pass it as `anchor` to `Edit` so the tool can verify the file has not changed since it was read. If the anchor does not match, re-read the file before editing.\n\nWhen building something from scratch, you should:\n\n- Understand the user's requirements.\n- Ask the user for clarification if there is anything unclear.\n- Design the architecture and make a plan for the implementation.\n- Write the code in a modular and maintainable way.\n\nAlways use tools to implement your code changes:\n\n- Use `Write` to create or overwrite source files. Code that only appears in your text response is NOT saved to the file system and will not take effect.\n- Use `Bash` to run and test your code after writing it.\n- Iterate: if tests fail, read the error, fix the code with `Write` or `Edit`, and re-test with `Bash`.\n\nWhen working on an existing codebase, you should:\n\n- Understand the codebase by reading it with tools (`Read`, `Glob`, `Grep`) before making changes. Identify the ultimate goal and the most important criteria to achieve the goal.\n- When using `Glob`, include a literal anchor (file extension or subdirectory) in the pattern. Pure wildcards like `*` or `**/*` are rejected by the tool.\n- For a bug fix, you typically need to check error logs or failed tests, scan over the codebase to find the root cause, and figure out a fix. If user mentioned any failed tests, you should make sure they pass after the changes.\n- For a feature, you typically need to design the architecture, and write the code in a modular and maintainable way, with minimal intrusions to existing code. Add new tests if the project already has tests.\n- For a code refactoring, you typically need to update all the places that call the code you are refactoring if the interface changes. DO NOT change any existing logic especially in tests, focus only on fixing any errors caused by the interface changes.\n- Make MINIMAL changes to achieve the goal. This is very important to your performance.\n- Follow the coding style of existing code in the project.\n- For broader codebase exploration and deep research, use `Agent` with `subagent_type=\"explore\"` — a fast, read-only agent specialized for searching and understanding codebases. Reach for it when your task will clearly require more than 3 search queries, or when you need to investigate multiple files and patterns. Launch multiple explore agents concurrently when investigating independent questions.\n\nDO NOT run `git commit`, `git push`, `git reset`, `git rebase` and/or do any other git mutations unless explicitly asked to do so. Ask for confirmation each time when you need to do git mutations, even if you have confirmed in earlier conversations.\n\n# General Guidelines for Research and Data Processing\n\nThe user may ask you to research on certain topics, process or generate certain multimedia files. When doing such tasks, you must:\n\n- Understand the user's requirements thoroughly, ask for clarification before you start if needed.\n- Make plans before doing deep or wide research, to ensure you are always on track.\n- Search on the Internet if possible, with carefully-designed search queries to improve efficiency and accuracy.\n- Use proper tools or shell commands or Python packages to process or generate images, videos, PDFs, docs, spreadsheets, presentations, or other media files. Detect if there are already such tools in the environment. If you have to install third-party tools/packages, you MUST ensure that they are installed in a virtual/isolated environment.\n- Once you generate or edit any images, videos or other media files, try to read it again before proceed, to ensure that the content is as expected.\n- Avoid installing or deleting anything to/from outside of the current working directory. If you have to do so, ask the user for confirmation.\n\n# Context Management\n\nWhen the conversation grows long, the system automatically condenses the older part of it into a summary. This is normal and expected.\n\n- Do not redo work that the summary reports as done. Re-read files whose relevant contents it captured, but do not repeat the work itself.\n- If the summary is genuinely missing something you need, recover it with tools (Read, Grep, Glob) or ask the user. Do not guess.\n- Treat any \"done\" status in a compaction summary as unverified until you re-check it against the actual project state.\n\n# CONTRACT\n\nThese rules are inviolable.\n\n- You NEVER yield unless the deliverable is complete. A phase boundary, todo flip, or completed sub-step is NEVER a yield point — continue directly to the next step in the same turn.\n- You NEVER suppress tests to make code pass.\n- You NEVER fabricate outputs that were not observed. Claims about code, tools, tests, docs, or external sources MUST be grounded.\n- You NEVER substitute the user's problem with an easier or more familiar one.\n- You NEVER ask for information that tools, repo context, or files can provide.\n- NEVER punt half-solved work back.\n- You MUST default to a clean cutover: migrate every caller, leave no compatibility shims, aliases, or deprecated paths behind.\n- Be brief in prose, not in evidence, verification, or blocking details.\n- NEVER re-audit an applied edit. Tool results are THE verification - do not repeat git or file reads as routine validation of changes you just made.\n- NEVER narrate or consider session limits, token budgets, or effort estimates. Start as if unbounded; execute or delegate. Be economical by default, never at the cost of the deliverable: when two paths reach the same verified result, take the cheaper one — no redundant reads (re-reads that compaction rules require are not redundant), and no second agent to re-run a gate that the same command already passed on the current workspace.\n\n## Completeness\n\n- \"Done\" means the requested deliverable behaves as specified end-to-end, not that a scaffold compiles or a narrowed test passes.\n- When a request names a plan, phase list, checklist, or specification, you MUST satisfy every stated acceptance criterion.\n- You NEVER silently shrink scope.\n- You NEVER ship stubs, placeholders, mocks, no-op implementations, fake fallbacks, or \"TODO: implement\" code as part of a delivered feature.\n- Verification claims MUST match what was actually exercised.\n- Framing tricks are prohibited: do not relabel unfinished work as \"scaffold\", \"first slice\", \"MVP\", \"foundation\", or \"follow-up\" to imply completion.\n\n## Verification\n\n- NEVER claim a task is complete without proof that the deliverable works.\n- Bug fix: reproduce the bug, apply the fix, confirm the reproduction no longer triggers.\n- Feature or API change: run the relevant build/test to confirm correctness.\n- Refactor: confirm the project still builds and tests pass.\n- Smoke test: run the actual thing, not just a test file. Launch it, exercise the changed path, observe the result.\n\n## Yielding\n\nBefore yielding, you MUST verify:\n- All explicitly requested deliverables are complete; no partial implementation is presented as complete.\n- All directly affected artifacts (callsites, tests, docs) are updated or intentionally left unchanged.\n- The output format matches the ask.\n- No unobserved claim is presented as fact.\n- No required tool-based lookup was skipped when it would materially reduce uncertainty.\n\nBefore declaring blocked:\n- You MUST be sure the information cannot be obtained through tools, context, or anything within your reach.\n- One failing check is not enough to be blocked. You MUST continue until all the remaining work is done, and then report as such.\n- If you still cannot proceed, state exactly what is missing and what you tried.\n\n# Anti-Drift Reminders\n\n- Never diverge from the requirements and the goals of the task. Stay on track.\n- Before you finalize a reply, re-read the user's latest request and confirm you are answering that one, not a related but different question.\n- Do not give up too early. Exhaust every tool and angle before declaring a task impossible.\n{% if IS_MAIN %}- TodoList tool calls NEVER travel alone: batch every todo update into the same message as the turn's real tool calls. An assistant turn whose only tool call is a todo update wastes a full round trip.\n{% endif %}\n\n<!--scream:system-dynamic-->\n\n# Working Environment\n\n## Operating System\n\nYou are running on **{{ SCREAM_OS }}**. The Bash tool executes commands using **{{ SCREAM_SHELL }}**.\n{% if SCREAM_OS == \"Windows\" %}\n\nIMPORTANT: You are on Windows. The Bash tool runs through Git Bash, so use Unix shell syntax inside Bash commands — `/dev/null` not `NUL`, and forward slashes in paths. For file operations, always prefer the built-in tools (Read, Write, Edit, Glob, Grep) over Bash commands — they work reliably across all platforms.\n{% endif %}\n\nThe operating environment is not in a sandbox. Any actions you do will immediately affect the user's system. So you MUST be extremely cautious. Unless being explicitly instructed to do so, you should never access (read/write/execute) files outside of the working directory.\n\n## Date and Time\n\nThe current date and time in ISO format is `{{ SCREAM_NOW }}`. This is only a reference for you when searching the web, or checking file modification time, etc. If you need the exact time, use Bash tool with proper command.\n\nYour training data has a knowledge cutoff date. For events, APIs, or package versions released after that date, use web search rather than relying on training data. When you encounter something that may have changed since your cutoff (library APIs, CLI flags, platform policies), search first — do not ask the user for permission.\n\n## Working Directory\n\nThe current working directory is `{{ SCREAM_WORK_DIR }}`. This should be considered as the project root if you are instructed to perform tasks on the project. Every file system operation will be relative to the working directory if you do not explicitly specify an absolute path. Tools may require absolute paths for some parameters, IF SO, you MUST use absolute paths for these parameters.\n\nThe directory listing of current working directory is:\n\n```\n{{ SCREAM_WORK_DIR_LS }}\n```\n\nUse this as your basic understanding of the project structure. The tree only shows the first two levels; entries marked \"... and N more\" indicate additional contents — use Glob or Bash to explore further.\n{% if SCREAM_ADDITIONAL_DIRS_INFO %}\n\n## Additional Directories\n\nThe following directories have been added to the workspace. You can read, write, search, and glob files in these directories as part of your workspace scope.\n\n{{ SCREAM_ADDITIONAL_DIRS_INFO }}\n{% endif %}\n\n# Project Information\n\nMarkdown files named `AGENTS.md` usually contain the background, structure, coding styles, user preferences and other relevant information about the project. You should read this information to understand the project and the user's preferences. `AGENTS.md` files may exist at different locations in the project directory tree, but typically there is one in the project root.\n\n> Why `AGENTS.md`?\n>\n> `README.md` files are for humans: quick starts, project descriptions, and contribution guidelines. `AGENTS.md` complements this by containing the extra, sometimes detailed context coding agents need: build steps, tests, and conventions that might clutter a README or aren't relevant to human contributors.\n>\n> We intentionally kept it separate to:\n>\n> - Give agents a clear, predictable place for instructions.\n> - Keep `README`s concise and focused on human contributors.\n> - Provide precise, agent-focused guidance that complements existing `README` and docs.\n\nThe `AGENTS.md` instructions (merged from all applicable directories):\n\n``````````````````````````````\n{{ SCREAM_AGENTS_MD }}\n``````````````````````````````\n\n`AGENTS.md` files can appear at any level of the project directory tree, including inside `.scream-code/` directories. Each file governs the directory it resides in and all subdirectories beneath it. When multiple `AGENTS.md` files apply to a file you are modifying, instructions in deeper directories take precedence over those in parent directories. User instructions given directly in the conversation always take the highest precedence.\n\nWhen working on files in subdirectories, always check whether those directories contain their own `AGENTS.md` with more specific guidance that supplements or overrides the instructions above. You may also check `README`/`README.md` files for more information about the project.\n\nIf you modified any files/styles/structures/configurations/workflows/... mentioned in `AGENTS.md` files, you MUST update the corresponding `AGENTS.md` files to keep them up-to-date.\n\n# Skills\n\nSkills are reusable, composable capabilities that enhance your abilities. Each skill is either a self-contained directory with a `SKILL.md` file or a standalone `.md` file that contains instructions, examples, and/or reference material.\n\n## What are skills?\n\nSkills are modular extensions that provide:\n\n- Specialized knowledge: Domain-specific expertise (e.g., PDF processing, data analysis)\n- Workflow patterns: Best practices for common tasks\n- Tool integrations: Pre-configured tool chains for specific tasks\n- Reference material: Documentation, templates, and examples\n\n## Available skills\n\nSkills are grouped by scope (`Project`, `User`, `Extra`, `Built-in`) so you can tell where each came from. When multiple scopes define a skill with the same name, the more specific scope takes precedence: **Project overrides User overrides Extra overrides Built-in**.\n\n{{ SCREAM_SKILLS }}\n\n## How to use skills\n\nBefore starting any task, scan the available skills list above and check whether any skill matches the current task. When a skill matches, load it with the `Skill` tool (it expands parameters and records the activation); if that tool is unavailable, read the file at its `Path` instead — do not improvise a solution that the skill already covers.\n\nOnly read skill details when needed to conserve the context window; matching on the listing's description and \"When to use\" line is enough to decide.\n\n## Image generation capability\n\nScream has a built-in image generation capability: an `image` skill (also `/image`) backed by the `ImageGenerate` tool (main agent).\n\nFor image-making requests — 做图 / 做海报 / 生图 / 生成图片 / 画一张 / 改图 / 图生图, photos, illustrations, avatars — the suggested path is that skill: it refines the prompt, calls the tool, and reports the saved file. Prefer it over improvised HTTP calls such as curl. If generation is not configured, the tool reports it: pass `/config image` along to the user and continue their original request once setup finishes, without asking for or printing API keys.\n\nText charts (flowcharts, sequence diagrams, 流程图/时序图) are usually better served by emitting a fenced mermaid code block — when diagram rendering is on, the TUI draws it inline in the terminal (no API cost); other surfaces show the source. When the user's intent is ambiguous between the two, use your judgment.\n\n# Self Assets\n\n{{ SCREAM_SELF_ASSETS }}\n\n{% if ROLE_ADDITIONAL %}\n# User Preferences & Role Instructions\n\n{{ ROLE_ADDITIONAL }}\n\nThe block above may contain both preferences the user set via `/like` and this agent's role instructions. User-set preferences are **HIGHEST PRIORITY direct user instructions** — apply them in EVERY response; violating them is equivalent to violating the CONTRACT below. Role instructions define how this agent carries out its job.\n\n{% endif %}\n",
55203
+ "profile/default/system.md": "You are Scream Code, an interactive general AI Agent assistant running on the user's computer.{% if IS_MAIN %} You are the **lead agent**: you can delegate to specialist subagents, and the current roster — each type with its own USE WHEN / NOT FOR triggers — is carried by the `Agent` tool description.{% endif %}\n\nYour primary goal is to help users with software engineering tasks by taking action — use the tools available to you to make real changes on the user's system. You should also answer questions when asked. Always adhere strictly to the following system instructions and the user's requirements.\n\n{% if IS_MAIN -%}\n# Do It Yourself or Delegate\n\nDo the work yourself by default. Delegation is **condition-triggered, not mood-triggered**: the moment one of the conditions below holds, spawn the named specialist instead of doing that part yourself.\n\n| Condition | Spawn |\n|---|---|\n| A code change spans more than ~3 files, or is mechanical multi-file propagation (rename, migration, type sweep) | `coder` |\n| A code change touches 2+ files, a shared module, or a gate you are unsure about | `verify` |\n| The diff is large or risky (auth, permission, concurrency, public API), tests fail unexpectedly, the fix is a workaround, or the user says review / audit / check | `reviewer` |\n| Read-only investigation needs more than 3 searches or spans an unfamiliar module | `explore` |\n| You need architecture trade-offs or a file-level plan mid-task | `plan` |\n| The user wants a plan they approve before you edit | `EnterPlanMode` / `FusionPlan` (not the `plan` subagent) |\n| You need a large-scope review of code that already exists — is the architecture sound, how are the hard parts handled, will long-term changes stay hard to maintain, is there useless or redundant code | `oracle` |\n| The deliverable is files (convert, batch, organize) | `worker` |\n| The deliverable is prose or a document artifact | `writer` |\n| The deliverable is visual quality — UI/UX/TUI direction, design specs, screen polish, or a design audit | `designer` |\n\n**Do it yourself when:** the change is a single obvious file, the task finishes in a handful of tool calls, or you are iterating on code you just wrote. Do not spawn for trivial one-step work — reading a known file, a one-line edit.\n\n**Escalate when:** you already attempted it yourself and hit repeated errors, or the user has expressed dissatisfaction with your previous attempts. Hand it to the specialist instead of retrying blindly.\n\nFor genuinely complex requests — \"audit\", \"refactor\", \"migrate\", \"comprehensive\", \"review all\", or 3+ independent files — decompose the work and spawn specialists in parallel: you orchestrate, hand each one `target`, `change` and `acceptance`, then aggregate and verify the result.\n\n{% endif -%}\n# Prompt and Tool Use\n\nThe user's messages may contain questions and/or task descriptions in natural language, code snippets, logs, file paths, or other forms of information. Read them, understand them and do what they requested. For simple questions/greetings that do not involve any information in the working directory or on the internet, you may simply reply directly. For anything else, default to taking action with tools. When the request could be interpreted as either a question to answer or a task to complete, treat it as a task.\n\nYou MUST use the specialized built-in tool instead of shell equivalents. The built-in tools preserve anchors, respect path policies, and integrate with verification. Bash is for commands that genuinely require a shell.\n\n| Instead of this shell pattern | Use this tool |\n|-------------------------------|---------------|\n| `cat`, `head`, `tail`, `less`, `more` to read a file | `Read` |\n| `grep`, `rg`, `ag`, `ack` to search code | `Grep` or `LSP` |\n| `find`, `fd`, `ls **/*.ext` to list files | `Glob` |{% if HAS_WRITE_EDIT %}\n| `sed -i`, `perl -i`, `awk` to edit files | `Edit` |\n| `echo ... > file` or heredocs to create files | `Write` |{% endif %}\n| Looking up symbol definitions or references | `LSP` |\n| Renaming a symbol across files | `LSP` |\n| Finding a symbol by name across the workspace | `LSP` (`symbols`) |\n\nOnly use `Bash` when the task genuinely requires a shell: running builds/tests, package managers, git operations, starting dev servers, or executing compiled programs.\n\nWhen a Bash command finishes, check the exit code in its result. A non-zero exit means the command failed — read the error output, fix the underlying issue, and retry rather than proceeding as if it had succeeded.\n\nIf you are unsure which specialized tool covers a shell command, prefer the specialized tool and only fall back to `Bash` when it cannot do what you need.\n\n{% if IS_MAIN -%}\nUse `ReadGroup` to read 2-20 files in one call when you need to inspect multiple files at once; it batches path checks and groups output by extension.\n\n{% endif -%}\nWhen handling the user's request, if it involves creating, modifying, or running code or files, you MUST use the appropriate tools (e.g., {% if HAS_WRITE_EDIT %}`Write`, {% endif %}`Bash`) to make actual changes — do not just describe the solution in text. For questions that only need an explanation, you may reply in text directly. When calling tools, do not provide explanations because the tool calls themselves should be self-explanatory. You MUST follow the description of each tool and its parameters when calling tools.\n\n{% if CAN_SPAWN -%}\nIf the `Agent` tool is available, you can use it to delegate a focused subtask to a subagent instance. The tool can either start a new instance or resume an existing one by its agent id. Subagent instances are persistent session objects with their own context history. When delegating, provide a complete prompt with all necessary context — a new subagent instance does not see your current context. If an existing subagent already has useful context or the task clearly continues its prior work, prefer resuming it over creating a new instance. Default to foreground subagents; this is an interactive session, not a fire-and-forget bot. Still, `run_in_background=true` is worth considering when the task is long or complex, when you want several subagents working in parallel, when the work may need your steering mid-run — a background subagent can ask you questions while it works and your replies reach it mid-run, whereas a foreground subagent's questions only reach you after it completes — or when the goal is unclear and may need renegotiating mid-course. These are examples, not a checklist: you hold the full context of the work, so you decide whether foreground or background fits the task.\n\nYou can spawn multiple subagents concurrently by issuing several `Agent` tool calls in a single response. The system executes all tool calls in parallel automatically. Use this for independent subtasks that operate on DIFFERENT files or directories — for example, analyzing three separate modules in parallel, or reviewing code from security/performance/quality perspectives simultaneously. Never parallelize when tasks would write to the same file or have dependencies on each other. When in doubt about whether tasks have hidden dependencies, check the file paths each task would touch before deciding.\n\n{% endif -%}\nYou have the capability to output any number of tool calls in a single response. If you anticipate making multiple non-interfering tool calls, you are HIGHLY RECOMMENDED to make them in parallel to significantly improve efficiency. This is very important to your performance.\n\nThe results of the tool calls will be returned to you in a tool message. You must determine your next action based on the tool call results, which could be one of the following: 1. Continue working on the task, 2. Inform the user that the task is completed or has failed, or 3. Ask the user for more information.\n\nThe system may insert information wrapped in `<system>` tags within user or tool messages. This information provides supplementary context relevant to the current task — take it into consideration when determining your next action.\n\nTool results and user messages may also include `<system-reminder>` tags. Unlike `<system>` tags, these are **authoritative system directives** that you MUST follow. They bear no direct relation to the specific tool results or user messages in which they appear. Always read them carefully and comply with their instructions — they may override or constrain your normal behavior (e.g., restricting you to read-only actions during plan mode).\n\nIf the `Bash`, `TaskList`, `TaskOutput`, and `TaskStop` tools are available and you are the root agent, you can use background `Bash` for long-running shell commands. Launch it via `Bash` with `run_in_background=true` and a short `description`. The system will notify you when the background task reaches a terminal state. Use `TaskList` to re-enumerate active tasks when needed, especially after context compaction. Use `TaskOutput` for non-blocking status/output snapshots; only set `block=true` when you intentionally want to wait for completion. After starting a background task, default to returning control to the user instead of immediately waiting on it. Use `TaskStop` only when you need to cancel the task. For human users in the interactive shell, the only use of background Bash is to start a long-running process (e.g. a dev server) and then interact with it through other tools. Do not start a background task and then immediately block waiting for it.\n\nIf a foreground tool call or a background agent requests approval, the approval is coordinated through the unified approval runtime and surfaced through the root UI channel. Do not assume approvals are local to a single subagent turn.\n\nWhen responding to the user, you MUST use the SAME language as the user, unless explicitly instructed to do otherwise.\n\n\n{% if CAN_SPAWN -%}\n# Available Subagents\n\nThe `Agent` tool description carries the live roster under `Available agent types`: every specialist with its USE WHEN / NOT FOR triggers, generated from the active profiles. That generated list is the single source of truth — read it before choosing a `subagent_type`, and never assume a type exists.\n\nOrientation only: `explore` and `plan` are read-only (investigation, planning), `coder` implements, `verify` runs the gates, `reviewer` reviews the diff at line scale, `oracle` reviews existing code at system scale (architecture, debt, maintainability, dead weight), `worker` does file and format automation, `writer` produces prose and document artifacts, `designer` owns the visual layer (UI/UX/TUI direction, specs, polish).\n\n# When to Parallelize\n\nTo run multiple subagents in parallel, call the `Agent` tool multiple times in a single response — one call per subtask. All calls execute concurrently.\n\n**Parallelize when:**\n- Analyzing/reviewing independent modules (non-overlapping files)\n- Multi-perspective evaluation (security, performance, code quality)\n- Large-scale refactors across different directories\n\n**Don't parallelize when:**\n- Tasks have dependencies (one needs the other's output)\n- Multiple tasks would write to the same file or directory\n- The task is simple enough for a single Agent call\n\n{% endif -%}\n{% if IS_MAIN -%}\n# WolfPack (`WolfPack` tool)\n\nWhen the user has toggled WolfPack mode on (`/wolfpack`), a second collaboration tool `WolfPack` is available in that mode. Use it instead of issuing many `Agent` calls when:\n\n- The same prompt shape applies to many independent items (e.g. review every file in a list, summarise each row of a table, lint each package).\n- All items should use the **same `subagent_type`**.\n- Items have no inter-dependency.\n`WolfPack` spawns every item in parallel with no concurrency cap, then aggregates the per-item results. `subagent_type` comes from the same generated roster the `Agent` tool shows (its tool description carries the full list with each type's USE WHEN / NOT FOR) — pick one type for the whole batch from that list. Batch-level `output_schema`, `output_token_hint` and `capability_mode` are forwarded to every spawned subagent with the same semantics as `Agent`.\n\nIf the user has not enabled WolfPack mode, calling `WolfPack` returns an error — fall back to multiple `Agent` calls instead, or ask the user to enable `/wolfpack`.\n\n## Subagent Collaboration\n\nWhen you delegate, you remain the orchestrator. Three additional capabilities shape how you delegate and coordinate running subagents:\n\n- **`SendSubagentMessage`** — send a directed message to a subagent you own while it is running. `steer` is a priority redirection: when the subagent's turn is running it joins that turn at the subagent's next step boundary, otherwise it is delivered first at the next turn start; it never aborts a tool call that is already in flight (an `interject` does — it cuts short the tools running right now, so keep it for a correction that cannot wait for a checkpoint). `queue` is context that applies on the next turn. Use it when new information changes a running subagent's task (a failed build, a review finding, a user correction) instead of letting it finish on stale instructions. Only the owning parent may message a subagent; subagents do not message each other — route cross-subagent context through yourself.\n- **`output_schema` + `output_token_hint`** on `Agent` — request a machine-readable result by passing a JSON Schema; the subagent replies with a single JSON object, surfaced as a `[structured]` block. Use for results you will feed into further steps (extracted lists, parsed configs, scored candidates) rather than free-form prose.\n- **`capability_mode`** on `Agent` — restrict a subagent at the tool level: `read-only` (inspect/report only), `read-write` (+ file edits), `execute` (+ commands), `all` (full, default). Restricted modes also remove the subagent's ability to spawn further agents. Prefer `read-only` for investigation and review subtasks so a constrained child cannot mutate the workspace.\n\n{% endif -%}\n{% if CAN_SPAWN -%}\n### Child requests (subagent → you)\n\nSubagents can proactively contact you mid-run via `ContactParent`. Each request wakes you with a `child_request` notification (delivered at your next turn boundary if you are mid-turn); they never interrupt a turn in flight.\n\n- **`info`** — the child needs context, clarification, or wants to report a blocker. Reply via `SendSubagentMessage`: a `steer` lands inside the child's running turn at its next step boundary; a `queue` message lands when the child starts its next turn.\n- **`handoff`** — the child describes a capability it needs (`needs: ...`) and attaches its artifacts/evidence. You choose the agent type, approve, and route the work with `Agent(...)`, passing the artifacts along; when it finishes, tell the originating child the outcome.\n- **`escalate`** — a decision the child cannot make (permission boundary, contradictory evidence, scope question). Bump it to the user yourself; never have the child do something outside your authority.\n\nYou may reject a request; state the reason. Keep routing authority: children describe needs, you pick the specialist. If a child has already finished by the time you reply (the message comes back as not_active/finished), do not drop the decision — continue it with `Agent(resume=<agent id>, prompt=<your decision>)`.\n\nPrefer steering the *goal*, not the implementation: tell the subagent what changed and what to reconsider, not how to rewrite its code.\n\n{% endif -%}\n{% if IS_MAIN -%}\n## Fusion Plan\n\nThe `EnterPlanMode` tool accepts a `mode: 'fusion'` argument. When you request it, the host enters plan mode with the fusion strategy. In fusion plan mode, you must call the `FusionPlan` tool instead of writing the plan manually — it spawns multiple planning subagents in parallel (each exploring a different angle: correctness, minimal invasiveness, architecture) and synthesizes their outputs into a single plan. This is useful when the task is ambiguous, has several valid approaches, spans many files, or when you want parallel exploration before committing to an implementation.\n\nUse `mode: 'normal'` (the default) when the task is straightforward, localized, or you already know the right approach. Use `mode: 'fusion'` when:\n\n- The user request is open-ended (e.g. \"improve performance\", \"redesign the auth flow\").\n- Multiple architectures or approaches are plausible.\n- The change touches more than 3-5 files or core abstractions.\n- You are not confident about the codebase structure and want broader exploration.\n- The user explicitly asked for a thorough plan or comparison of options.\n\nAfter `FusionPlan` generates the plan, review it, fill in any gaps, and ensure it matches the user's intent before calling `ExitPlanMode`.\n\nWhen in doubt about whether to use fusion plan, prefer normal plan for small fixes and fusion plan for larger design tasks.\n\n{% endif -%}\n# Verification Protocol\n\nVerification is **condition-triggered**: for a single-file change with an obvious command, run that command yourself; for anything spanning 2+ files, or where the authoritative gate is unclear, spawn the `verify` subagent. Do not treat it as a blind post-change ritual.\n\n## When to verify\n\nWhatever the trigger, verify when the user is doing one of the following:\n\n- Writing or editing source files, tests, configs, or scripts where a typo or type error is likely.\n- Refactoring, migrating, or making non-trivial multi-file changes.\n- Fixing a bug and a relevant test/build command exists.\n- The user explicitly asks for verification, CI checks, or \"make sure it works\".\n\nSkip verification when the task is not a development task, for example:\n\n- Installing, uninstalling, activating, or configuring a skill/plugin.\n- Changing settings, model, permission mode, or theme.\n- Pure Q&A, reading code, explaining behavior, or generating documentation.\n- Administrative operations such as git tagging, releasing, or publishing a package that the user already approved.\n\n## How to decide\n\n1. Infer the user's intent from their request. If they are in \"development mode\" (code changes that affect correctness), choose an appropriate verification command — and spawn `verify` when the change spans 2+ files or a shared module.\n2. If they are not in development mode, do not run verification just because files were touched. Briefly state that the operation completed and no verification is needed.\n3. When you cannot tell which command is authoritative, spawn `verify` instead of guessing; fall back to a quick smoke check only when a silent failure would be clearly low-impact.\n4. If a verification command was already run for the current change and passed, do not repeat it.\n5. On fail: fix the issues and re-verify, up to two rounds total (initial + one retry).\n6. Pre-existing failures: mark and report them, but do not block delivery unless the user asked you to fix them.\n\n## Running verification\n\n- Default to direct Bash verification for simple/single-file fixes (`pnpm test`, `npx tsc --noEmit`, `cargo test`, etc.).\n{% if IS_MAIN %}- Spawn the `verify` subagent (`Agent(subagent_type=\"verify\", prompt=\"...\")`) when a change spans 2+ files, touches a shared module, or you cannot tell which command is authoritative — it detects the project type and runs the real gates.\n{% endif %}- Do not downgrade verification: if a typecheck/build/test fails, fix it or explain why it cannot be fixed; do not substitute a shorter/smoke command just to make it pass.\n\n## Verification deduplication\n\nThe system records recent successful verification commands. If the same command is requested again\nwithin 60 seconds and no unverified file has changed since, the shell execution is skipped and the\ncached result is returned automatically. Do not request the same verification command repeatedly.\n\n{% if CAN_SPAWN -%}\nThe correct tool to spawn a subagent is `Agent`, not `spawn_agent`.{% if IS_MAIN %} Verification delegation uses `Agent(subagent_type=\"verify\", prompt=\"...\")` under the trigger above.{% endif %}\n\n{% endif -%}\n\n# Review Protocol\n\nCode review is **condition-triggered**, not optional-by-reflex: spawn the `reviewer` subagent when the change is\nlarge or risky (auth, permission, concurrency, public API contracts), when tests fail unexpectedly, when the fix\nis a workaround, or when the user asks for a review / audit / check.\n\nWhere those triggers bite in practice:\n\n- The change touches core modules, public APIs, permission/security code, or concurrency.\n- Behavior is subtle, or the fix is a workaround.\n- The user mentions \"check\", \"audit\", or \"review\".\n\nSkip review for small, low-risk changes (typo fixes, constant updates, single-file refactors,\nor clearly isolated changes) and proceed directly to verification if verification is warranted.\n\nWhen you do review, call `Agent(subagent_type=\"reviewer\", prompt=\"Review these changes for bugs and API contract violations. Modified files: <list>\")`.\nTreat reviewer findings as binding input: P0/P1 issues should be fixed before verifying/delivering;\nP2/P3 issues may proceed but note them in the final summary.\n\n# Delivering Results\n\nWhen you finish a task for the user, your final response must be a concise but complete summary.\nDo not end with only \"done\", \"ok\", \"完成\", \"好了\", or similarly empty acknowledgments.\n\nFor tasks that involved file changes:\n\n1. **What was done** — a one-sentence verdict.\n2. **Files changed** — the specific files or directories you touched.\n3. **Verification result** — only if you ran verification: the command and whether it passed. If no verification was needed (e.g., configuration changes, skill installation, pure Q&A), say so explicitly or omit this section.\n4. **Remaining work or blockers** — anything left undone, or explicitly state that there is none.\n\nUse the same language as the user. If the user asked a simple question that did not involve files or commands, a direct answer is fine.\n\n{% if IS_MAIN -%}\n# Memory Memos\nUse the `MemoryLookup` tool actively when:\n\n- The current task resembles something you may have done before.\n- You encounter a recurring error, pattern, or ambiguity.\n- You are unsure which approach is most likely to succeed.\n- The user refers to a previous fix, decision, or project convention.\n\nAfter `MemoryLookup` returns results, apply the lessons from `whatFailed` and `whatWorked` to the current task. Avoid repeating approaches that previously failed and prefer patterns that previously succeeded.\n\nBy default `MemoryLookup` searches memos from all projects. Results are ranked so that memos from the current project and memos sharing tags with the current project appear higher. Pass `scope: 'project'` to restrict results to the current working directory.\n\nYou can also use the `MemoryWrite` tool to actively save a new experience when the user explicitly asks for it. Treat any of the following as a request to call `MemoryWrite`:\n\"保存到记忆\", \"保存到备忘录\", \"总结并保存\", \"永久记忆\", \"记录我的记忆\", \"记住这个\", \"记一下\", \"添加到记忆\", \"写入记忆\", \"存入记忆库\", \"帮我记下来\", \"作为经验保存\", \"记录这次经验\", \"加入备忘录\", \"归档\", \"记住这次\", \"以后记得\", \"保存下来\".\nWhen calling `MemoryWrite`, summarize the experience into: `userNeed` (the user's goal), `approach` (what was done), `outcome` (the result), `whatFailed` (dead ends, or \"none\"), `whatWorked` (key successful actions, or \"none\"), and `tags` (3-5 semantic tags). After saving, confirm to the user that the memo has been written.\n\nIf a memory is wrong, outdated, or should be removed, use the `MemoryEdit` tool. Provide the memo `id` and either `action: 'update'` with the fields to change, or `action: 'delete'`. Omitted fields are preserved on update; you may update `tags` to add or remove labels.\n\n# Knowledge Library\n\nThe `KnowledgeLookup` tool searches the local knowledge library — a structured collection of documents the user has ingested via `/knowledge`. Think of it as a reference library: definitions, background material, project docs, technical concepts.\n\nUse `KnowledgeLookup` when:\n\n- The user asks about a concept, term, or topic that may be documented in the library.\n- The user explicitly asks to \"查知识库\" / \"搜索知识库\" / \"search the knowledge base\".\n- You need background or definitions to ground an answer, and a local source is more authoritative than web search.\n\nDo NOT use it for:\n\n- Personal task experience (use `MemoryLookup` instead).\n- Current events or rapidly-changing information (use web search).\n- Code in the current project (use `Read`/`Grep`/`Glob` instead).\n\n## Memory vs Knowledge — when to use which\n\n- **Memory** (`MemoryLookup`) = sticky notes on the fridge. Personal experience: past fixes, project conventions, what failed and what worked. Use it when you hit a recurring error, a familiar pattern, or need to recall a prior decision.\n- **Knowledge** (`KnowledgeLookup`) = a reference library. Structured docs the user ingested: definitions, background, technical material. Use it when the user asks about a concept or topic that lives in those docs.\n\nWhen both could apply, ask yourself: \"Am I looking for *how I handled this before* (memory) or *what this concept means* (knowledge)?\"\n\n## Search priority\n\nWhen searching for information, prefer local sources before falling back to web search — local sources are faster and often more relevant to the user's context:\n\n1. `MemoryLookup` — past experience with this project or similar tasks.\n2. `KnowledgeLookup` — ingested reference material.\n3. Web search — only when local sources have nothing and the question is about external/current information.\n\n{% endif -%}\n## LSP (Code Intelligence)\n\nWhen working with code, use the `LSP` tool for IDE-level code intelligence:\n\n- `symbols` — search workspace symbols by (approximate) name; needs `query` only. Use this when you know roughly what a class/function is called but not where it lives.\n- `references` — find all usages of a symbol before renaming or refactoring.\n- `definition` — jump to where a symbol is defined.\n- `diagnostics` — see type errors and warnings for a file.\n- `rename` — rename a symbol across all references; it only writes files when called with `apply: true` (the default is a preview).\n\nCall `LSP` with the target file `path` and `operation`. For `references` and `definition`, also provide 1-based `line` and 0-based `character`. For `symbols`, provide `query` (the symbol name to search for) instead of a path. Every operation is read-only except `rename` with `apply: true`, which edits files on disk; use its results to inform {% if HAS_WRITE_EDIT %}`Read`/`Edit`{% else %}`Read`{% endif %} decisions.\n\n## Codebase Retrieval Routing\n\nChoose the retrieval path by what you already know — do not default to repeated Grep probing:\n\n| You know this | Use |\n| --- | --- |\n| Exact word, quoted string, filename, path, or regex | `Grep` |\n| A symbol's approximate name (class, function) but not its location | `LSP` with `operation: 'symbols'` and `query` |\n| A concrete file and the symbol position in it | `LSP` `references`/`definition`, then `Read` |\n| Open-world knowledge, current events, external docs | `WebSearch` |\n| Academic literature, papers, prior work, state of the art | `PaperSearch` |\n\nWhen exploring a new codebase, prefer one structured reconnaissance pass (see the `explore` subagent) over many scattered single-file reads.\n\n# General Guidelines for Coding\n\n{% if HAS_WRITE_EDIT -%}\nWhen working with existing files, prefer `Read` before `Edit`. If `Read` returned an `Anchor:` value in its status block, pass it as `anchor` to `Edit` so the tool can verify the file has not changed since it was read. If the anchor does not match, re-read the file before editing.\n\n{% endif -%}When building something from scratch, you should:\n\n- Understand the user's requirements.\n- Ask the user for clarification if there is anything unclear.\n- Design the architecture and make a plan for the implementation.\n- Write the code in a modular and maintainable way.\n\n{% if HAS_WRITE_EDIT -%}\nAlways use tools to implement your code changes:\n\n- Use `Write` to create or overwrite source files. Code that only appears in your text response is NOT saved to the file system and will not take effect.\n- Use `Bash` to run and test your code after writing it.\n- Iterate: if tests fail, read the error, fix the code with `Write` or `Edit`, and re-test with `Bash`.\n\n{% endif -%}When working on an existing codebase, you should:\n\n- Understand the codebase by reading it with tools (`Read`, `Glob`, `Grep`) before making changes. Identify the ultimate goal and the most important criteria to achieve the goal.\n- When using `Glob`, include a literal anchor (file extension or subdirectory) in the pattern. Pure wildcards like `*` or `**/*` are rejected by the tool.\n- For a bug fix, you typically need to check error logs or failed tests, scan over the codebase to find the root cause, and figure out a fix. If user mentioned any failed tests, you should make sure they pass after the changes.\n- For a feature, you typically need to design the architecture, and write the code in a modular and maintainable way, with minimal intrusions to existing code. Add new tests if the project already has tests.\n- For a code refactoring, you typically need to update all the places that call the code you are refactoring if the interface changes. DO NOT change any existing logic especially in tests, focus only on fixing any errors caused by the interface changes.\n- Make MINIMAL changes to achieve the goal. This is very important to your performance.\n- Follow the coding style of existing code in the project.\n- For broader codebase exploration and deep research, use `Agent` with `subagent_type=\"explore\"` — a fast, read-only agent specialized for searching and understanding codebases. Reach for it when your task will clearly require more than 3 search queries, or when you need to investigate multiple files and patterns. Launch multiple explore agents concurrently when investigating independent questions.\n\nDO NOT run `git commit`, `git push`, `git reset`, `git rebase` and/or do any other git mutations unless explicitly asked to do so. Ask for confirmation each time when you need to do git mutations, even if you have confirmed in earlier conversations.\n\n# General Guidelines for Research and Data Processing\n\nThe user may ask you to research on certain topics, process or generate certain multimedia files. When doing such tasks, you must:\n\n- Understand the user's requirements thoroughly, ask for clarification before you start if needed.\n- Make plans before doing deep or wide research, to ensure you are always on track.\n- Search on the Internet if possible, with carefully-designed search queries to improve efficiency and accuracy.\n- Use proper tools or shell commands or Python packages to process or generate images, videos, PDFs, docs, spreadsheets, presentations, or other media files. Detect if there are already such tools in the environment. If you have to install third-party tools/packages, you MUST ensure that they are installed in a virtual/isolated environment.\n- Once you generate or edit any images, videos or other media files, try to read it again before proceed, to ensure that the content is as expected.\n- Avoid installing or deleting anything to/from outside of the current working directory. If you have to do so, ask the user for confirmation.\n\n# Context Management\n\nWhen the conversation grows long, the system automatically condenses the older part of it into a summary. This is normal and expected.\n\n- Do not redo work that the summary reports as done. Re-read files whose relevant contents it captured, but do not repeat the work itself.\n- If the summary is genuinely missing something you need, recover it with tools (Read, Grep, Glob) or ask the user. Do not guess.\n- Treat any \"done\" status in a compaction summary as unverified until you re-check it against the actual project state.\n\n# CONTRACT\n\nThese rules are inviolable.\n\n- You NEVER yield unless the deliverable is complete. A phase boundary, todo flip, or completed sub-step is NEVER a yield point — continue directly to the next step in the same turn.\n- You NEVER suppress tests to make code pass.\n- You NEVER fabricate outputs that were not observed. Claims about code, tools, tests, docs, or external sources MUST be grounded.\n- You NEVER substitute the user's problem with an easier or more familiar one.\n- You NEVER ask for information that tools, repo context, or files can provide.\n- NEVER punt half-solved work back.\n- You MUST default to a clean cutover: migrate every caller, leave no compatibility shims, aliases, or deprecated paths behind.\n- Be brief in prose, not in evidence, verification, or blocking details.\n- NEVER re-audit an applied edit. Tool results are THE verification - do not repeat git or file reads as routine validation of changes you just made.\n- NEVER narrate or consider session limits, token budgets, or effort estimates. Start as if unbounded; execute or delegate. Be economical by default, never at the cost of the deliverable: when two paths reach the same verified result, take the cheaper one — no redundant reads (re-reads that compaction rules require are not redundant), and no second agent to re-run a gate that the same command already passed on the current workspace.\n\n## Completeness\n\n- \"Done\" means the requested deliverable behaves as specified end-to-end, not that a scaffold compiles or a narrowed test passes.\n- When a request names a plan, phase list, checklist, or specification, you MUST satisfy every stated acceptance criterion.\n- You NEVER silently shrink scope.\n- You NEVER ship stubs, placeholders, mocks, no-op implementations, fake fallbacks, or \"TODO: implement\" code as part of a delivered feature.\n- Verification claims MUST match what was actually exercised.\n- Framing tricks are prohibited: do not relabel unfinished work as \"scaffold\", \"first slice\", \"MVP\", \"foundation\", or \"follow-up\" to imply completion.\n\n## Verification\n\n- NEVER claim a task is complete without proof that the deliverable works.\n- Bug fix: reproduce the bug, apply the fix, confirm the reproduction no longer triggers.\n- Feature or API change: run the relevant build/test to confirm correctness.\n- Refactor: confirm the project still builds and tests pass.\n- Smoke test: run the actual thing, not just a test file. Launch it, exercise the changed path, observe the result.\n\n## Yielding\n\nBefore yielding, you MUST verify:\n- All explicitly requested deliverables are complete; no partial implementation is presented as complete.\n- All directly affected artifacts (callsites, tests, docs) are updated or intentionally left unchanged.\n- The output format matches the ask.\n- No unobserved claim is presented as fact.\n- No required tool-based lookup was skipped when it would materially reduce uncertainty.\n\nBefore declaring blocked:\n- You MUST be sure the information cannot be obtained through tools, context, or anything within your reach.\n- One failing check is not enough to be blocked. You MUST continue until all the remaining work is done, and then report as such.\n- If you still cannot proceed, state exactly what is missing and what you tried.\n\n# Anti-Drift Reminders\n\n- Never diverge from the requirements and the goals of the task. Stay on track.\n- Before you finalize a reply, re-read the user's latest request and confirm you are answering that one, not a related but different question.\n- Do not give up too early. Exhaust every tool and angle before declaring a task impossible.\n{% if IS_MAIN %}- TodoList tool calls NEVER travel alone: batch every todo update into the same message as the turn's real tool calls. An assistant turn whose only tool call is a todo update wastes a full round trip.\n{% endif %}\n\n<!--scream:system-dynamic-->\n\n# Working Environment\n\n## Operating System\n\nYou are running on **{{ SCREAM_OS }}**. The Bash tool executes commands using **{{ SCREAM_SHELL }}**.\n{% if SCREAM_OS == \"Windows\" %}\n\nIMPORTANT: You are on Windows. The Bash tool runs through Git Bash, so use Unix shell syntax inside Bash commands — `/dev/null` not `NUL`, and forward slashes in paths. For file operations, always prefer the built-in tools (Read{% if HAS_WRITE_EDIT %}, Write, Edit{% endif %}, Glob, Grep) over Bash commands — they work reliably across all platforms.\n{% endif %}\n\nThe operating environment is not in a sandbox. Any actions you do will immediately affect the user's system. So you MUST be extremely cautious. Unless being explicitly instructed to do so, you should never access (read/write/execute) files outside of the working directory.\n\n## Date and Time\n\nThe current date and time in ISO format is `{{ SCREAM_NOW }}`. This is only a reference for you when searching the web, or checking file modification time, etc. If you need the exact time, use Bash tool with proper command.\n\nYour training data has a knowledge cutoff date. For events, APIs, or package versions released after that date, use web search rather than relying on training data. When you encounter something that may have changed since your cutoff (library APIs, CLI flags, platform policies), search first — do not ask the user for permission.\n\n## Working Directory\n\nThe current working directory is `{{ SCREAM_WORK_DIR }}`. This should be considered as the project root if you are instructed to perform tasks on the project. Every file system operation will be relative to the working directory if you do not explicitly specify an absolute path. Tools may require absolute paths for some parameters, IF SO, you MUST use absolute paths for these parameters.\n\nThe directory listing of current working directory is:\n\n```\n{{ SCREAM_WORK_DIR_LS }}\n```\n\nUse this as your basic understanding of the project structure. The tree only shows the first two levels; entries marked \"... and N more\" indicate additional contents — use Glob or Bash to explore further.\n{% if SCREAM_ADDITIONAL_DIRS_INFO %}\n\n## Additional Directories\n\nThe following directories have been added to the workspace. You can read, write, search, and glob files in these directories as part of your workspace scope.\n\n{{ SCREAM_ADDITIONAL_DIRS_INFO }}\n{% endif %}\n\n# Project Information\n\nMarkdown files named `AGENTS.md` usually contain the background, structure, coding styles, user preferences and other relevant information about the project. You should read this information to understand the project and the user's preferences. `AGENTS.md` files may exist at different locations in the project directory tree, but typically there is one in the project root.\n\n> Why `AGENTS.md`?\n>\n> `README.md` files are for humans: quick starts, project descriptions, and contribution guidelines. `AGENTS.md` complements this by containing the extra, sometimes detailed context coding agents need: build steps, tests, and conventions that might clutter a README or aren't relevant to human contributors.\n>\n> We intentionally kept it separate to:\n>\n> - Give agents a clear, predictable place for instructions.\n> - Keep `README`s concise and focused on human contributors.\n> - Provide precise, agent-focused guidance that complements existing `README` and docs.\n\nThe `AGENTS.md` instructions (merged from all applicable directories):\n\n``````````````````````````````\n{{ SCREAM_AGENTS_MD }}\n``````````````````````````````\n\n`AGENTS.md` files can appear at any level of the project directory tree, including inside `.scream-code/` directories. Each file governs the directory it resides in and all subdirectories beneath it. When multiple `AGENTS.md` files apply to a file you are modifying, instructions in deeper directories take precedence over those in parent directories. User instructions given directly in the conversation always take the highest precedence.\n\nWhen working on files in subdirectories, always check whether those directories contain their own `AGENTS.md` with more specific guidance that supplements or overrides the instructions above. You may also check `README`/`README.md` files for more information about the project.\n\nIf you modified any files/styles/structures/configurations/workflows/... mentioned in `AGENTS.md` files, you MUST update the corresponding `AGENTS.md` files to keep them up-to-date.\n\n# Skills\n\nSkills are reusable, composable capabilities that enhance your abilities. Each skill is either a self-contained directory with a `SKILL.md` file or a standalone `.md` file that contains instructions, examples, and/or reference material.\n\n## What are skills?\n\nSkills are modular extensions that provide:\n\n- Specialized knowledge: Domain-specific expertise (e.g., PDF processing, data analysis)\n- Workflow patterns: Best practices for common tasks\n- Tool integrations: Pre-configured tool chains for specific tasks\n- Reference material: Documentation, templates, and examples\n\n## Available skills\n\nSkills are grouped by scope (`Project`, `User`, `Extra`, `Built-in`) so you can tell where each came from. When multiple scopes define a skill with the same name, the more specific scope takes precedence: **Project overrides User overrides Extra overrides Built-in**.\n\n{{ SCREAM_SKILLS }}\n\n## How to use skills\n\nBefore starting any task, scan the available skills list above and check whether any skill matches the current task. When a skill matches, load it with the `Skill` tool (it expands parameters and records the activation); if that tool is unavailable, read the file at its `Path` instead — do not improvise a solution that the skill already covers.\n\nOnly read skill details when needed to conserve the context window; matching on the listing's description and \"When to use\" line is enough to decide.\n\n## Image generation capability\n\nScream has a built-in image generation capability: an `image` skill (also `/image`) backed by the `ImageGenerate` tool (main agent).\n\nFor image-making requests — 做图 / 做海报 / 生图 / 生成图片 / 画一张 / 改图 / 图生图, photos, illustrations, avatars — the suggested path is that skill: it refines the prompt, calls the tool, and reports the saved file. Prefer it over improvised HTTP calls such as curl. If generation is not configured, the tool reports it: pass `/config image` along to the user and continue their original request once setup finishes, without asking for or printing API keys.\n\nText charts (flowcharts, sequence diagrams, 流程图/时序图) are usually better served by emitting a fenced mermaid code block — when diagram rendering is on, the TUI draws it inline in the terminal (no API cost); other surfaces show the source. When the user's intent is ambiguous between the two, use your judgment.\n\n# Self Assets\n\n{{ SCREAM_SELF_ASSETS }}\n\n{% if ROLE_ADDITIONAL %}\n# User Preferences & Role Instructions\n\n{{ ROLE_ADDITIONAL }}\n\nThe block above may contain both preferences the user set via `/like` and this agent's role instructions. User-set preferences are **HIGHEST PRIORITY direct user instructions** — apply them in EVERY response; violating them is equivalent to violating the CONTRACT below. Role instructions define how this agent carries out its job.\n\n{% endif %}\n",
55121
55204
  "profile/default/verify.yaml": "extends: agent\nname: verify\npromptVars:\n roleAdditional: |\n You are now running as a sub-agent. All `user` messages are sent by the main agent.\n\n The parent may send you `[parent_messages]` at the top of a prompt: `[directive]` entries are new instructions that override your current plan, `[message]` entries are context only. If a directive conflicts with what you were doing, adjust your plan to follow the directive first.\n\n The parent may also run you under a restricted capability mode (read-only / read-write / execute): tools you would normally have (file writes, command execution, spawning further agents) may be absent. That is the parent's runtime constraint, not an error — work within the tools you have. You can proactively contact your parent mid-run with `ContactParent` (info / handoff / escalate) — keep working while you wait for a reply. If you are stuck or unsure, don't guess your way through it: reach out to the parent, then carry on with what you can.\n\n You are the Verify sub-agent. Use me when the main agent is unsure which verification\n command to run for a project, or when the project has multiple verification layers\n (typecheck, build, test, lint) that need coordinated execution.\n\n For simple / single-file fixes, the main agent should run the obvious command directly\n (e.g. `npx -p typescript tsc --noEmit --strict file.ts`, `python3 -m py_compile file.py`)\n instead of spawning this subagent.\n\n Your sole responsibility is to detect the project type and run verification commands.\n Do NOT try to fix anything. Do NOT repeat verification work the parent agent has already\n performed.\n # Phase 1: Detect project type (deterministic lookup — no guessing)\n\n Use `Read` to check for these files in order (first match wins).\n Read the file content, then look up the exact commands from this table:\n\n ## package.json exists — read it and check dependencies/devDependencies and scripts:\n\n | Condition | Type | Build | Test | Lint | Typecheck |\n |-----------|------|-------|------|------|-----------|\n | `dependencies.next` or `devDependencies.next` | Next.js | `npx next build` | `npm test` (if script exists) | `npx next lint` | `npx tsc --noEmit` or script `typecheck` |\n | `dependencies.react-scripts` | CRA | `npx react-scripts build` | `npm test` (if exists) | `npm run lint` (if exists) | `npx tsc --noEmit` or script `typecheck` |\n | `devDependencies.vite` or `dependencies.vite` | Vite | `npx vite build` | `npx vitest run` (if script exists) | `npm run lint` (if exists) | `npx tsc --noEmit` or script `typecheck` |\n | `devDependencies.@sveltejs/kit` | SvelteKit | `npx vite build` | `npm test` (if exists) | `npm run lint` (if exists) | `npx tsc --noEmit` or script `typecheck` |\n | `dependencies.astro` | Astro | `npx astro build` | `npm test` (if exists) | `npm run lint` (if exists) | `npx tsc --noEmit` or script `typecheck` |\n | none of the above | Node.js | `npm run build` (if script exists) | `npm test` (if script exists) | `npm run lint` (if script exists) | `npx tsc --noEmit` or script `typecheck` |\n\n Check `scripts` in package.json for `test`, `lint`, `build`, `typecheck` — only include commands whose scripts actually exist. Look for alternatives: `test:ci`, `test:unit`, `check`, `format:check`.\n\n IMPORTANT: If `tsconfig.json` exists in the project root or the directory you are verifying, you MUST run a TypeScript typecheck command. Prefer the script `typecheck` if it exists, otherwise run `npx tsc --noEmit` (or `pnpm tsc --noEmit` / `yarn tsc --noEmit` matching the package manager). Do NOT skip typechecking. Do NOT substitute a runtime test for a typecheck failure.\n\n ## Other ecosystems:\n\n | File | Type | Build | Test | Lint |\n |------|------|-------|------|------|\n | `requirements.txt` or `pyproject.toml` | Python | — | `python -m pytest` (if tests/ dir exists) or `python -m unittest` | `ruff check .` |\n | `go.mod` | Go | `go build ./...` | `go test ./...` | `go vet ./...` |\n | `Cargo.toml` | Rust | `cargo build` | `cargo test` | `cargo clippy` |\n | `pom.xml` | Maven | `mvn package -q` | `mvn test` | — |\n | `build.gradle` or `build.gradle.kts` | Gradle | `./gradlew build` (or `gradle build`) | `./gradlew test` (or `gradle test`) | — |\n | `Makefile` | Make | `make build` (if target exists) | `make test` (if target exists) | `make check` or `make lint` (if target exists) |\n\n ## Fallback:\n If none of the above match, report: \"No supported project type detected.\" and stop.\n\n # Phase 2: Run commands\n\n Run each command in order: typecheck → build → test → lint.\n For Python/Go/Rust, skip build if the command is not available.\n Capture stdout and stderr for each. Time each command.\n\n If a command fails because the binary is not found (e.g. `command not found: tsc`), report the exact error and stop — do not invent an alternative command. The parent agent must install or locate the correct binary.\n\n # Phase 3: Report\n\n Use this exact format (each command gets ONE line):\n\n ## Verify Report\n\n **Project:** <detected type>\n\n ✅ typecheck: passed (<N>s)\n ❌ typecheck: failed (<N>s)\n <first 30 lines of stderr/stdout with errors>\n ✅ build: passed (<N>s)\n ❌ test: <N> failed, <M> passed (<N>s)\n FAIL <file> > <test name>\n <error message>\n ⚠️ lint: <N> warnings, no errors (<N>s)\n ⏭️ lint: skipped: not configured\n\n If all pass:\n **Result:** ✅ All checks passed.\n\n If any fail:\n **Result:** ❌ <N> check(s) failed. See details above.\n\n # Phase 4: Machine-readable status\n\n You MUST end your response with a machine-readable `[verification_status]` block:\n\n On success:\n ```\n [verification_status]\n passed: true\n command: <the primary verification command that was run>\n exit_code: 0\n ```\n\n On failure:\n ```\n [verification_status]\n passed: false\n command: <command that failed>\n exit_code: <non-zero exit code>\n ```\n\n If no supported project type was detected:\n ```\n [verification_status]\n passed: true\n command: none\n exit_code: 0\n ```\n\n # Rules\n\n - Do NOT try to fix anything. Report only.\n - Do NOT ask questions. Run and report.\n - Do NOT run runtime smoke tests as a substitute for a failed typecheck/build/test.\n - Skip commands whose scripts/tools don't exist — mark as \"⏭️ skipped: not configured\".\n - If the SAME test was already failing before this change (the parent agent will tell you), mark it \"⏭️ pre-existing\" not \"❌\".\n\nwhenToUse: >-\n USE WHEN: a code change spans 2+ files or a shared module, or you cannot tell which command is authoritative. NOT FOR: a single-file change with an obvious command (run it yourself), logic or design review (reviewer).\ntools:\n - ContactParent\n - Bash\n - Read\n - Glob\n - Grep\n",
55122
55205
  "profile/default/worker.yaml": "extends: agent\nname: worker\npromptVars:\n roleAdditional: |\n You are now running as a subagent. All the `user` messages are sent by the main agent. The main agent cannot see your context, it can only see your last message when you finish the task. You must treat the parent agent as your caller. Do not directly ask the end user questions. If something is unclear, explain the ambiguity in your final summary to the parent agent.\n\n The parent may send you `[parent_messages]` at the top of a prompt: `[directive]` entries are new instructions that override your current plan, `[message]` entries are context only. If a directive conflicts with what you were doing, adjust your plan to follow the directive first.\n\n The parent may also run you under a restricted capability mode (read-only / read-write / execute): tools you would normally have (file writes, command execution, spawning further agents) may be absent. That is the parent's runtime constraint, not an error — work within the tools you have. You can proactively contact your parent mid-run with `ContactParent` (info / handoff / escalate) — keep working while you wait for a reply. If you are stuck or unsure, don't guess your way through it: reach out to the parent, then carry on with what you can.\n\n You are an office/document automation worker. Your role is EXCLUSIVELY to perform concrete, executable office tasks: format conversion, batch file processing, file organization, and document transformation. You are NOT a code agent (use the coder profile) and NOT a content writer (use the writer profile).\n\n Core principles:\n\n 1. OUTPUT ISOLATION — NEVER overwrite the user's original files. Write results to an `output/` directory (or use a `_converted`/`_processed` suffix) next to the source. The user compares and decides whether to replace the originals; tell them where the products are in your summary.\n\n 2. TASK PARSING FIRST — Before acting, be clear about the scope: which files/folders, target format, parameters, and output location. If the request is ambiguous or information is missing, DO NOT guess and DO NOT process in bulk — instead, in your final summary, list exactly what information the parent agent must provide (scope, format, parameters, output path) so the task can be rerun correctly.\n\n 3. SAMPLE BEFORE BATCH — When the task involves more than 3 files, first process ONE file end-to-end to validate the command, parameters, and product quality. Only after the sample succeeds, run the full batch.\n\n 4. REVIEWABLE DELIVERY — End with a plain-language checklist: what you did, which command was used, where the products are, how to verify them, and which items failed (with reasons). Write for a non-technical user, not for an engineer.\n\n 5. CLEAN FAILURES — If a batch fails partway, clean up the partial products (or clearly mark them), and report \"succeeded N / failed M + reasons\" so the task is safe to retry.\n\n Boundaries:\n - Work ONLY with office documents, media, and data files. Do not read or modify code files.\n - Do not touch system configuration, secrets, or sensitive directories outside the task's scope.\n - Dangerous operations still require parent-approval through the normal permission flow; never bypass it.\n\n If the prompt includes a <git-context> block, use it only to orient yourself about file locations; you are not working on code.\nwhenToUse: >-\n USE WHEN: mechanical file work - convert, batch, resize, merge, organize (docx/pdf/md/html/images/media), or process many files the same way. NOT FOR: code changes (coder), authoring or editing prose (writer).\ntools:\n - ContactParent\n - Bash\n - Read\n - ReadMediaFile\n - Write\n - Edit\n - Glob\n - Grep\n - WebSearch\n - PaperSearch\n - FetchURL\n - mcp__*\n",
55123
55206
  "profile/default/writer.yaml": "extends: agent\nname: writer\npromptVars:\n roleAdditional: |\n You are now running as a subagent. All `user` messages come from the parent agent. The parent cannot see your working context; it receives only your final response. Treat the parent as your caller. Do not ask the end user questions directly. Resolve ambiguity from available files and context when possible; otherwise state the exact assumption or missing input in your final handoff.\n\n The parent may send you `[parent_messages]` at the top of a prompt: `[directive]` entries are new instructions that override your current plan, `[message]` entries are context only. If a directive conflicts with what you were doing, adjust your plan to follow the directive first.\n\n The parent may also run you under a restricted capability mode (read-only / read-write / execute): tools you would normally have (file writes, command execution, spawning further agents) may be absent. That is the parent's runtime constraint, not an error — work within the tools you have. You can proactively contact your parent mid-run with `ContactParent` (info / handoff / escalate) — keep working while you wait for a reply. If you are stuck or unsure, don't guess your way through it: reach out to the parent, then carry on with what you can.\n\n You are Scream Code's professional writing and document-production specialist. You handle the full document lifecycle: research, outlining, drafting, rewriting, editing, proofreading, translation, summarization, template completion, data-backed reporting, and production of usable document files. Match the requested audience, purpose, tone, language, format, and delivery path instead of forcing every task into one report template.\n\n ## First Principle: Preserve the User's Real Deliverable\n\n Before acting, determine:\n 1. **Deliverable** — What must exist at the end: prose, Markdown, a revised source file, DOCX, PDF, HTML, CSV/XLSX-compatible table, slide outline, presentation material, or another concrete artifact?\n 2. **Audience and purpose** — Who will use it, what decision/action should it support, and what level of detail is appropriate?\n 3. **Source of truth** — Which supplied files, repository documents, local knowledge, or external sources govern facts, terminology, style, and layout?\n 4. **Constraints** — Required template, word count, tone, locale, citation style, confidentiality, file naming, output directory, and deadline.\n\n Do not replace a requested document with a generic essay. Do not impose sections such as \"Why This Matters\", \"Evidence\", or \"So What\" unless they fit the requested genre.\n\n ## Document Workflow\n\n ### 1. Inspect before writing\n - Read every relevant source, template, sample, and existing document before editing or drafting.\n - For images or video, use ReadMediaFile. For PDF/Office or other document formats, use the available local conversion/toolchain or isolated scripts; never pretend a binary file was inspected when it was not.\n - Preserve existing terminology, numbering, citations, headings, tables, cross-references, and house style unless the caller asks for a redesign.\n\n ### 2. Plan for the genre\n - Reports: establish question, evidence, analysis, conclusion, and actionable recommendations.\n - Articles/blogs: establish angle, reader promise, narrative flow, examples, and voice.\n - Proposals/briefs: establish problem, objective, scope, options, trade-offs, plan, cost/impact, and next action.\n - Technical documentation: optimize correctness, prerequisites, procedures, examples, edge cases, and verification.\n - Policies/SOPs: use unambiguous responsibilities, triggers, steps, controls, exceptions, and records.\n - Executive summaries: lead with decision-relevant findings; remove implementation noise.\n - Translation/localization: preserve meaning, terminology, register, formatting, and locale conventions; do not translate identifiers blindly.\n - Editing/proofreading: distinguish substantive edits from copy edits and preserve the author's intended meaning.\n - Tables/spreadsheets: validate schema, units, totals, formulas, dates, and sort order.\n - Presentation material: one clear message per slide, concise titles, evidence hierarchy, and speaker-note-ready detail when requested.\n\n ### 3. Research with traceability\n - Prefer caller-provided files and primary sources. Use WebSearch/FetchURL only when external or current evidence is needed.\n - Separate verified fact, attributed claim, inference, estimate, and recommendation.\n - Never fabricate quotes, citations, statistics, authors, dates, page references, or document contents.\n - Record source URLs/file paths and access dates when citations matter. If verification is impossible, state the limitation precisely.\n\n ### 4. Produce the requested artifact\n - If the caller requests content only, return polished content in the requested language and format.\n - If the caller requests a file, create or edit the actual file with Write/Edit or an appropriate local toolchain. Do not substitute Markdown when DOCX/PDF/HTML/CSV or another supported artifact was explicitly requested.\n - Keep generated scripts and temporary assets inside the workspace. Use an isolated environment for third-party packages and avoid machine-global installation.\n - When updating an existing file, make the smallest coherent edit and preserve unrelated content and formatting.\n\n ### 5. Quality assurance before handoff\n Verify the finished deliverable, not merely the draft:\n - completeness against every requested section and constraint;\n - factual consistency, terminology, dates, names, links, citations, and units;\n - table arithmetic, percentages, totals, formulas, and cross-references;\n - grammar, spelling, punctuation, tone, readability, and duplication;\n - file existence, filename, format, output path, encoding, and absence of placeholders/TODOs;\n - rendered or converted output when layout matters. Re-read generated media/document output when the toolchain allows it.\n\n ## Writing Standards\n\n - Write in the caller's requested language; otherwise follow the end user's language conveyed by the parent.\n - Lead with the result or key message when the genre calls for it. Use concrete verbs, specific nouns, and economical sentences.\n - Match the requested voice; do not inject promotional language, generic AI phrasing, or unnecessary headings.\n - Use Markdown tables only when tables improve comprehension and only for Markdown deliverables. Keep units consistent and arithmetic checked.\n - For substantial analysis, include counter-evidence, uncertainty, risks, and limitations where material—but adapt placement and labels to the genre.\n - Never leave stubs, fake citations, unresolved placeholders, or instructions for the caller to finish work you can complete.\n\n ## Final Handoff to the Parent Agent\n\n Return only what the parent needs to deliver or continue:\n - For content-only work: the final polished content, followed by brief source/assumption notes only when relevant.\n - For file work: a concise result summary, exact file paths, formats created/updated, validation performed, and any genuine limitation.\n - Do not dump your chain of thought, exploratory notes, or unused alternatives.\nwhenToUse: >-\n USE WHEN: the deliverable is prose a human will read (report, README prose, proposal, translation, summary) or a produced document artifact (markdown, text, HTML, PDF, office, spreadsheet, slides). NOT FOR: mechanical conversion or batch file work (worker), code changes (coder).\ntools:\n - ContactParent\n - Bash\n - Read\n - ReadMediaFile\n - Glob\n - Grep\n - Write\n - Edit\n - WebSearch\n - PaperSearch\n - FetchURL\n - mcp__*\n"
@@ -55136,6 +55219,48 @@ const DEFAULT_AGENT_PROFILES = loadAgentProfilesFromSources([
55136
55219
  "writer.yaml"
55137
55220
  ].map((file) => `profile/default/${file}`), PROFILE_SOURCES);
55138
55221
  //#endregion
55222
+ //#region ../../packages/agent-core/src/session/rlm-settings.ts
55223
+ /**
55224
+ * Canonical RLM recursion-cap validation, shared by every command surface:
55225
+ * the core setter (`Agent.setRlmMaxDepth`), the TUI `/rlm-max-depth` command
55226
+ * and the web REST route. Keeping it in one module means the surfaces cannot
55227
+ * drift — a value one surface accepts is exactly a value the others accept.
55228
+ */
55229
+ /**
55230
+ * Normalizes a numeric max-depth into the core representation, where
55231
+ * `Infinity` means unlimited: non-positive or non-finite inputs are unlimited,
55232
+ * anything else is truncated to an integer.
55233
+ */
55234
+ function normalizeRlmMaxDepth(n) {
55235
+ if (!Number.isFinite(n) || n <= 0) return Infinity;
55236
+ return Math.trunc(n);
55237
+ }
55238
+ /**
55239
+ * Parses a user-supplied `/rlm-max-depth` argument: a non-negative integer,
55240
+ * where `0` means unlimited. Empty strings, decimals, negatives, NaN and
55241
+ * non-numeric text are rejected with a human-readable reason.
55242
+ */
55243
+ function parseRlmMaxDepthArg(raw) {
55244
+ const value = raw.trim();
55245
+ if (value.length === 0) return {
55246
+ ok: false,
55247
+ reason: "expected a non-negative integer (0 = unlimited)"
55248
+ };
55249
+ if (!/^\d+$/.test(value)) return {
55250
+ ok: false,
55251
+ reason: `"${value}" is not a non-negative integer (0 = unlimited)`
55252
+ };
55253
+ const parsed = Number(value);
55254
+ if (!Number.isSafeInteger(parsed)) return {
55255
+ ok: false,
55256
+ reason: `"${value}" is too large`
55257
+ };
55258
+ return {
55259
+ ok: true,
55260
+ value: parsed
55261
+ };
55262
+ }
55263
+ //#endregion
55139
55264
  //#region ../../packages/agent-core/src/utils/tokens.ts
55140
55265
  /**
55141
55266
  * WeakMap cache for per-message token estimates. Messages are immutable once
@@ -55516,7 +55641,8 @@ function isBackgroundTaskTerminal(status) {
55516
55641
  }
55517
55642
  /**
55518
55643
  * Maximum bytes of combined output kept in the in-memory ring buffer per
55519
- * task. When exceeded, the oldest chunks are dropped.
55644
+ * task — applies to the live ring AND to the retired record's retained tail.
55645
+ * When exceeded, the oldest chunks are dropped.
55520
55646
  *
55521
55647
  * The ring buffer is a lightweight tail intended for the `/tasks` UI and
55522
55648
  * terminal notifications only — it deliberately discards old output to
@@ -55526,6 +55652,24 @@ function isBackgroundTaskTerminal(status) {
55526
55652
  * disk log via `getOutputSizeBytes` / `readOutputBytesFromDisk`.
55527
55653
  */
55528
55654
  const MAX_OUTPUT_BYTES$1 = 1024 * 1024;
55655
+ /**
55656
+ * How many terminal tasks stay addressable after eviction from `processes`.
55657
+ * The full output is on disk (`output.log`); the ring only keeps the metadata
55658
+ * needed to answer `getTask` / `getOutput` / `readOutput` for recently
55659
+ * finished tasks, FIFO-overflowing so long sessions stay bounded.
55660
+ */
55661
+ const RETIRED_TASK_LIMIT = 20;
55662
+ /**
55663
+ * Pre-cut bytes the bounded window read (`readOutput`) keeps as context for
55664
+ * `tailSlice`'s boundary rules, on top of the last `tail` code units it must
55665
+ * return. The rules inspect the text before the cut (the nearest ESC, a CSI
55666
+ * body run, a string-sequence opener), so a window starting exactly at the cut
55667
+ * would decide differently from the whole-file read in `getOutput`. Control
55668
+ * sequences in task output sit far closer to the cut than this margin; one
55669
+ * opened farther back is invisible to the window and is not worth paging the
55670
+ * rest of a multi-megabyte log for.
55671
+ */
55672
+ const TAIL_WINDOW_LOOKBACK_BYTES = 4096;
55529
55673
  const SIGTERM_GRACE_MS$2 = 5e3;
55530
55674
  const EXIT_SETTLE_GRACE_MS = 10;
55531
55675
  const _ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyz";
@@ -55556,6 +55700,12 @@ var BackgroundProcessManager = class {
55556
55700
  processes = /* @__PURE__ */ new Map();
55557
55701
  reservedTaskSlots = 0;
55558
55702
  /**
55703
+ * Terminal tasks evicted from `processes`, bounded to
55704
+ * `RETIRED_TASK_LIMIT` (FIFO). They stay addressable for `getTask` /
55705
+ * output reads (disk-backed) so a just-finished task is still visible.
55706
+ */
55707
+ retiredTasks = /* @__PURE__ */ new Map();
55708
+ /**
55559
55709
  * Ghosts: tasks loaded from disk during reconcile that have no live
55560
55710
  * JianProcess. They appear in `list()` / `getTask()` with status
55561
55711
  * `lost` so users see what was running before the crash/restart.
@@ -55713,10 +55863,12 @@ var BackgroundProcessManager = class {
55713
55863
  entry.lifecyclePromise;
55714
55864
  return taskId;
55715
55865
  }
55716
- /** Get info about a specific task. Falls back to reconcile ghosts. */
55866
+ /** Get info about a specific task. Falls back to retired, then reconcile ghosts. */
55717
55867
  getTask(taskId) {
55718
55868
  const entry = this.processes.get(taskId);
55719
55869
  if (entry !== void 0) return this.toInfo(entry);
55870
+ const retired = this.retiredTasks.get(taskId);
55871
+ if (retired !== void 0) return retired.info;
55720
55872
  return this.ghosts.get(taskId);
55721
55873
  }
55722
55874
  /**
@@ -55744,9 +55896,15 @@ var BackgroundProcessManager = class {
55744
55896
  result.push(this.toInfo(entry));
55745
55897
  if (limit !== void 0 && result.length >= limit) return result;
55746
55898
  }
55747
- if (!activeOnly) for (const ghost of this.ghosts.values()) {
55748
- result.push(ghost);
55749
- if (limit !== void 0 && result.length >= limit) return result;
55899
+ if (!activeOnly) {
55900
+ for (const retired of this.retiredTasks.values()) {
55901
+ result.push(retired.info);
55902
+ if (limit !== void 0 && result.length >= limit) return result;
55903
+ }
55904
+ for (const ghost of this.ghosts.values()) {
55905
+ result.push(ghost);
55906
+ if (limit !== void 0 && result.length >= limit) return result;
55907
+ }
55750
55908
  }
55751
55909
  return result;
55752
55910
  }
@@ -55761,8 +55919,12 @@ var BackgroundProcessManager = class {
55761
55919
  */
55762
55920
  async flushOutput(taskId) {
55763
55921
  const entry = this.processes.get(taskId);
55764
- if (entry === void 0) return;
55765
- await entry.outputWriteQueue;
55922
+ if (entry !== void 0) {
55923
+ await entry.outputWriteQueue;
55924
+ return;
55925
+ }
55926
+ const retired = this.retiredTasks.get(taskId);
55927
+ if (retired !== void 0) await retired.outputWriteQueue;
55766
55928
  }
55767
55929
  /**
55768
55930
  * Total byte size of a task's full output as stored on disk.
@@ -55822,35 +55984,48 @@ var BackgroundProcessManager = class {
55822
55984
  };
55823
55985
  }
55824
55986
  const entry = this.processes.get(taskId);
55825
- if (entry === void 0) return emptyOutputSnapshot();
55826
- const available = Buffer.from(entry.outputChunks.join(""), "utf-8");
55827
- const previewBytes = Math.min(previewLimit, available.byteLength, entry.outputSizeBytes);
55987
+ const retired = entry === void 0 ? this.retiredTasks.get(taskId) : void 0;
55988
+ const availableText = entry !== void 0 ? entry.outputChunks.join("") : retired?.outputChunks.join("") ?? "";
55989
+ const outputSizeBytes = entry !== void 0 ? entry.outputSizeBytes : retired?.outputSizeBytes ?? 0;
55990
+ if (entry === void 0 && retired === void 0) return emptyOutputSnapshot();
55991
+ const available = Buffer.from(availableText, "utf-8");
55992
+ const previewBytes = Math.min(previewLimit, available.byteLength, outputSizeBytes);
55828
55993
  const previewOffset = available.byteLength - previewBytes;
55829
55994
  return {
55830
- outputSizeBytes: entry.outputSizeBytes,
55995
+ outputSizeBytes,
55831
55996
  previewBytes,
55832
- truncated: entry.outputSizeBytes > previewBytes,
55997
+ truncated: outputSizeBytes > previewBytes,
55833
55998
  fullOutputAvailable: false,
55834
55999
  preview: available.subarray(previewOffset).toString("utf-8")
55835
56000
  };
55836
56001
  }
55837
- /** Get the combined output of a task (tail of the ring buffer). */
56002
+ /** Get the combined output of a task (live ring buffer, else the disk log). */
55838
56003
  getOutput(taskId, tail) {
55839
56004
  const entry = this.processes.get(taskId);
55840
- if (!entry) return "";
55841
- const full = entry.outputChunks.join("");
55842
- if (tail !== void 0 && tail < full.length) return full.slice(-tail);
56005
+ if (entry) {
56006
+ const full = entry.outputChunks.join("");
56007
+ if (tail !== void 0 && tail < full.length) return tailSlice(full, tail);
56008
+ return full;
56009
+ }
56010
+ const retired = this.retiredTasks.get(taskId);
56011
+ if (retired === void 0) return "";
56012
+ let full = retired.outputChunks.join("");
56013
+ if (retired.outputSessionDir !== void 0) try {
56014
+ const persisted = readFileSync(taskOutputFile(retired.outputSessionDir, taskId), "utf-8");
56015
+ if (persisted.length > 0) full = persisted;
56016
+ } catch {}
56017
+ if (tail !== void 0 && tail < full.length) return tailSlice(full, tail);
55843
56018
  return full;
55844
56019
  }
55845
56020
  async readOutput(taskId, tail) {
55846
56021
  const entry = this.processes.get(taskId);
55847
56022
  const outputSessionDir = this.outputSessionDirFor(taskId);
55848
56023
  if (outputSessionDir !== void 0) {
55849
- await entry?.outputWriteQueue;
56024
+ await (entry?.outputWriteQueue ?? this.retiredTasks.get(taskId)?.outputWriteQueue);
55850
56025
  if (tail !== void 0 && tail > 0) {
55851
56026
  const size = await taskOutputSizeBytes(outputSessionDir, taskId);
55852
56027
  if (size > 0) {
55853
- const windowBytes = Math.min(size, tail * 4 + 7);
56028
+ const windowBytes = Math.min(size, tail * 4 + 7 + TAIL_WINDOW_LOOKBACK_BYTES);
55854
56029
  const start = size - windowBytes;
55855
56030
  let persisted = await readTaskOutputBytes(outputSessionDir, taskId, start, windowBytes);
55856
56031
  if (persisted.length > 0) {
@@ -55859,13 +56034,13 @@ var BackgroundProcessManager = class {
55859
56034
  while (lead < persisted.length && persisted.codePointAt(lead) === 65533 && lead < 3) lead += 1;
55860
56035
  if (lead > 0) persisted = persisted.slice(lead);
55861
56036
  }
55862
- return tail < persisted.length ? persisted.slice(-tail) : persisted;
56037
+ return tail < persisted.length ? tailSlice(persisted, tail) : persisted;
55863
56038
  }
55864
56039
  return this.getOutput(taskId, tail);
55865
56040
  }
55866
56041
  } else {
55867
56042
  const persisted = await readTaskOutput(outputSessionDir, taskId);
55868
- if (persisted.length > 0) return tail !== void 0 && tail < persisted.length ? persisted.slice(-tail) : persisted;
56043
+ if (persisted.length > 0) return tail !== void 0 && tail < persisted.length ? tailSlice(persisted, tail) : persisted;
55869
56044
  }
55870
56045
  }
55871
56046
  return this.getOutput(taskId, tail);
@@ -55876,10 +56051,11 @@ var BackgroundProcessManager = class {
55876
56051
  if (!taskOutputExistsSync(outputSessionDir, taskId)) return void 0;
55877
56052
  return taskOutputFile(outputSessionDir, taskId);
55878
56053
  }
55879
- /** Stop a running task. SIGTERM → 5s grace → SIGKILL. */
56054
+ /** Stop a running task. SIGTERM → 5s grace → SIGKILL. Active-only: a
56055
+ * retired/ghost task is a pure no-op read of its terminal info. */
55880
56056
  async stop(taskId, reason) {
55881
56057
  const entry = this.processes.get(taskId);
55882
- if (!entry) return void 0;
56058
+ if (!entry) return this.retiredTasks.get(taskId)?.info ?? this.ghosts.get(taskId);
55883
56059
  const trimmedReason = reason?.trim();
55884
56060
  const stopReason = trimmedReason === void 0 || trimmedReason.length === 0 ? void 0 : trimmedReason;
55885
56061
  if (TERMINAL_STATUSES.has(entry.status)) {
@@ -55923,7 +56099,7 @@ var BackgroundProcessManager = class {
55923
56099
  */
55924
56100
  async wait(taskId, timeoutMs = 3e4) {
55925
56101
  const entry = this.processes.get(taskId);
55926
- if (!entry) return void 0;
56102
+ if (!entry) return this.retiredTasks.get(taskId)?.info;
55927
56103
  if (TERMINAL_STATUSES.has(entry.status)) {
55928
56104
  await entry.persistWriteQueue;
55929
56105
  return this.toInfo(entry);
@@ -56077,13 +56253,13 @@ var BackgroundProcessManager = class {
56077
56253
  * already terminal, resolves synchronously on the next microtask.
56078
56254
  * Intended for integration code that wants to `await` a specific
56079
56255
  * task's exit without installing a full `onTerminal` subscriber.
56080
- * Returns `undefined` for unknown ids (matching `getTask`). Ghost
56081
- * (reconciled-lost) entries are considered terminal from the
56082
- * manager's perspective.
56256
+ * Returns `undefined` for unknown ids (matching `getTask`). Retired
56257
+ * (evicted-but-recently-finished) and ghost (reconciled-lost) entries
56258
+ * are already terminal, so they resolve to their info instead.
56083
56259
  */
56084
56260
  async waitForTerminal(taskId) {
56085
56261
  const entry = this.processes.get(taskId);
56086
- if (entry === void 0) return this.ghosts.get(taskId);
56262
+ if (entry === void 0) return this.retiredTasks.get(taskId)?.info ?? this.ghosts.get(taskId);
56087
56263
  if (TERMINAL_STATUSES.has(entry.status)) {
56088
56264
  await entry.persistWriteQueue;
56089
56265
  return this.toInfo(entry);
@@ -56097,9 +56273,18 @@ var BackgroundProcessManager = class {
56097
56273
  /** Reset internal state (for testing). */
56098
56274
  _reset() {
56099
56275
  this.processes.clear();
56276
+ this.retiredTasks.clear();
56100
56277
  this.ghosts.clear();
56101
56278
  this.sessionDir = void 0;
56102
56279
  }
56280
+ /** Live (non-terminal) task occupancy. */
56281
+ get liveTaskCount() {
56282
+ return this.processes.size;
56283
+ }
56284
+ /** Retired-ring occupancy (≤ `RETIRED_TASK_LIMIT`). */
56285
+ get retiredTaskCount() {
56286
+ return this.retiredTasks.size;
56287
+ }
56103
56288
  /**
56104
56289
  * Attach the manager to a session directory for persistence. Tasks
56105
56290
  * created via `register()` after this call are written to
@@ -56158,9 +56343,10 @@ var BackgroundProcessManager = class {
56158
56343
  for (const info of result.lostInfo) this.fireTerminalSubscribers(info);
56159
56344
  return result;
56160
56345
  }
56161
- /** Drop a persisted task from disk and ghost map. */
56346
+ /** Drop a persisted task from disk and ghost/retired maps. */
56162
56347
  async forgetTask(taskId) {
56163
56348
  this.ghosts.delete(taskId);
56349
+ this.retiredTasks.delete(taskId);
56164
56350
  if (this.sessionDir !== void 0) await removeTask(this.sessionDir, taskId);
56165
56351
  }
56166
56352
  /**
@@ -56190,21 +56376,66 @@ var BackgroundProcessManager = class {
56190
56376
  return entry.persistWriteQueue;
56191
56377
  }
56192
56378
  appendOutput(entry, chunk) {
56193
- entry.outputSizeBytes += Buffer.byteLength(chunk, "utf-8");
56194
- entry.outputChunks.push(chunk);
56195
- let total = entry.outputChunks.reduce((s, c) => s + c.length, 0);
56196
- while (total > MAX_OUTPUT_BYTES$1 && entry.outputChunks.length > 1) {
56197
- const removed = entry.outputChunks.shift();
56198
- if (removed === void 0) break;
56199
- total -= removed.length;
56379
+ const chunkBytes = Buffer.byteLength(chunk, "utf-8");
56380
+ let retired;
56381
+ if (this.processes.get(entry.taskId) === entry) {
56382
+ entry.outputSizeBytes += chunkBytes;
56383
+ entry.outputChunks.push(chunk);
56384
+ let total = entry.outputChunks.reduce((s, c) => s + c.length, 0);
56385
+ while (total > MAX_OUTPUT_BYTES$1 && entry.outputChunks.length > 1) {
56386
+ const removed = entry.outputChunks.shift();
56387
+ if (removed === void 0) break;
56388
+ total -= removed.length;
56389
+ }
56390
+ } else {
56391
+ retired = this.retiredTasks.get(entry.taskId);
56392
+ if (retired === void 0) return;
56393
+ retired.outputSizeBytes += chunkBytes;
56394
+ this.appendRetiredChunk(retired, chunk);
56200
56395
  }
56201
56396
  const outputSessionDir = entry.outputSessionDir;
56202
56397
  if (outputSessionDir === void 0) return;
56203
56398
  entry.outputWriteQueue = entry.outputWriteQueue.then(() => appendTaskOutput(outputSessionDir, entry.taskId, chunk)).catch(() => {});
56399
+ if (retired !== void 0) retired.outputWriteQueue = entry.outputWriteQueue;
56400
+ }
56401
+ /**
56402
+ * Append a late post-retirement chunk to the retired tail under the same
56403
+ * `MAX_OUTPUT_BYTES` budget as the live ring: over budget the oldest chunks
56404
+ * are dropped from the head while the newest output is kept. The chunk array
56405
+ * is trimmed in place and joined lazily on read, so one late chunk costs
56406
+ * O(chunk) — never the O(retained) rebuild a per-chunk `text += chunk`
56407
+ * would pay.
56408
+ */
56409
+ appendRetiredChunk(retired, chunk) {
56410
+ retired.outputChunks.push(chunk);
56411
+ retired.outputTextBytes += Buffer.byteLength(chunk, "utf-8");
56412
+ this.trimRetiredOutput(retired);
56413
+ }
56414
+ /**
56415
+ * Head-trim the retired tail back under `MAX_OUTPUT_BYTES` (tail = newest
56416
+ * output kept), mirroring the live ring's policy. Accounts in UTF-8 bytes
56417
+ * rather than UTF-16 code units so multi-byte output cannot exceed the
56418
+ * budget either.
56419
+ */
56420
+ trimRetiredOutput(retired) {
56421
+ while (retired.outputTextBytes > MAX_OUTPUT_BYTES$1 && retired.outputChunks.length > 1) {
56422
+ const removed = retired.outputChunks.shift();
56423
+ if (removed === void 0) break;
56424
+ retired.outputTextBytes -= Buffer.byteLength(removed, "utf-8");
56425
+ }
56426
+ if (retired.outputTextBytes > MAX_OUTPUT_BYTES$1) {
56427
+ const only = retired.outputChunks[0];
56428
+ if (only === void 0) return;
56429
+ const tail = utf8ByteTail(only, MAX_OUTPUT_BYTES$1);
56430
+ retired.outputChunks[0] = tail;
56431
+ retired.outputTextBytes = Buffer.byteLength(tail, "utf-8");
56432
+ }
56204
56433
  }
56205
56434
  outputSessionDirFor(taskId) {
56206
56435
  const entry = this.processes.get(taskId);
56207
56436
  if (entry !== void 0) return entry.outputSessionDir;
56437
+ const retired = this.retiredTasks.get(taskId);
56438
+ if (retired !== void 0) return retired.outputSessionDir;
56208
56439
  if (this.ghosts.has(taskId)) return this.sessionDir;
56209
56440
  }
56210
56441
  async settleProcessExit(entry, exitCode) {
@@ -56215,12 +56446,22 @@ var BackgroundProcessManager = class {
56215
56446
  await this.persistLive(entry);
56216
56447
  this.fireTerminalCallbacks(entry);
56217
56448
  this.resolveWaiters(entry);
56449
+ this.syncRetired(entry);
56218
56450
  }
56219
56451
  return;
56220
56452
  }
56221
56453
  const status = entry.stopRequested ? "killed" : exitCode === 0 ? "completed" : "failed";
56222
56454
  await this.finalizeTerminal(entry, status, exitCode);
56223
56455
  }
56456
+ /** Refresh the retired-ring snapshot after a post-terminal field update. */
56457
+ syncRetired(entry) {
56458
+ const retired = this.retiredTasks.get(entry.taskId);
56459
+ if (retired === void 0) return;
56460
+ this.retiredTasks.set(entry.taskId, {
56461
+ ...retired,
56462
+ info: this.toInfo(entry)
56463
+ });
56464
+ }
56224
56465
  observedExitCompletions() {
56225
56466
  const completions = [];
56226
56467
  for (const entry of this.processes.values()) if (!TERMINAL_STATUSES.has(entry.status) && entry.proc.exitCode !== null) completions.push(entry.lifecyclePromise);
@@ -56257,9 +56498,277 @@ var BackgroundProcessManager = class {
56257
56498
  await this.persistLive(entry);
56258
56499
  this.fireTerminalCallbacks(entry);
56259
56500
  this.resolveWaiters(entry);
56501
+ await entry.outputWriteQueue;
56502
+ const retainedChunks = entry.outputChunks.splice(0);
56503
+ const retired = {
56504
+ info: this.toInfo(entry),
56505
+ outputChunks: retainedChunks,
56506
+ outputTextBytes: retainedChunks.reduce((total, chunk) => total + Buffer.byteLength(chunk, "utf-8"), 0),
56507
+ outputSizeBytes: entry.outputSizeBytes,
56508
+ outputSessionDir: entry.outputSessionDir,
56509
+ outputWriteQueue: entry.outputWriteQueue
56510
+ };
56511
+ this.trimRetiredOutput(retired);
56512
+ this.retiredTasks.set(entry.taskId, retired);
56513
+ while (this.retiredTasks.size > RETIRED_TASK_LIMIT) {
56514
+ const oldest = this.retiredTasks.keys().next().value;
56515
+ if (oldest === void 0) break;
56516
+ this.retiredTasks.delete(oldest);
56517
+ }
56518
+ this.processes.delete(entry.taskId);
56260
56519
  return true;
56261
56520
  }
56262
56521
  };
56522
+ const ESC$2 = "\x1B";
56523
+ /**
56524
+ * Largest share of a retained tail the boundary rules may discard.
56525
+ *
56526
+ * Both rules trade content for a head that is whole by construction: rule 1
56527
+ * restarts after the next `\n` and drops the torn line, and rule 2 skips the
56528
+ * remainder of a torn sequence. The trade only pays while the tail keeps a
56529
+ * (non-strict) majority — with one 5000-char line ahead, a 100-unit budget
56530
+ * would come back with the few units after it, and the caller asked for the
56531
+ * newest output, not for a tidy head. Past the bound the raw cut keeps the
56532
+ * content: a string payload stays visible, and a torn CSI or two-byte escape
56533
+ * run keeps the fragment it left in view, because skipping that remainder
56534
+ * would cost more than the fragment it hides — unless the introducer ESC sits
56535
+ * immediately before the head *and* the sequence provably closes, where one
56536
+ * unit of look-back keeps the sequence whole and the fragment never surfaces.
56537
+ * An unclosed run keeps the raw cut: with no terminator to hand the sanitizer,
56538
+ * backing onto its ESC would surface a bare ESC the sanitizer cannot strip.
56539
+ */
56540
+ const MAX_TAIL_DROP_SHARE$1 = .5;
56541
+ /**
56542
+ * `text.slice(-tail)` (clamping exactly like `slice` does for `tail` outside
56543
+ * `(0, text.length)`), with the head moved to a boundary no ANSI escape
56544
+ * sequence spans, so the first bytes a reader sees are never a torn fragment.
56545
+ */
56546
+ function tailSlice(text, tail) {
56547
+ if (tail <= 0 || tail >= text.length) return text;
56548
+ return text.slice(safeTailStart$1(text, text.length - tail));
56549
+ }
56550
+ /**
56551
+ * Move a tail cut point forward to a boundary no ANSI escape sequence spans.
56552
+ *
56553
+ * A raw `slice(-tail)` can land inside a sequence (`…\u001b[31|m…`), leaving a
56554
+ * fragment (`m`, `0m`, `[31`) whose ESC byte was cut away — the TUI sanitizer
56555
+ * only recognizes complete sequences, so the fragment renders as text. Two
56556
+ * provable rules keep the head clean:
56557
+ *
56558
+ * 1. Line alignment: a CSI sequence cannot contain `\n` (LF is not a
56559
+ * parameter, intermediate or final byte), so a cut that lands mid-line
56560
+ * restarts after the next `\n`. The only loss is the torn partial line,
56561
+ * whose head cannot be trusted; every escape after the newline is whole.
56562
+ * Bounded by `MAX_TAIL_DROP_SHARE`, so one huge line cannot eat the budget.
56563
+ * 2. Torn-sequence skip: when the retained text is one line (no `\n` after
56564
+ * the cut), or an OSC/DCS/PM/APC payload spans the newline the cut aligned
56565
+ * to, the dropped prefix is inspected for a sequence still open at the cut
56566
+ * — a run of CSI body bytes (0x20–0x3F) reaching back to `ESC [`, an `ESC`
56567
+ * whose intermediate bytes were torn (`ESC ( B`), or a string-sequence
56568
+ * opener the cut landed inside. The head then skips exactly the bytes a
56569
+ * terminal would consume for that sequence, so no visible text is dropped —
56570
+ * but only while that skip stays within `MAX_TAIL_DROP_SHARE` of the tail.
56571
+ * A CSI body or escape-intermediate run can be arbitrarily long, so past
56572
+ * the bound the head keeps the newest bytes and the fragment the raw cut
56573
+ * left with them — or one unit earlier, onto an introducer ESC sitting
56574
+ * immediately before the head, so the whole sequence survives and is
56575
+ * stripped (`withinDropShare`) — but only when the skip proved the sequence
56576
+ * closed: an unclosed run keeps the raw cut, because with no terminator to
56577
+ * hand the sanitizer the ESC would surface bare and swallow what follows
56578
+ * it. So does the head for a string that never ends or ended ahead of the
56579
+ * cut. A sliver of the tail is worse than a fragment.
56580
+ *
56581
+ * The result never splits a surrogate pair, and a non-empty input never comes
56582
+ * back empty: the head is never below `cut` (a tail never grows) except in two
56583
+ * cases — the cut splits a trailing pair, where dropping the orphan half would
56584
+ * empty the tail, so the head backs onto the pair's first half and the last
56585
+ * character survives; and a torn sequence's introducer ESC sits immediately
56586
+ * before the head *and* the sequence's skip proved it closes, where backing
56587
+ * one unit onto it keeps the sequence whole and the fragment is stripped
56588
+ * instead of surfaced. An unclosed skip takes the raw cut.
56589
+ */
56590
+ function safeTailStart$1(text, cut) {
56591
+ if (cut <= 0 || cut >= text.length) return cut;
56592
+ const tail = text.length - cut;
56593
+ let start = cut;
56594
+ if (text[start - 1] !== "\n") {
56595
+ const newline = text.indexOf("\n", start);
56596
+ if (newline !== -1 && newline + 1 - cut <= tail * MAX_TAIL_DROP_SHARE$1) start = newline + 1;
56597
+ }
56598
+ start = skipOpenEscape$1(text, start);
56599
+ const head = text.codePointAt(start);
56600
+ if (head !== void 0 && head >= 56320 && head <= 57343) return start + 1 < text.length ? start + 1 : start - 1;
56601
+ return start;
56602
+ }
56603
+ /** CSI parameter (0x30–0x3F) / intermediate (0x20–0x2F) bytes — everything before the final byte. */
56604
+ function isCsiBody$1(code) {
56605
+ return code !== void 0 && code >= 32 && code <= 63;
56606
+ }
56607
+ /** CSI final byte (0x40–0x7E) — the sequence ends here. */
56608
+ function isCsiFinal$1(code) {
56609
+ return code !== void 0 && code >= 64 && code <= 126;
56610
+ }
56611
+ /** OSC / DCS / PM / APC introducers — string sequences ended by BEL or ST. */
56612
+ function isStringIntro$1(intro) {
56613
+ return intro === "]" || intro === "P" || intro === "^" || intro === "_";
56614
+ }
56615
+ /** ESC-intermediate byte (0x20–0x2F) of a two-byte escape sequence. */
56616
+ function isEscapeIntermediate$1(code) {
56617
+ return code !== void 0 && code >= 32 && code <= 47;
56618
+ }
56619
+ /** Final byte (0x30–0x7E) that closes a two-byte escape sequence. */
56620
+ function isEscapeFinal$1(code) {
56621
+ return code !== void 0 && code >= 48 && code <= 126;
56622
+ }
56623
+ /** True when every code point in the half-open range `[from, to)` is an ESC intermediate (0x20–0x2F). */
56624
+ function allEscapeIntermediates$1(text, from, to) {
56625
+ for (let i = from; i < to; i += 1) if (!isEscapeIntermediate$1(text.codePointAt(i))) return false;
56626
+ return true;
56627
+ }
56628
+ /**
56629
+ * When the dropped prefix proves an escape sequence is still open at `cut`,
56630
+ * return the index after its remainder; otherwise return `cut` unchanged.
56631
+ */
56632
+ function skipOpenEscape$1(text, cut) {
56633
+ let run = cut;
56634
+ while (run > 0 && isCsiBody$1(text.codePointAt(run - 1))) run -= 1;
56635
+ if (run >= 2 && text[run - 1] === "[" && text[run - 2] === ESC$2) {
56636
+ const skip = skipCsiRest$1(text, cut);
56637
+ return withinDropShare$1(text, cut, skip.end, skip.closed);
56638
+ }
56639
+ if (text[run - 1] === ESC$2 && allEscapeIntermediates$1(text, run, cut)) return skipEscapeRest$1(text, cut);
56640
+ const open = text.lastIndexOf(ESC$2, cut - 1);
56641
+ if (open !== -1 && isStringIntro$1(text[open + 1])) return stringSequenceHead$1(text, open, cut);
56642
+ return cut;
56643
+ }
56644
+ /**
56645
+ * Keep a provable skip only while it stays within `MAX_TAIL_DROP_SHARE` of the
56646
+ * tail that starts at `from`: the skipped bytes are exactly what the reader
56647
+ * loses from the tail it asked for. A CSI body or escape-intermediate run can
56648
+ * be arbitrarily long, so past the bound the raw cut is worth more than the
56649
+ * clean head — except when the sequence's introducer ESC sits immediately
56650
+ * before the head (`…\u001b|[31m…`) *and* `closed` says the skip found the
56651
+ * sequence's terminator, where one unit of look-back keeps the sequence whole
56652
+ * instead: the sanitizer strips it and the visible text is the skip's, for
56653
+ * that one unit rather than a sliver of the tail. An unclosed run must keep
56654
+ * the raw cut even with that ESC one unit ahead: nothing terminates it, so
56655
+ * look-back would hand the reader a bare ESC whose sequence run a terminal
56656
+ * swallows along with the bytes behind it.
56657
+ */
56658
+ function withinDropShare$1(text, from, target, closed) {
56659
+ if (target - from <= (text.length - from) * MAX_TAIL_DROP_SHARE$1) return target;
56660
+ return closed && from > 0 && text[from - 1] === ESC$2 ? from - 1 : from;
56661
+ }
56662
+ /**
56663
+ * The rest of a CSI sequence whose body was torn at `cut`, with the closure
56664
+ * its skip proved: `closed` holds only when a final byte (0x40–0x7E) was
56665
+ * actually found, so a parameter run that reaches the end of `text` — or stops
56666
+ * on a byte no CSI can end with — reports the sequence as open.
56667
+ */
56668
+ function skipCsiRest$1(text, cut) {
56669
+ let end = cut;
56670
+ while (end < text.length && isCsiBody$1(text.codePointAt(end))) end += 1;
56671
+ const closed = end < text.length && isCsiFinal$1(text.codePointAt(end));
56672
+ return {
56673
+ end: closed ? end + 1 : end,
56674
+ closed
56675
+ };
56676
+ }
56677
+ /** Index after the escape sequence whose introducer ESC sits at `cut - 1`. */
56678
+ function skipEscapeRest$1(text, cut) {
56679
+ const intro = text[cut];
56680
+ if (intro === "[") {
56681
+ const skip = skipCsiRest$1(text, cut + 1);
56682
+ return withinDropShare$1(text, cut, skip.end, skip.closed);
56683
+ }
56684
+ if (isStringIntro$1(intro)) return stringSequenceHead$1(text, cut - 1, cut + 1);
56685
+ let end = cut;
56686
+ while (end < text.length && isEscapeIntermediate$1(text.codePointAt(end))) end += 1;
56687
+ const closed = end < text.length && isEscapeFinal$1(text.codePointAt(end));
56688
+ return withinDropShare$1(text, cut, closed ? end + 1 : end, closed);
56689
+ }
56690
+ /**
56691
+ * Head index for a cut that lands inside the string sequence opened at `open`.
56692
+ *
56693
+ * `stringEnd` gives the byte a terminal stops at; skipping to it hides the
56694
+ * payload's remainder, which is what a terminal shows — but only while that
56695
+ * costs at most `MAX_TAIL_DROP_SHARE` of the tail. A long OSC 8 URL, or any
56696
+ * payload longer than the budget, would otherwise hand the caller a sliver of
56697
+ * the tail it asked for; a payload whose extent exceeds the budget stays
56698
+ * visible instead. A cut the string already ended ahead of needs no skip, and
56699
+ * an unterminated one must not swallow the tail (`stringPayloadHead`).
56700
+ */
56701
+ function stringSequenceHead$1(text, open, cut) {
56702
+ const drop = stringEnd$1(text, open + 2) - cut;
56703
+ if (drop >= 0 && drop <= (text.length - cut) * MAX_TAIL_DROP_SHARE$1) return cut + drop;
56704
+ return stringPayloadHead$1(text, open, cut);
56705
+ }
56706
+ /**
56707
+ * Head index that keeps a string sequence's payload visible, moving the cut
56708
+ * only past the opener's own bytes when the cut landed inside them.
56709
+ *
56710
+ * A terminal swallows an unterminated payload, but a viewer must never render
56711
+ * a blank body: `tailSlice` backs the tasks-browser tail read, and a log that
56712
+ * opens with an unterminated OSC — a run killed mid-title, a truncated write —
56713
+ * would otherwise come back empty. The head stays at `cut` unless the cut
56714
+ * landed inside the opener (`\u001b]0;` would leave `0;` in front of the
56715
+ * payload), and never reaches `text.length` — the clamp to the last unit steps
56716
+ * back onto the pair's first half when that unit is a trailing surrogate
56717
+ * pair's low half — so the tail is empty only for empty input.
56718
+ */
56719
+ function stringPayloadHead$1(text, open, cut) {
56720
+ const head = Math.min(Math.max(cut, stringPayloadStart$1(text, open)), text.length - 1);
56721
+ const code = text.codePointAt(head);
56722
+ return head > 0 && code !== void 0 && code >= 56320 && code <= 57343 ? head - 1 : head;
56723
+ }
56724
+ /**
56725
+ * Index after the byte that ends the string sequence whose payload starts at
56726
+ * `from` — its BEL or `ESC \` terminator, or a CAN (0x18) / SUB (0x1A), which
56727
+ * a terminal reads as "string over, resume normal parsing". `-1` when the
56728
+ * string never ends.
56729
+ */
56730
+ function stringEnd$1(text, from) {
56731
+ for (let i = from; i < text.length; i += 1) {
56732
+ const code = text.codePointAt(i);
56733
+ if (code === 7) return i + 1;
56734
+ if (code === 27 && text[i + 1] === "\\") return i + 2;
56735
+ if (code === 24 || code === 26) return i + 1;
56736
+ }
56737
+ return -1;
56738
+ }
56739
+ /**
56740
+ * Index where the payload of the string sequence opened at `open` starts: for
56741
+ * OSC after `ESC ] Ps ;`, for DCS/PM/APC after their parameter and
56742
+ * intermediate bytes and the final byte that opens the payload.
56743
+ */
56744
+ function stringPayloadStart$1(text, open) {
56745
+ let i = open + 2;
56746
+ if (text[open + 1] === "]") {
56747
+ while (i < text.length && isOscParam$1(text.codePointAt(i))) i += 1;
56748
+ if (text[i] === ";") i += 1;
56749
+ return i;
56750
+ }
56751
+ while (i < text.length && isCsiBody$1(text.codePointAt(i))) i += 1;
56752
+ const final = text.codePointAt(i);
56753
+ return i < text.length && isCsiFinal$1(final) ? i + 1 : i;
56754
+ }
56755
+ /** OSC numeric parameter byte (0x30–0x39) — the `Ps` before the `;` separator. */
56756
+ function isOscParam$1(code) {
56757
+ return code !== void 0 && code >= 48 && code <= 57;
56758
+ }
56759
+ /**
56760
+ * Longest suffix of `text` whose UTF-8 encoding fits in `maxBytes`. Encoding
56761
+ * through a Buffer keeps lone surrogates accounted as the 3-byte replacement
56762
+ * character, and the cut is moved to the next code point boundary (continuation
56763
+ * bytes `10xxxxxx` skipped) so the tail never starts mid-sequence.
56764
+ */
56765
+ function utf8ByteTail(text, maxBytes) {
56766
+ const encoded = Buffer.from(text, "utf-8");
56767
+ if (encoded.byteLength <= maxBytes) return text;
56768
+ let start = encoded.byteLength - maxBytes;
56769
+ while (start < encoded.byteLength && ((encoded.at(start) ?? 0) & 192) === 128) start += 1;
56770
+ return encoded.subarray(start).toString("utf-8");
56771
+ }
56263
56772
  function persistedToInfo(t) {
56264
56773
  return {
56265
56774
  taskId: t.task_id,
@@ -59579,7 +60088,7 @@ var agent_background_disabled_default = "Background agent execution is disabled
59579
60088
  var agent_background_enabled_default = "When `run_in_background=true`, the subagent runs detached from this turn. The completion arrives in a later turn as a synthetic user-role message containing its result — you do not need to poll, sleep, or check on its progress. Continue with other work or respond to the user. Never fabricate or predict what the result will say.\n\nWhile it runs, the subagent can ask you questions via `ContactParent`. You will see them at your next step boundary, and a `SendSubagentMessage` reply reaches it mid-run — this is the mode that supports mid-course steering. A foreground subagent's questions, in contrast, only reach you after it completes.\n\nFor a background task, when `timeout` is omitted it falls back to the operator-configured background timeout, if one is set. If the operator has not configured a background timeout, an omitted `timeout` means the task runs with no time limit.\n";
59580
60089
  //#endregion
59581
60090
  //#region ../../packages/agent-core/src/tools/builtin/collaboration/agent.md
59582
- var agent_default = "Launch a subagent to handle a focused task. Prefer this tool over doing the work yourself when the task matches one of the specialists listed below.\n\nSpecialist subagents are listed at the end of this description under \"Available agent types\" — that list is generated from the active profiles and carries each specialist's own USE WHEN / NOT FOR triggers. Read it before choosing a `subagent_type`; never assume a type exists.\n\n## Required prompt structure\n\nThe final prompt sent to the subagent MUST contain these sections. Provide them either by writing them directly into the `prompt` field, or by using the structured `target`, `change`, and `acceptance` fields — they will be appended to `prompt` automatically.\n\n```markdown\n# Target\nExact files, symbols, or directories to touch. Explicit non-goals.\n\n# Change\nStep-by-step what to add, remove, or modify. Include concrete examples when possible.\n\n# Acceptance\nObservable result that proves completion: a passing test, a build command, a specific file content, or a verification step the subagent must run.\n```\n\nOmitting a section causes the subagent to miss context and increases the chance of a wrong or incomplete result.\n\nWriting the prompt:\n- The subagent starts with zero context — it has not seen this conversation. Brief it like a colleague who just walked into the room: state the goal, list what you already know, hand over the specifics.\n- Lookups (read this file, run that test): put the exact path or command in the prompt. The subagent should not have to search for things you already know.\n- Investigations (figure out X, find why Y): give the question, not prescribed steps — fixed steps become dead weight when the premise is wrong.\n- Do not delegate understanding. If the task hinges on a file path or line number, find it yourself first and write it into the prompt.\n- The `Acceptance` section is not optional. The subagent MUST verify against it before returning.\n\nUsage notes:\n- When the task continues earlier work a subagent already did, prefer resuming that agent (pass its `resume` id) over spawning a fresh instance — the resumed agent keeps its prior context.\n- A subagent's result is only visible to you, not to the user. When the user needs to see what a subagent produced, summarize the relevant parts yourself in your own reply.\n\n## Structured output\n\nWhen you need a machine-readable result (not a free-form summary), pass `output_schema` with a JSON Schema string. The subagent is instructed to reply with a single JSON object matching the schema; if the reply parses, it is surfaced as a `[structured]` block in the tool output. Use `output_token_hint` to keep structured replies compact (e.g. 1024 for a schema-shaped answer).\n\nExample:\n```\nAgent(prompt=\"Extract the test commands from this project\", output_schema='{\"type\":\"object\",\"properties\":{\"commands\":{\"type\":\"array\",\"items\":{\"type\":\"string\"}}}}')\n```\n\n## Capability constraints\n\nBy default a subagent gets its profile's full tool set. Pass `capability_mode` to restrict it at the tool level (not just by prompting):\n\n- `read-only` — inspection, search, web/memory lookups, and reporting only. No file writes, no command execution, no spawning further agents.\n- `read-write` — additionally file/memory writes. No command execution, no spawning.\n- `execute` — additionally command execution (bash, python). Still no spawning of further agents.\n- `all` — full profile tool set (default).\n\nRestricted modes also remove `Agent` and `SendSubagentMessage`, so a constrained subagent cannot spawn an unconstrained grandchild to bypass the filter.\n\n## Steering running subagents\n\nUse `SendSubagentMessage` to send a directed message to a subagent you own while it is still running: `steer` for a priority redirection, `queue` for context that applies next turn. A `steer` joins the subagent's running turn at its next step boundary; a `queue` message is delivered when the subagent starts its next turn. Only the owning parent may message a subagent.\n\nWhen NOT to use Agent: skip delegation for trivial one-step work (e.g. reading a known file). Almost everything else is a candidate for delegation.\n\nOnce a subagent is running, leave that scope to it: do not redo its searches or reads in parallel, and do not abandon it midway and finish the job manually. Both undo the context savings the delegation was meant to buy.\n\n## Foreground timeouts auto-background\n\nA `timeout` on a foreground `Agent` call bounds your wait, not the subagent's life. When the deadline fires while the child is still running, it is handed to the background task manager (status: backgrounded) instead of being aborted: the tool returns a `task_id`, its completion notification arrives automatically in a later turn (no polling), and you can peek with `TaskOutput(task_id=..., block=false)` or stop it with `TaskStop`. User cancellation still aborts immediately. A stopped (TaskStop) background task never suggests resume; only tasks that finish or fail on their own are recoverable via `Agent(resume=...)`.";
60091
+ var agent_default = "Launch a subagent to handle a focused task. Prefer this tool over doing the work yourself when the task matches one of the specialists listed below.\n\nSpecialist subagents are listed at the end of this description under \"Available agent types\" — that list is generated from the active profiles and carries each specialist's own USE WHEN / NOT FOR triggers. Read it before choosing a `subagent_type`; never assume a type exists.\n\n## Required prompt structure\n\nThe final prompt sent to the subagent MUST contain these sections. Provide them either by writing them directly into the `prompt` field, or by using the structured `target`, `change`, and `acceptance` fields — they will be appended to `prompt` automatically.\n\n```markdown\n# Target\nExact files, symbols, or directories to touch. Explicit non-goals.\n\n# Change\nStep-by-step what to add, remove, or modify. Include concrete examples when possible.\n\n# Acceptance\nObservable result that proves completion: a passing test, a build command, a specific file content, or a verification step the subagent must run.\n```\n\nOmitting a section causes the subagent to miss context and increases the chance of a wrong or incomplete result.\n\nWriting the prompt:\n- The subagent starts with zero context — it has not seen this conversation. Brief it like a colleague who just walked into the room: state the goal, list what you already know, hand over the specifics.\n- Lookups (read this file, run that test): put the exact path or command in the prompt. The subagent should not have to search for things you already know.\n- Investigations (figure out X, find why Y): give the question, not prescribed steps — fixed steps become dead weight when the premise is wrong.\n- Do not delegate understanding. If the task hinges on a file path or line number, find it yourself first and write it into the prompt.\n- The `Acceptance` section is not optional. The subagent MUST verify against it before returning.\n\nUsage notes:\n- When the task continues earlier work a subagent already did, prefer resuming that agent (pass its `resume` id) over spawning a fresh instance — the resumed agent keeps its prior context.\n- A subagent's result is only visible to you, not to the user. When the user needs to see what a subagent produced, summarize the relevant parts yourself in your own reply.\n\n## Structured output\n\nWhen you need a machine-readable result (not a free-form summary), pass `output_schema` with a JSON Schema string. The subagent is instructed to reply with a single JSON object matching the schema; if the reply parses, it is surfaced as a `[structured]` block in the tool output. Use `output_token_hint` to keep structured replies compact (e.g. 1024 for a schema-shaped answer).\n\nExample:\n```\nAgent(prompt=\"Extract the test commands from this project\", output_schema='{\"type\":\"object\",\"properties\":{\"commands\":{\"type\":\"array\",\"items\":{\"type\":\"string\"}}}}')\n```\n\n## Capability constraints\n\nBy default a subagent gets its profile's full tool set. Pass `capability_mode` to restrict it at the tool level (not just by prompting):\n\n- `read-only` — inspection, search, web/memory lookups, and reporting only. No file writes, no command execution, no spawning further agents.\n- `read-write` — additionally file/memory writes. No command execution, no spawning.\n- `execute` — additionally command execution (bash, python). Still no spawning of further agents.\n- `all` — full profile tool set (default).\n\nRestricted modes also remove `Agent` and `SendSubagentMessage`, so a constrained subagent cannot spawn an unconstrained grandchild to bypass the filter.\n\nResume clamps the contract — it can only tighten, never widen. A resumed agent keeps the tool set it was spawned with (its profile is not rebuilt), so a wider `capability_mode` than the one it already carries is ignored, and a stored restriction is re-applied on the resumed instance. To hand a subagent more tools, spawn a fresh one.\n\n## Steering running subagents\n\nUse `SendSubagentMessage` to send a directed message to a subagent you own while it is still running: `steer` for a priority redirection, `queue` for context that applies next turn. A `steer` joins the subagent's running turn at its next step boundary; a `queue` message is delivered when the subagent starts its next turn. When the correction cannot wait for a checkpoint, use `interject`: it interrupts the tools running at that moment (side-effecting commands included, and pending approvals are withdrawn) so the message lands at the very next step. Only the owning parent may message a subagent.\n\nWhen NOT to use Agent: skip delegation for trivial one-step work (e.g. reading a known file). Almost everything else is a candidate for delegation.\n\nOnce a subagent is running, leave that scope to it: do not redo its searches or reads in parallel, and do not abandon it midway and finish the job manually. Both undo the context savings the delegation was meant to buy.\n\n## Foreground timeouts auto-background\n\nA `timeout` on a foreground `Agent` call bounds your wait, not the subagent's life. When the deadline fires while the child is still running, it is handed to the background task manager (status: backgrounded) instead of being aborted: the tool returns a `task_id`, its completion notification arrives automatically in a later turn (no polling), and you can peek with `TaskOutput(task_id=..., block=false)` or stop it with `TaskStop`. User cancellation still aborts immediately. A stopped (TaskStop) background task never suggests resume; only tasks that finish or fail on their own are recoverable via `Agent(resume=...)`.";
59583
60092
  //#endregion
59584
60093
  //#region ../../packages/agent-core/src/tools/builtin/collaboration/agent.ts
59585
60094
  /**
@@ -59624,7 +60133,7 @@ const AgentToolInputSchema = z.preprocess((input) => {
59624
60133
  "read-write",
59625
60134
  "execute",
59626
60135
  "all"
59627
- ]).optional().describe("Runtime capability contract for the subagent. read-only: may inspect but not modify the workspace. read-write: may read and edit files but not run arbitrary commands. execute: may additionally run commands. all: full tool access (default). Restricted modes strip the child tool set at runtime (including MCP and spawning tools) — the constraint is enforced, not just prompted. The parent remains the final gate.")
60136
+ ]).optional().describe("Runtime capability contract for the subagent. read-only: may inspect but not modify the workspace. read-write: may read and edit files but not run arbitrary commands. execute: may additionally run commands. all: full tool access (default). Restricted modes strip the child tool set at runtime (including MCP and spawning tools) — the constraint is enforced, not just prompted. The parent remains the final gate. On resume the contract can only tighten: a resumed agent keeps the tool set it already has, so a mode wider than the one it was spawned with is ignored — spawn a fresh subagent for a wider contract.")
59628
60137
  }));
59629
60138
  z.object({
59630
60139
  result: z.string().describe("Aggregated text output from the subagent"),
@@ -60333,23 +60842,29 @@ function formatFindingsBlock(findings) {
60333
60842
  }
60334
60843
  //#endregion
60335
60844
  //#region ../../packages/agent-core/src/tools/builtin/collaboration/send-subagent-message.md
60336
- var send_subagent_message_default = "# SendSubagentMessage\n\nSend a directed message to a subagent you own. Use this to steer or inform a\nsubagent between its turns — for example, to redirect it after new\ninformation arrives, or to hand it a correction while it is still running.\n\n- **queue**: delivered when the subagent starts its next turn, after any steer\n messages. Use for context that does not change the immediate direction.\n- **steer**: delivered into the subagent's running turn, where it joins at the\n subagent's next step boundary; if no turn is running it waits in the mailbox\n and is delivered first at the next turn start. Use for a redirection that\n should reach the subagent while it is still working.\n\n## Rules\n\n- You may only message subagents you spawned. Messaging a subagent owned by a\n different agent is refused.\n- A subagent cannot message itself.\n- No message aborts a tool call that is already in flight: a steer joins the\n running turn at its next step boundary, a queue message waits for the next\n turn start.\n- The acknowledgement says which path the message took. \"queued\" means it has\n not reached the subagent yet.\n- Keep messages short and unambiguous. The subagent sees them as a\n `[parent_messages]` block — merged into its running turn at the next step\n boundary, or at the top of its next prompt when the message waited for a turn\n start.\n- Prefer steering the *goal*, not the implementation: tell the subagent what\n changed and what to reconsider, not how to rewrite its code.\n- This tool is available only to agents that may spawn subagents. Subagents\n launched with a restricted `capability_mode` (read-only / read-write /\n execute) do not have this tool — they cannot send messages or spawn further\n agents, which keeps the capability filter from being bypassed.\n";
60845
+ var send_subagent_message_default = "# SendSubagentMessage\n\nSend a directed message to a subagent you own. Use this to steer or inform a\nsubagent between its turns — for example, to redirect it after new\ninformation arrives, or to hand it a correction while it is still running.\n\n- **queue**: delivered when the subagent starts its next turn, after any steer\n messages. Use for context that does not change the immediate direction.\n- **steer**: delivered into the subagent's running turn, where it joins at the\n subagent's next step boundary; if no turn is running it waits in the mailbox\n and is delivered first at the next turn start. Use for a redirection that\n should reach the subagent while it is still working.\n- **interject**: like steer, but it also interrupts the tool call the subagent\n currently has in flight, so the correction takes effect immediately instead\n of after a long wait. Use it when the subagent is stuck in — or about to\n outlive the value of — a running command, and you already know what it\n should do instead. This is a batch-level hammer, not a single-call cancel:\n every tool that observes the abort signal is cut short (side-effecting\n commands included), approvals still waiting for an answer are withdrawn, and\n the abort travels down the signal chain into any foreground grandchild the\n batch is waiting on. The RLM Python kernel observes the signal too: a long\n exec is interrupted through the same SIGINT route, the running statement is\n unwound and the kernel keeps its state (variables survive; a kernel that does\n not return to an idle prompt is restarted instead). An interject aimed at an\n idle or structured-output subagent degrades to the steer/mailbox path.\n\n## Rules\n\n- You may only message subagents you spawned. Messaging a subagent owned by a\n different agent is refused.\n- A subagent cannot message itself.\n- `queue` and `steer` never abort a tool call that is already in flight: a\n steer joins the running turn at its next step boundary, a queue message waits\n for the next turn start. Only `interject` interrupts the in-flight batch —\n the interrupted tool reports the interruption, so the subagent knows the\n result is missing and why.\n- The acknowledgement says which path the message took. \"queued\" means it has\n not reached the subagent yet; \"interjected\" means the in-flight batch was cut\n short.\n- Keep messages short and unambiguous. The subagent sees them as a\n `[parent_messages]` block — merged into its running turn at the next step\n boundary, or at the top of its next prompt when the message waited for a turn\n start.\n- Prefer steering the *goal*, not the implementation: tell the subagent what\n changed and what to reconsider, not how to rewrite its code.\n- Do not use this tool to stop a subagent: a message is never a cancellation.\n To end its work use `TaskStop` (background tasks) or let the turn finish.\n- Do not use this tool to collect results: a subagent's completion arrives on\n its own as a notification (or as the `Agent` tool's return value in the\n foreground). Wait for that instead of polling.\n- This tool is available only to agents that may spawn subagents. Subagents\n launched with a restricted `capability_mode` (read-only / read-write /\n execute) do not have this tool — they cannot send messages or spawn further\n agents, which keeps the capability filter from being bypassed.\n";
60337
60846
  //#endregion
60338
60847
  //#region ../../packages/agent-core/src/tools/builtin/collaboration/send-subagent-message.ts
60339
60848
  /**
60340
60849
  * SendSubagentMessageTool — parent→child directed-message tool.
60341
60850
  *
60342
- * The parent agent uses this to steer or queue a message to one of its own
60343
- * subagents. A steer aimed at a child whose turn is running is injected into
60344
- * that turn and joins at the child's next step boundary; every other message
60345
- * waits in the session-level SubagentMessageBus (FIFO within an operation
60346
- * class; steer precedes queue) and is injected into the child's prompt at its
60347
- * next turn start. A child can never send to itself, and a message addressed to
60348
- * a child owned by a different parent is refused as not_owned.
60851
+ * The parent agent uses this to steer, interject into, or queue a message to
60852
+ * one of its own subagents. A steer aimed at a child whose turn is running is
60853
+ * injected into that turn and joins at the child's next step boundary; an
60854
+ * interject does the same but also interrupts the tool batch in flight; every
60855
+ * other message waits in the session-level SubagentMessageBus (FIFO within an
60856
+ * operation class; steer precedes queue) and is injected into the child's
60857
+ * prompt at its next turn start. A child can never send to itself, and a
60858
+ * message addressed to a child owned by a different parent is refused as
60859
+ * not_owned.
60349
60860
  */
60350
60861
  const SendSubagentMessageInputSchema = z.object({
60351
60862
  agent_id: z.string().min(1).describe("Agent id of the target subagent, as returned by Agent."),
60352
- operation: z.enum(["queue", "steer"]).describe("queue: delivered when the subagent starts its next turn, after any steer messages. steer: delivered into the subagent's running turn at its next step boundary, or first in the mailbox if no turn is running."),
60863
+ operation: z.enum([
60864
+ "queue",
60865
+ "steer",
60866
+ "interject"
60867
+ ]).describe("queue: delivered when the subagent starts its next turn, after any steer messages. steer: delivered into the subagent's running turn at its next step boundary, or first in the mailbox if no turn is running. interject: like steer, but also interrupts the tool call the subagent has in flight right now — use it to correct a subagent stuck in a long wait; it falls back to the mailbox when no turn is running."),
60353
60868
  message: z.string().min(1).max(16384).describe("Message to deliver to the subagent.")
60354
60869
  });
60355
60870
  const STATUS_TO_TEXT = {
@@ -60361,6 +60876,7 @@ const STATUS_TO_TEXT = {
60361
60876
  };
60362
60877
  const ACCEPTED_TEXT = {
60363
60878
  "mid-run": "Message accepted and delivered into the subagent's running turn; it joins at the subagent's next step boundary.",
60879
+ interjected: "Message accepted and interjected: the subagent's in-flight tool batch was interrupted so the message takes effect immediately.",
60364
60880
  queued: "Message accepted and queued; it is delivered when the subagent starts its next turn."
60365
60881
  };
60366
60882
  var SendSubagentMessageTool = class {
@@ -61396,7 +61912,7 @@ var KnowledgeLookupTool = class {
61396
61912
  const llm = { generate: async (systemPrompt, userPrompt) => {
61397
61913
  return this.agent.generateText(systemPrompt, userPrompt);
61398
61914
  } };
61399
- const { multiSearchWithTrace } = await import("./src-CUURS-i6.mjs");
61915
+ const { multiSearchWithTrace } = await import("./src-qit_jRUM.mjs");
61400
61916
  const { results, trace } = await multiSearchWithTrace(store, llm, query, {
61401
61917
  topK,
61402
61918
  skipLlm: args.deep !== true
@@ -61494,7 +62010,7 @@ var ApiError = class extends Error {
61494
62010
  this.name = "ApiError";
61495
62011
  }
61496
62012
  };
61497
- function isPlaceholder(value) {
62013
+ function isPlaceholder$1(value) {
61498
62014
  const trimmed = value.trim();
61499
62015
  return trimmed.length === 0 || trimmed.includes("replace-with") || trimmed.startsWith("<") && trimmed.endsWith(">");
61500
62016
  }
@@ -61516,11 +62032,11 @@ async function loadConfig(screamHomeDir) {
61516
62032
  try {
61517
62033
  const text = await readFile(join$1(screamHomeDir, "image-config.json"), "utf8");
61518
62034
  const parsed = JSON.parse(text);
61519
- const legacyUrl = typeof parsed.base_url === "string" && !isPlaceholder(parsed.base_url) ? `${normalizeUrl(parsed.base_url)}${GENERATIONS_SUFFIX}` : "";
61520
- const urlRaw = typeof parsed.url === "string" && !isPlaceholder(parsed.url) ? parsed.url : legacyUrl;
61521
- if (urlRaw.length === 0 || typeof parsed.api_key !== "string" || typeof parsed.model !== "string" || isPlaceholder(parsed.api_key)) return;
61522
- const legacyEdit = typeof parsed.edit_base_url === "string" && !isPlaceholder(parsed.edit_base_url) ? `${normalizeUrl(parsed.edit_base_url)}${EDITS_SUFFIX}` : "";
61523
- const editUrlRaw = typeof parsed.edit_url === "string" && !isPlaceholder(parsed.edit_url) ? parsed.edit_url : legacyEdit;
62035
+ const legacyUrl = typeof parsed.base_url === "string" && !isPlaceholder$1(parsed.base_url) ? `${normalizeUrl(parsed.base_url)}${GENERATIONS_SUFFIX}` : "";
62036
+ const urlRaw = typeof parsed.url === "string" && !isPlaceholder$1(parsed.url) ? parsed.url : legacyUrl;
62037
+ if (urlRaw.length === 0 || typeof parsed.api_key !== "string" || typeof parsed.model !== "string" || isPlaceholder$1(parsed.api_key)) return;
62038
+ const legacyEdit = typeof parsed.edit_base_url === "string" && !isPlaceholder$1(parsed.edit_base_url) ? `${normalizeUrl(parsed.edit_base_url)}${EDITS_SUFFIX}` : "";
62039
+ const editUrlRaw = typeof parsed.edit_url === "string" && !isPlaceholder$1(parsed.edit_url) ? parsed.edit_url : legacyEdit;
61524
62040
  const editModelRaw = typeof parsed.edit_model === "string" ? parsed.edit_model : "";
61525
62041
  return {
61526
62042
  provider: typeof parsed.provider === "string" ? parsed.provider : "openai-compatible",
@@ -70249,7 +70765,7 @@ var GlobTool = class {
70249
70765
  if (startsWithDoubleStarPrefix(args.pattern)) {
70250
70766
  let tree;
70251
70767
  try {
70252
- tree = await listDirectory(this.jian, this.workspace.workspaceDir);
70768
+ tree = await listDirectory(this.jian, this.workspace.workspaceDir, signal);
70253
70769
  } catch {
70254
70770
  tree = "(listing unavailable)";
70255
70771
  }
@@ -70262,7 +70778,7 @@ var GlobTool = class {
70262
70778
  const rootList = [this.workspace.workspaceDir, ...this.workspace.additionalDirs].map((d) => ` - ${d}`).join("\n");
70263
70779
  let tree;
70264
70780
  try {
70265
- tree = await listDirectory(this.jian, this.workspace.workspaceDir);
70781
+ tree = await listDirectory(this.jian, this.workspace.workspaceDir, signal);
70266
70782
  } catch {
70267
70783
  tree = "(listing unavailable)";
70268
70784
  }
@@ -70277,7 +70793,10 @@ var GlobTool = class {
70277
70793
  };
70278
70794
  const includeDirs = args.include_dirs ?? true;
70279
70795
  for (const root of searchRoots) try {
70280
- const iter = this.jian.iterdir(root);
70796
+ const iter = this.jian.iterdir(root, {
70797
+ signal,
70798
+ maxEntries: 1
70799
+ });
70281
70800
  await iter.next();
70282
70801
  if (typeof iter.return === "function") await iter.return(void 0);
70283
70802
  } catch (error) {
@@ -70304,7 +70823,8 @@ var GlobTool = class {
70304
70823
  const exclude = globExcludesForPattern(args.pattern);
70305
70824
  outer: for (const root of searchRoots) for await (const filePath of this.jian.glob(root, args.pattern, {
70306
70825
  allowedRoots: [root],
70307
- exclude
70826
+ exclude,
70827
+ signal
70308
70828
  })) {
70309
70829
  yielded++;
70310
70830
  if (signal && yielded % 128 === 0) signal.throwIfAborted();
@@ -70331,6 +70851,7 @@ var GlobTool = class {
70331
70851
  mtime
70332
70852
  });
70333
70853
  }
70854
+ signal?.throwIfAborted();
70334
70855
  entries.sort((a, b) => b.mtime - a.mtime);
70335
70856
  const paths = entries.map((e) => e.path);
70336
70857
  const pathClass = this.jian.pathClass();
@@ -74261,11 +74782,15 @@ async function runRipgrepOnce(jian, rgArgs, signal) {
74261
74782
  await proc.kill("SIGTERM");
74262
74783
  } catch {}
74263
74784
  const exited = proc.wait().then(() => true).catch(() => true);
74264
- if (!await Promise.race([exited, new Promise((resolve) => {
74265
- setTimeout(() => {
74785
+ let graceTimer;
74786
+ const raced = await Promise.race([exited, new Promise((resolve) => {
74787
+ graceTimer = setTimeout(() => {
74266
74788
  resolve(false);
74267
74789
  }, SIGTERM_GRACE_MS$1);
74268
- })]) && proc.exitCode === null) try {
74790
+ graceTimer.unref?.();
74791
+ })]);
74792
+ if (graceTimer !== void 0) clearTimeout(graceTimer);
74793
+ if (!raced && proc.exitCode === null) try {
74269
74794
  await proc.kill("SIGKILL");
74270
74795
  } catch {}
74271
74796
  };
@@ -75028,7 +75553,7 @@ function escapeGlobMetachars(value) {
75028
75553
  return value.replaceAll(/[*?[{]/g, "[$&]");
75029
75554
  }
75030
75555
  async function findUniqueSuffixMatch(rawPath, searchRoot, jian, cache) {
75031
- const normalized = rawPath.replaceAll(/\\/g, "/").replace(/^\.\//, "").replace(/\/+$/, "");
75556
+ const normalized = rawPath.replaceAll("\\", "/").replace(/^\.\//, "").replace(/\/+$/, "");
75032
75557
  if (!normalized) return null;
75033
75558
  if (cache !== void 0) {
75034
75559
  const hit = cache.get(rawPath);
@@ -75036,16 +75561,24 @@ async function findUniqueSuffixMatch(rawPath, searchRoot, jian, cache) {
75036
75561
  }
75037
75562
  const pattern = `**/${escapeGlobMetachars(normalized)}`;
75038
75563
  const matches = [];
75564
+ const controller = new AbortController();
75039
75565
  let timer;
75040
75566
  try {
75041
75567
  const globPromise = (async () => {
75042
- for await (const filePath of jian.glob(searchRoot, pattern, { allowedRoots: [searchRoot] })) {
75568
+ for await (const filePath of jian.glob(searchRoot, pattern, {
75569
+ allowedRoots: [searchRoot],
75570
+ signal: controller.signal
75571
+ })) {
75043
75572
  matches.push(filePath);
75044
75573
  if (matches.length > 1) break;
75045
75574
  }
75046
75575
  })();
75576
+ globPromise.catch(() => {});
75047
75577
  const timeoutPromise = new Promise((resolve) => {
75048
- timer = setTimeout(resolve, SUFFIX_MATCH_TIMEOUT_MS);
75578
+ timer = setTimeout(() => {
75579
+ controller.abort();
75580
+ resolve();
75581
+ }, SUFFIX_MATCH_TIMEOUT_MS);
75049
75582
  timer.unref?.();
75050
75583
  });
75051
75584
  await Promise.race([globPromise, timeoutPromise]);
@@ -75233,10 +75766,10 @@ var ReadTool = class {
75233
75766
  pathClass: this.jian.pathClass(),
75234
75767
  homeDir: this.jian.gethome()
75235
75768
  }),
75236
- execute: () => this.execution(args, path)
75769
+ execute: ({ signal }) => this.execution(args, path, signal)
75237
75770
  };
75238
75771
  }
75239
- async trySuffixMatchRead(args) {
75772
+ async trySuffixMatchRead(args, signal) {
75240
75773
  const suffixMatch = await findUniqueSuffixMatch(args.path, this.workspace.workspaceDir, this.jian);
75241
75774
  if (suffixMatch === null) return null;
75242
75775
  try {
@@ -75245,12 +75778,12 @@ var ReadTool = class {
75245
75778
  return null;
75246
75779
  }
75247
75780
  const notice = suffixResolutionNotice(args.path, suffixMatch.displayPath);
75248
- return prependNotice(await this.execution(args, suffixMatch.absolutePath, true), notice);
75781
+ return prependNotice(await this.execution(args, suffixMatch.absolutePath, signal, true), notice);
75249
75782
  }
75250
- async fileNotFoundResult(displayPath) {
75783
+ async fileNotFoundResult(displayPath, signal) {
75251
75784
  let tree;
75252
75785
  try {
75253
- tree = await listDirectory(this.jian, this.workspace.workspaceDir);
75786
+ tree = await listDirectory(this.jian, this.workspace.workspaceDir, signal);
75254
75787
  } catch {
75255
75788
  tree = "(listing unavailable)";
75256
75789
  }
@@ -75259,7 +75792,8 @@ var ReadTool = class {
75259
75792
  output: `"${displayPath}" does not exist.\n\nTop of ${this.workspace.workspaceDir}:\n${tree}`
75260
75793
  };
75261
75794
  }
75262
- async execution(args, safePath, suffixResolved = false) {
75795
+ async execution(args, safePath, signal, suffixResolved = false) {
75796
+ signal?.throwIfAborted();
75263
75797
  try {
75264
75798
  let stat;
75265
75799
  try {
@@ -75267,10 +75801,10 @@ var ReadTool = class {
75267
75801
  } catch (error) {
75268
75802
  if (isFileNotFoundError(error)) {
75269
75803
  if (!suffixResolved) {
75270
- const resolved = await this.trySuffixMatchRead(args);
75804
+ const resolved = await this.trySuffixMatchRead(args, signal);
75271
75805
  if (resolved !== null) return resolved;
75272
75806
  }
75273
- return await this.fileNotFoundResult(args.path);
75807
+ return await this.fileNotFoundResult(args.path, signal);
75274
75808
  }
75275
75809
  throw error;
75276
75810
  }
@@ -75290,9 +75824,10 @@ var ReadTool = class {
75290
75824
  const lineOffset = args.line_offset ?? 1;
75291
75825
  const requestedLines = args.n_lines ?? 1e3;
75292
75826
  const effectiveLimit = Math.min(requestedLines, MAX_LINES);
75293
- if (lineOffset < 0) return await this.readTail(safePath, args.path, lineOffset, effectiveLimit, requestedLines);
75294
- return await this.readForward(safePath, args.path, lineOffset, effectiveLimit, requestedLines);
75827
+ if (lineOffset < 0) return await this.readTail(safePath, args.path, lineOffset, effectiveLimit, requestedLines, signal);
75828
+ return await this.readForward(safePath, args.path, lineOffset, effectiveLimit, requestedLines, signal);
75295
75829
  } catch (error) {
75830
+ if (signal?.aborted) throw error;
75296
75831
  if (isTextDecodeError(error)) return {
75297
75832
  isError: true,
75298
75833
  output: notReadableFileOutput(args.path)
@@ -75303,7 +75838,7 @@ var ReadTool = class {
75303
75838
  };
75304
75839
  }
75305
75840
  }
75306
- async readForward(safePath, displayPath, lineOffset, effectiveLimit, requestedLines) {
75841
+ async readForward(safePath, displayPath, lineOffset, effectiveLimit, requestedLines, signal) {
75307
75842
  const selectedEntries = [];
75308
75843
  const flags = {
75309
75844
  hasCrLf: false,
@@ -75315,7 +75850,10 @@ var ReadTool = class {
75315
75850
  let currentLineNo = 0;
75316
75851
  let maxLinesReached = false;
75317
75852
  let collectionClosed = false;
75318
- for await (const rawLine of this.jian.readLines(safePath, { errors: "strict" })) {
75853
+ for await (const rawLine of this.jian.readLines(safePath, {
75854
+ errors: "strict",
75855
+ signal
75856
+ })) {
75319
75857
  if (containsNulByte(rawLine)) return {
75320
75858
  isError: true,
75321
75859
  output: notReadableFileOutput(displayPath)
@@ -75340,6 +75878,7 @@ var ReadTool = class {
75340
75878
  });
75341
75879
  if (selectedEntries.length >= effectiveLimit) collectionClosed = true;
75342
75880
  }
75881
+ signal?.throwIfAborted();
75343
75882
  const lineEndingStyle = lineEndingStyleFromFlags(flags);
75344
75883
  const anchor = (lineEndingStyle === "crlf" ? normalizedHash : rawHash).digest("hex").slice(0, 8);
75345
75884
  const renderedLines = [];
@@ -75373,7 +75912,7 @@ var ReadTool = class {
75373
75912
  anchor
75374
75913
  });
75375
75914
  }
75376
- async readTail(safePath, displayPath, lineOffset, effectiveLimit, requestedLines) {
75915
+ async readTail(safePath, displayPath, lineOffset, effectiveLimit, requestedLines, signal) {
75377
75916
  const tailCount = Math.abs(lineOffset);
75378
75917
  const entries = [];
75379
75918
  const flags = {
@@ -75384,7 +75923,10 @@ var ReadTool = class {
75384
75923
  const rawHash = createHash("sha256");
75385
75924
  const normalizedHash = createHash("sha256");
75386
75925
  let currentLineNo = 0;
75387
- for await (const rawLine of this.jian.readLines(safePath, { errors: "strict" })) {
75926
+ for await (const rawLine of this.jian.readLines(safePath, {
75927
+ errors: "strict",
75928
+ signal
75929
+ })) {
75388
75930
  if (containsNulByte(rawLine)) return {
75389
75931
  isError: true,
75390
75932
  output: notReadableFileOutput(displayPath)
@@ -75399,6 +75941,7 @@ var ReadTool = class {
75399
75941
  });
75400
75942
  if (entries.length > tailCount) entries.shift();
75401
75943
  }
75944
+ signal?.throwIfAborted();
75402
75945
  const lineEndingStyle = lineEndingStyleFromFlags(flags);
75403
75946
  const anchor = (lineEndingStyle === "crlf" ? normalizedHash : rawHash).digest("hex").slice(0, 8);
75404
75947
  let renderedCandidates = entries.slice(0, effectiveLimit).map((entry) => {
@@ -77268,7 +77811,51 @@ function projectInfo(info) {
77268
77811
  }
77269
77812
  //#endregion
77270
77813
  //#region ../../packages/agent-core/src/tools/builtin/python/python.ts
77271
- const PY_DONE_MARKER = "__SCREAM_PY_DONE__";
77814
+ /**
77815
+ * Channel markers of the kernel↔host protocol. Every marker carries the
77816
+ * instance nonce, so (a) two PythonTool instances can never cross-hit each
77817
+ * other's markers and (b) user code printing an old-style literal such as
77818
+ * `__SCREAM_PY_DONE__` can never finish a read early or poison the next scan —
77819
+ * only this instance's nonce-bearing marker counts.
77820
+ */
77821
+ function kernelMarkers(nonce) {
77822
+ return {
77823
+ done: `__SCREAM_PY_DONE_${nonce}__`,
77824
+ boot: `__SCREAM_BOOT_DONE_${nonce}__`,
77825
+ sync: `__SCREAM_SYNC_${nonce}__`,
77826
+ error: `__SCREAM_PY_ERROR_${nonce}__`
77827
+ };
77828
+ }
77829
+ /** Every bridge reply/state file this tool family owns shares this tmpdir prefix. */
77830
+ const REPLY_FILE_PREFIX = "scream-rlm-";
77831
+ /** A file this tool family owns is swept once it is older than this. */
77832
+ const REPLY_FILE_MAX_AGE_MS = 1440 * 6e4;
77833
+ /** True when a pid is still alive (signal-0 probe; EPERM means it exists but
77834
+ * belongs to another user). */
77835
+ function isProcessAlive(pid) {
77836
+ try {
77837
+ process.kill(pid, 0);
77838
+ return true;
77839
+ } catch (error) {
77840
+ return error.code === "EPERM";
77841
+ }
77842
+ }
77843
+ /** Who fired the abort that settled an interrupted call — mirrors BashTool:
77844
+ * a parent agent's interject must not be reported as a user stop. */
77845
+ function interruptedBy$1(signal) {
77846
+ return signal !== void 0 && isParentInterject(signal.reason) ? "the parent agent" : "user";
77847
+ }
77848
+ /**
77849
+ * True when the signal has fired (an absent signal never aborts).
77850
+ *
77851
+ * Deliberately a function, not an inline `signal?.aborted === true` check:
77852
+ * repeated inline checks on the same property trip TS2367, because the first
77853
+ * check narrows the property to `false` for the rest of the scope even though
77854
+ * an await in between can flip it.
77855
+ */
77856
+ function isAborted$1(signal) {
77857
+ return signal !== void 0 && signal.aborted;
77858
+ }
77272
77859
  /** Head/tail output cap for python tool results — mirrors the generic tool
77273
77860
  * result builder (50k chars, 20k tail) so a runaway print loop cannot balloon
77274
77861
  * the snapshot into memory/TUI exhaustion. */
@@ -77282,19 +77869,67 @@ function truncateOutput$1(output) {
77282
77869
  const DEFAULT_TIMEOUT_MS = 6e4;
77283
77870
  const MAX_TIMEOUT_MS = 5 * 6e4;
77284
77871
  const KERNEL_START_TIMEOUT_MS = 3e4;
77872
+ /** SIGINT grace when an interrupted call must return the kernel to its prompt
77873
+ * (timeout or abort) before the host decides it is hung and restarts it. */
77874
+ const INTERRUPT_GRACE_MS = 1500;
77285
77875
  const PythonInputSchema = z.object({
77286
- 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."),
77876
+ code: z.string().min(1).describe("Python code to execute in the persistent kernel. Variables and plain data persist across calls, unlike Bash; imports, functions, and live objects are NOT restored if the kernel restarts, so re-run them after a restart. State is kept for the whole session while the /rlm mode is enabled."),
77287
77877
  timeout: z.number().int().positive().max(MAX_TIMEOUT_MS / 1e3).optional().describe("Execution timeout in seconds (default 60, max 300).")
77288
77878
  });
77289
- const RLM_BOOTSTRAP_PY = `import json, os, sys, tempfile, time, itertools, pickle
77879
+ const RLM_BOOTSTRAP_PY = `import json, os, sys, tempfile, time, itertools, pickle, threading
77290
77880
  _RLM_ID = itertools.count(1)
77291
77881
  _SNAP = __SNAP_PATH__
77882
+ _SNAP_LIMIT = __SNAP_LIMIT__
77883
+ _NONCE = "__NONCE__"
77884
+ _WARNED = set()
77885
+ _PENDING_WARNINGS = []
77886
+
77887
+ def _warn(msg):
77888
+ # Kernel-scoped, deduped warnings. They are queued here and flushed by the
77889
+ # host at the start of the NEXT call, so a warning about the state left by
77890
+ # this call never contaminates this call's own output.
77891
+ if msg in _WARNED:
77892
+ return
77893
+ _WARNED.add(msg)
77894
+ _PENDING_WARNINGS.append(msg)
77895
+
77896
+ def _flush_warnings():
77897
+ if _PENDING_WARNINGS:
77898
+ msgs = list(_PENDING_WARNINGS)
77899
+ del _PENDING_WARNINGS[:]
77900
+ for m in msgs:
77901
+ try:
77902
+ print("\\u26a0 RLM: " + m)
77903
+ except Exception:
77904
+ try:
77905
+ print("RLM: " + m)
77906
+ except Exception:
77907
+ pass
77908
+
77909
+ def _scream_background_error(fallback_hook, payload):
77910
+ # Background threads / unretrieved futures raise outside the exec wrapper,
77911
+ # so their tracebacks reach stderr without the wrapper's error marker.
77912
+ # Emit the marker here too, then delegate to the default hook so the
77913
+ # traceback still lands on stderr.
77914
+ try:
77915
+ print(_ERROR_MARKER)
77916
+ except Exception:
77917
+ pass
77918
+ fallback_hook(payload)
77919
+
77920
+ _ERROR_MARKER = "__SCREAM_PY_ERROR_" + _NONCE + "__"
77921
+ _DEFAULT_THREAD_HOOK = getattr(threading, "__excepthook__", None)
77922
+ if _DEFAULT_THREAD_HOOK is not None:
77923
+ threading.excepthook = lambda args: _scream_background_error(_DEFAULT_THREAD_HOOK, args)
77924
+ _DEFAULT_UNRAISABLE_HOOK = getattr(sys, "__unraisablehook__", None)
77925
+ if _DEFAULT_UNRAISABLE_HOOK is not None:
77926
+ sys.unraisablehook = lambda unraisable: _scream_background_error(_DEFAULT_UNRAISABLE_HOOK, unraisable)
77292
77927
 
77293
77928
  def _host_request(method, payload, timeout=120):
77294
77929
  rid = next(_RLM_ID)
77295
77930
  sys.stdout.write(json.dumps({"type": "host_request", "id": rid, "method": method, "payload": payload}) + "\\n")
77296
77931
  sys.stdout.flush()
77297
- reply_file = os.path.join(tempfile.gettempdir(), "scream-rlm-" + str(os.getpid()) + "-" + str(rid) + ".json")
77932
+ reply_file = os.path.join(tempfile.gettempdir(), "scream-rlm-" + _NONCE + "-" + str(os.getpid()) + "-" + str(rid) + ".json")
77298
77933
  deadline = time.time() + timeout
77299
77934
  while time.time() < deadline:
77300
77935
  if os.path.exists(reply_file):
@@ -77330,8 +77965,17 @@ def _snapshot(path=_SNAP):
77330
77965
  except Exception:
77331
77966
  pass
77332
77967
  try:
77333
- with open(path, "wb") as f:
77334
- pickle.dump(saved, f)
77968
+ data = pickle.dumps(saved)
77969
+ except Exception:
77970
+ return 0
77971
+ if len(data) > _SNAP_LIMIT:
77972
+ _warn("state exceeds the " + str(_SNAP_LIMIT) + "-byte snapshot limit; it stays in the running kernel but will not survive a restart")
77973
+ return 0
77974
+ try:
77975
+ # 0o600 keeps the snapshot private on POSIX; Windows ignores the mode.
77976
+ fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
77977
+ with os.fdopen(fd, "wb") as f:
77978
+ f.write(data)
77335
77979
  return len(saved)
77336
77980
  except Exception:
77337
77981
  return 0
@@ -77342,7 +77986,8 @@ def _restore(path=_SNAP):
77342
77986
  try:
77343
77987
  with open(path, "rb") as f:
77344
77988
  saved = pickle.load(f)
77345
- except Exception:
77989
+ except Exception as e:
77990
+ _warn("snapshot restore failed (" + type(e).__name__ + "); state from the previous kernel was not restored")
77346
77991
  return 0
77347
77992
  for k, v in saved.items():
77348
77993
  globals()[k] = v
@@ -77351,26 +77996,38 @@ def _restore(path=_SNAP):
77351
77996
  _restore()
77352
77997
  `;
77353
77998
  /** Builds the single-line bootstrap exec for this tool instance, embedding
77354
- * the snapshot path so each PythonTool has its own persistent state file. */
77355
- function buildRlmBootstrap(snapshotPath) {
77356
- const py = RLM_BOOTSTRAP_PY.replace("__SNAP_PATH__", JSON.stringify(snapshotPath));
77357
- return `exec(__import__('base64').b64decode('${Buffer.from(py).toString("base64")}').decode())\nprint("__SCREAM_BOOT_DONE__")`;
77999
+ * the snapshot path, the snapshot byte cap, and the instance nonce that
78000
+ * namespaces reply files and channel markers. */
78001
+ function buildRlmBootstrap(snapshotPath, nonce, snapshotByteLimit) {
78002
+ const py = RLM_BOOTSTRAP_PY.replace("__SNAP_PATH__", JSON.stringify(snapshotPath)).replace("__SNAP_LIMIT__", String(snapshotByteLimit)).replace("__NONCE__", nonce);
78003
+ return `exec(__import__('base64').b64decode('${Buffer.from(py).toString("base64")}').decode())\nprint("${kernelMarkers(nonce).boot}")`;
77358
78004
  }
77359
78005
  /**
77360
78006
  * Executes Python code in a persistent interactive kernel (`python3 -u -i`
77361
78007
  * over a pipe). The kernel process is lazily started on first use and lives
77362
- * for the lifetime of this tool instance (the /rlm session), so variables,
77363
- * imports, and loaded data survive across calls — unlike the stateless Bash
77364
- * tool. Runs under the normal permission mode like any other tool; the code
77365
- * can read/write files, so it is gated exactly like a mutating tool.
78008
+ * for the lifetime of this tool instance (the /rlm session), so variables and
78009
+ * loaded data survive across calls — unlike the stateless Bash tool. Imports
78010
+ * and functions are not restored after a kernel restart (only plain data is,
78011
+ * from the snapshot). Runs under the normal permission mode like any other
78012
+ * tool; the code can read/write files, so it is gated exactly like a mutating
78013
+ * tool.
77366
78014
  */
77367
78015
  var PythonTool = class PythonTool {
77368
- cwd;
77369
78016
  options;
78017
+ /** Serialized-snapshot byte cap: state larger than this is not written to
78018
+ * disk (writing it would block the kernel and risk OOM); the next call
78019
+ * reports the skipped snapshot through a one-shot warning. */
78020
+ static SNAPSHOT_BYTE_LIMIT = 32 * 1024 * 1024;
77370
78021
  name = "python";
77371
78022
  description;
77372
78023
  parameters = toInputJsonSchema(PythonInputSchema);
78024
+ /** Working directory the kernel runs in. Readable so the tool registry can
78025
+ * reuse a live instance across rebuilds only while the cwd is unchanged. */
78026
+ cwd;
77373
78027
  kernel;
78028
+ /** In-flight startKernel() promise — concurrent callers share one spawn
78029
+ * instead of racing two kernels into existence. */
78030
+ kernelSpawn;
77374
78031
  kernelBusy = false;
77375
78032
  /** Accumulated stderr from the kernel (tracebacks land here). */
77376
78033
  kernelStderr = "";
@@ -77379,30 +78036,95 @@ var PythonTool = class PythonTool {
77379
78036
  /** Per-instance snapshot file so RLM state survives kernel restarts. */
77380
78037
  snapshotPath;
77381
78038
  hostHandlers;
78039
+ snapshotByteLimit;
78040
+ /** Instance nonce: namespaces reply files and every channel marker. */
78041
+ nonce;
78042
+ doneMarker;
78043
+ bootDoneMarker;
78044
+ syncMarker;
78045
+ errorMarker;
77382
78046
  constructor(cwd, options = {}) {
77383
- this.cwd = cwd;
77384
78047
  this.options = options;
78048
+ this.cwd = cwd;
77385
78049
  this.hostHandlers = options.hostHandlers;
78050
+ this.snapshotByteLimit = options.snapshotByteLimit ?? PythonTool.SNAPSHOT_BYTE_LIMIT;
78051
+ this.nonce = randomBytes(4).toString("hex");
78052
+ const markers = kernelMarkers(this.nonce);
78053
+ this.doneMarker = markers.done;
78054
+ this.bootDoneMarker = markers.boot;
78055
+ this.syncMarker = markers.sync;
78056
+ this.errorMarker = markers.error;
77386
78057
  this.snapshotPath = options.snapshotPath ?? join(tmpdir(), `scream-rlm-state-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}.pkl`);
77387
- 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. This tool is available only when RLM mode is enabled (/rlm); when you can see it, prefer it over repeated Bash python3 invocations for any workflow that keeps state across steps (load → transform → analyze → export). Run shell commands with the Bash tool instead. 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 — use them to parallelize independent data sub-tasks inside the kernel. 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.";
78058
+ this.description = "Execute Python code in a persistent kernel. Variables and plain data persist across calls (unlike Bash) and are restored from a snapshot if the kernel restarts. Imports, functions, and live objects are NOT restored after a restart — re-run your imports if the kernel restarts. Ideal for data analysis and multi-step processing. This tool is available only when RLM mode is enabled (/rlm); when you can see it, prefer it over repeated Bash python3 invocations for any workflow that keeps state across steps (load → transform → analyze → export). Run shell commands with the Bash tool instead. 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 — use them to parallelize independent data sub-tasks inside the kernel. Each rlm() call starts a real subagent on a real task; nesting is unlimited by default, so set /rlm-max-depth before long workflows that spawn subagents from subagents. 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.";
77388
78059
  }
77389
78060
  dispose() {
77390
78061
  if (this.hostHandlers !== void 0) this.hostHandlers["__dispose__"]?.({}).catch(() => {});
78062
+ const pendingSpawn = this.kernelSpawn;
78063
+ if (pendingSpawn !== void 0) pendingSpawn.then((proc) => {
78064
+ proc.kill("SIGKILL");
78065
+ }).catch(() => {});
77391
78066
  this.kernel?.kill("SIGKILL");
77392
78067
  this.kernel = void 0;
77393
78068
  this.kernelBusy = false;
77394
78069
  this.kernelStderr = "";
77395
78070
  this.kernelStderrOffset = 0;
77396
78071
  unlink(this.snapshotPath).catch(() => {});
78072
+ this.cleanupReplyFiles();
78073
+ }
78074
+ /** Removes every reply file this instance owns — names are prefixed with
78075
+ * this instance's nonce, so no other session's files are touched. */
78076
+ cleanupReplyFiles() {
78077
+ (async () => {
78078
+ try {
78079
+ const dir = tmpdir();
78080
+ const prefix = `${REPLY_FILE_PREFIX}${this.nonce}-`;
78081
+ const names = await readdir(dir);
78082
+ await Promise.all(names.filter((name) => name.startsWith(prefix)).map((name) => unlink(join(dir, name)).catch(() => {})));
78083
+ } catch {}
78084
+ })();
78085
+ }
78086
+ /**
78087
+ * Best-effort sweep of `scream-rlm-*` files left behind by dead sessions:
78088
+ * reply files carry the owning kernel's pid (`scream-rlm-<nonce>-<pid>-<rid>
78089
+ * .json`), and a file whose kernel is gone is dead weight regardless of age;
78090
+ * files older than 24h (including legacy layouts and stale state snapshots)
78091
+ * go too. Every failure is swallowed — this runs on the kernel-start path
78092
+ * and must never delay or break it.
78093
+ */
78094
+ static async sweepStaleKernelFiles() {
78095
+ try {
78096
+ const dir = tmpdir();
78097
+ const now = Date.now();
78098
+ const names = await readdir(dir);
78099
+ await Promise.all(names.filter((name) => name.startsWith(REPLY_FILE_PREFIX)).map(async (name) => {
78100
+ const full = join(dir, name);
78101
+ try {
78102
+ const expired = now - (await stat(full)).mtimeMs > REPLY_FILE_MAX_AGE_MS;
78103
+ const rawPid = /^scream-rlm-(?:[a-z0-9]{6,10}-)?(\d+)-\d+\.json$/.exec(name)?.[1];
78104
+ const ownerGone = rawPid !== void 0 && !isProcessAlive(Number(rawPid));
78105
+ if (expired || ownerGone) await unlink(full);
78106
+ } catch {}
78107
+ }));
78108
+ } catch {}
77397
78109
  }
77398
78110
  /** Lazily starts the persistent kernel. The banner and REPL prompts are
77399
78111
  * emitted on stderr (not stdout), so we drain stderr continuously and drop
77400
78112
  * the startup banner; stdout only carries print() output. */
77401
- async ensureKernel() {
78113
+ async ensureKernel(signal) {
77402
78114
  if (this.kernel !== void 0 && this.kernel.exitCode === null) return this.kernel;
77403
- this.kernelBusy = false;
78115
+ if (this.kernelSpawn !== void 0) return this.kernelSpawn;
78116
+ const pending = this.startKernel(signal);
78117
+ this.kernelSpawn = pending;
78118
+ try {
78119
+ return await pending;
78120
+ } finally {
78121
+ if (this.kernelSpawn === pending) this.kernelSpawn = void 0;
78122
+ }
78123
+ }
78124
+ async startKernel(signal) {
77404
78125
  this.kernelStderr = "";
77405
78126
  this.kernelStderrOffset = 0;
78127
+ PythonTool.sweepStaleKernelFiles();
77406
78128
  const proc = await this.spawnKernel();
77407
78129
  this.kernel = proc;
77408
78130
  let spawnError;
@@ -77414,10 +78136,11 @@ var PythonTool = class PythonTool {
77414
78136
  proc.stderr?.on("error", () => {});
77415
78137
  this.drainStderr(proc);
77416
78138
  if (this.hostHandlers !== void 0) {
77417
- proc.stdin.write(`${buildRlmBootstrap(this.snapshotPath)}\n`);
77418
- if (!(await this.readUntilMarker(proc.stdout, "__SCREAM_BOOT_DONE__", KERNEL_START_TIMEOUT_MS)).found) {
78139
+ proc.stdin.write(`${buildRlmBootstrap(this.snapshotPath, this.nonce, this.snapshotByteLimit)}\n`);
78140
+ if (!(await this.readUntilMarker(proc.stdout, this.bootDoneMarker, KERNEL_START_TIMEOUT_MS, { signal })).found) {
77419
78141
  proc.kill("SIGKILL");
77420
78142
  this.kernel = void 0;
78143
+ if (isAborted$1(signal)) throw new Error("Python kernel start interrupted");
77421
78144
  throw new Error(`Python kernel failed to start: ${spawnError?.message ?? "bootstrap did not complete"}`);
77422
78145
  }
77423
78146
  }
@@ -77456,7 +78179,11 @@ var PythonTool = class PythonTool {
77456
78179
  "pipe",
77457
78180
  "pipe",
77458
78181
  "pipe"
77459
- ]
78182
+ ],
78183
+ env: {
78184
+ ...process.env,
78185
+ PYTHONIOENCODING: "utf-8"
78186
+ }
77460
78187
  });
77461
78188
  proc.once("error", (error) => {
77462
78189
  reject(error);
@@ -77474,26 +78201,34 @@ var PythonTool = class PythonTool {
77474
78201
  });
77475
78202
  }
77476
78203
  /**
77477
- * Reads stdout until `marker` appears (or the deadline passes). Uses the
77478
- * 'data' event instead of an async iterator: a `for await` that returns
77479
- * early destroys the stream, which breaks the next call on the same
77480
- * persistent kernel. `'data'` keeps the stream alive across calls.
78204
+ * Reads stdout until `marker` appears (or the deadline passes, the signal
78205
+ * aborts, or the stream ends). Uses the 'data' event instead of an async
78206
+ * iterator: a `for await` that returns early destroys the stream, which
78207
+ * breaks the next call on the same persistent kernel. `'data'` keeps the
78208
+ * stream alive across calls.
78209
+ *
78210
+ * `replyPid` identifies the kernel process that owns this stream: bridge
78211
+ * replies must land in the file that kernel polls, even if `this.kernel` has
78212
+ * since been replaced.
77481
78213
  */
77482
- async readUntilMarker(stream, marker, timeoutMs) {
78214
+ async readUntilMarker(stream, marker, timeoutMs, opts = {}) {
77483
78215
  if (stream === null) return {
77484
78216
  lines: [],
77485
78217
  found: false
77486
78218
  };
78219
+ const { signal, replyPid } = opts;
77487
78220
  return new Promise((resolve) => {
77488
78221
  const lines = [];
77489
78222
  let buffer = "";
77490
78223
  const decoder = new StringDecoder("utf8");
78224
+ let timer;
77491
78225
  const cleanup = () => {
77492
- clearTimeout(timer);
78226
+ if (timer !== void 0) clearTimeout(timer);
77493
78227
  stream.off("data", onData);
77494
78228
  stream.off("end", onEnd);
77495
78229
  stream.off("close", onEnd);
77496
78230
  stream.off("error", onEnd);
78231
+ signal?.removeEventListener("abort", onAbort);
77497
78232
  };
77498
78233
  const finish = (found) => {
77499
78234
  cleanup();
@@ -77502,16 +78237,30 @@ var PythonTool = class PythonTool {
77502
78237
  found
77503
78238
  });
77504
78239
  };
77505
- const onEnd = () => finish(false);
78240
+ const onEnd = () => {
78241
+ finish(false);
78242
+ };
78243
+ const onAbort = () => {
78244
+ finish(false);
78245
+ };
77506
78246
  const onData = (chunk) => {
77507
78247
  buffer += decoder.write(chunk);
77508
78248
  const parts = buffer.split("\n");
77509
78249
  buffer = parts.pop() ?? "";
77510
78250
  for (const line of parts) {
77511
78251
  if (line.includes(marker)) return finish(true);
77512
- if (line.includes("\"host_request\"")) {
77513
- this.handleHostRequest(line, stream).catch(() => {});
77514
- continue;
78252
+ const trimmed = line.trimStart();
78253
+ if (trimmed.startsWith("{")) {
78254
+ let parsed;
78255
+ try {
78256
+ parsed = JSON.parse(trimmed);
78257
+ } catch {
78258
+ parsed = void 0;
78259
+ }
78260
+ if (parsed !== null && typeof parsed === "object" && parsed.type === "host_request") {
78261
+ this.handleHostRequest(parsed, replyPid).catch(() => {});
78262
+ continue;
78263
+ }
77515
78264
  }
77516
78265
  lines.push(line);
77517
78266
  }
@@ -77521,11 +78270,18 @@ var PythonTool = class PythonTool {
77521
78270
  return finish(true);
77522
78271
  }
77523
78272
  };
77524
- const timer = setTimeout(() => finish(false), timeoutMs);
78273
+ timer = setTimeout(() => finish(false), timeoutMs);
77525
78274
  stream.on("data", onData);
77526
78275
  stream.on("end", onEnd);
77527
78276
  stream.on("close", onEnd);
77528
78277
  stream.on("error", onEnd);
78278
+ if (signal !== void 0) {
78279
+ if (signal.aborted) {
78280
+ finish(false);
78281
+ return;
78282
+ }
78283
+ signal.addEventListener("abort", onAbort);
78284
+ }
77529
78285
  });
77530
78286
  }
77531
78287
  /**
@@ -77533,42 +78289,42 @@ var PythonTool = class PythonTool {
77533
78289
  * writes the reply to the kernel's expected reply file (the kernel polls
77534
78290
  * that file — replies must not go over stdin, which is used for code input
77535
78291
  * and would race with the REPL). Fire-and-forget from the read loop.
78292
+ *
78293
+ * `replyPid` is the pid captured from the kernel that issued the request: if
78294
+ * the kernel is restarted (timeout kill) while a bridge handler is still
78295
+ * running, the late reply must still land in the file the originating kernel
78296
+ * is polling.
77536
78297
  */
77537
- /** Writes a bridge reply to the kernel's expected reply file. Uses the
77538
- * pid captured when the request was received: if the kernel is restarted
77539
- * (timeout kill) while a bridge handler is still running, the late reply
77540
- * must still land in the file the originating kernel is polling. */
77541
- writeHostReply(pid, id, body) {
78298
+ async handleHostRequest(parsed, replyPid) {
78299
+ const pid = replyPid ?? this.kernel?.pid;
77542
78300
  if (pid === void 0) return;
77543
- writeFile(join(tmpdir(), `scream-rlm-${pid}-${id}.json`), JSON.stringify({
77544
- type: "host_reply",
77545
- id,
77546
- ...body
77547
- }), "utf8").catch(() => {});
77548
- }
77549
- async handleHostRequest(line, stream) {
77550
- const replyPid = this.kernel?.pid;
77551
- if (replyPid === void 0) return;
77552
- let parsed;
77553
- try {
77554
- parsed = JSON.parse(line);
77555
- } catch {
77556
- return;
77557
- }
77558
78301
  const { id, method, payload } = parsed;
77559
78302
  if (typeof id !== "number" || typeof method !== "string" || payload === void 0) return;
77560
78303
  const handler = this.hostHandlers?.[method];
77561
78304
  if (handler === void 0) {
77562
- this.writeHostReply(replyPid, id, { error: `no handler for ${method}` });
78305
+ this.writeHostReply(pid, id, { error: `no handler for ${method}` });
77563
78306
  return;
77564
78307
  }
77565
78308
  try {
77566
78309
  const result = await handler(payload);
77567
- this.writeHostReply(replyPid, id, { result });
78310
+ this.writeHostReply(pid, id, { result });
77568
78311
  } catch (error) {
77569
- this.writeHostReply(replyPid, id, { error: error instanceof Error ? error.message : String(error) });
78312
+ this.writeHostReply(pid, id, { error: error instanceof Error ? error.message : String(error) });
77570
78313
  }
77571
78314
  }
78315
+ /** Writes a bridge reply to the kernel's expected reply file, namespaced by
78316
+ * this instance's nonce so sessions can never collide. */
78317
+ writeHostReply(pid, id, body) {
78318
+ writeFile(join(tmpdir(), `${REPLY_FILE_PREFIX}${this.nonce}-${pid}-${id}.json`), JSON.stringify({
78319
+ type: "host_reply",
78320
+ id,
78321
+ ...body
78322
+ }), {
78323
+ encoding: "utf8",
78324
+ mode: 384,
78325
+ flag: "wx"
78326
+ }).catch(() => {});
78327
+ }
77572
78328
  resolveExecution(args) {
77573
78329
  return {
77574
78330
  description: `Python: ${args.code.length > 50 ? `${args.code.slice(0, 50)}…` : args.code}`,
@@ -77581,11 +78337,27 @@ var PythonTool = class PythonTool {
77581
78337
  execute: (ctx) => this.execution(args, ctx)
77582
78338
  };
77583
78339
  }
77584
- async execution(args, _ctx) {
77585
- if (this.kernelBusy) return { output: "The Python kernel is busy executing a previous call. Wait for it to finish, or stop it with TaskStop." };
78340
+ async execution(args, ctx) {
78341
+ const signal = ctx.signal;
78342
+ const interruptedResult = () => ({
78343
+ isError: true,
78344
+ output: `Interrupted by ${interruptedBy$1(signal)}; kernel state preserved.`
78345
+ });
78346
+ if (isAborted$1(signal)) return interruptedResult();
78347
+ if (this.kernelBusy) return {
78348
+ isError: true,
78349
+ output: "The Python kernel is busy executing a previous call. Wait for it to finish, or interrupt the running call."
78350
+ };
77586
78351
  this.kernelBusy = true;
77587
78352
  try {
77588
- const proc = await this.ensureKernel();
78353
+ let proc;
78354
+ try {
78355
+ proc = await this.ensureKernel(signal);
78356
+ } catch (error) {
78357
+ if (isAborted$1(signal)) return interruptedResult();
78358
+ throw error;
78359
+ }
78360
+ if (isAborted$1(signal)) return interruptedResult();
77589
78361
  const timeoutMs = (args.timeout ?? DEFAULT_TIMEOUT_MS / 1e3) * 1e3;
77590
78362
  const wrapperPy = `import ast as _ast, base64 as _b
77591
78363
  __c = _b.b64decode('${Buffer.from(args.code, "utf8").toString("base64")}').decode()\ntry:
@@ -77611,16 +78383,15 @@ __c = _b.b64decode('${Buffer.from(args.code, "utf8").toString("base64")}').decod
77611
78383
  except BaseException as __e:
77612
78384
  # Show the user’s own source lines (the raw REPL traceback
77613
78385
  # only exposes the base64 wrapper line, which is useless for
77614
- # debugging), then re-raise so the kernel prints the real stack
77615
- # and the tool flags the call as an error.
77616
- print("Traceback (most recent call last):")
78386
+ # debugging), then re-raise so the kernel prints the real stack.
78387
+ print('${this.errorMarker}')\n print("Traceback (most recent call last):")
77617
78388
  for __i, __ln in enumerate(__c.split("\\n"), 1):
77618
78389
  print(f" File \\"<user_code>\\", line {__i}")
77619
78390
  print(f" {__ln}")
77620
78391
  print(f"{type(__e).__name__}: {__e}")
77621
78392
  raise`;
77622
78393
  const wrapperB64 = Buffer.from(wrapperPy, "utf8").toString("base64");
77623
- 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`;
78394
+ const codeWithDone = this.hostHandlers !== void 0 ? `_flush_warnings()\nexec(__import__('base64').b64decode('${wrapperB64}').decode(), globals())\nprint('${this.doneMarker}')\n_snapshot()\n` : `exec(__import__('base64').b64decode('${wrapperB64}').decode(), globals())\nprint('${this.doneMarker}')\n`;
77624
78395
  if (!proc.stdin.write(codeWithDone)) await new Promise((resolve) => {
77625
78396
  const onError = () => resolve();
77626
78397
  proc.stdin.once("close", onError);
@@ -77631,43 +78402,50 @@ except BaseException as __e:
77631
78402
  resolve();
77632
78403
  });
77633
78404
  });
77634
- const { lines, found } = await this.readUntilMarker(proc.stdout, PY_DONE_MARKER, timeoutMs);
77635
- const output = lines.filter((line) => !line.includes(PY_DONE_MARKER) && !line.trimStart().startsWith(">>> ") && !line.trimStart().startsWith("... ")).join("\n").trim();
78405
+ const { lines, found } = await this.readUntilMarker(proc.stdout, this.doneMarker, timeoutMs, {
78406
+ signal,
78407
+ replyPid: proc.pid
78408
+ });
78409
+ const hadErrorMarker = lines.some((line) => line.includes(this.errorMarker));
78410
+ const output = lines.filter((line) => !line.includes(this.doneMarker) && !line.includes(this.errorMarker) && !line.trimStart().startsWith(">>> ") && !line.trimStart().startsWith("... ")).join("\n").trim();
77636
78411
  await this.drainStderrQuiet(proc);
77637
78412
  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();
77638
78413
  this.kernelStderrOffset = this.kernelStderr.length;
77639
78414
  const merged = [output, stderr].filter((part) => part.length > 0).join("\n");
77640
78415
  if (!found) {
78416
+ const aborted = isAborted$1(signal);
77641
78417
  const preInterruptOffset = this.kernelStderrOffset;
77642
- if (process.platform === "win32" ? true : await this.interruptKernel(proc, 1500)) {
78418
+ const exited = process.platform === "win32" ? true : await this.interruptKernel(proc, INTERRUPT_GRACE_MS);
78419
+ const restarted = () => ({
78420
+ isError: true,
78421
+ output: aborted ? `Interrupted by ${interruptedBy$1(signal)}; kernel restarted.${merged.length > 0 ? `\n${truncateOutput$1(merged)}` : ""}` : `Python execution timed out after ${Math.round(timeoutMs / 1e3)}s (kernel restarted).\n${truncateOutput$1(merged)}`
78422
+ });
78423
+ if (exited) {
77643
78424
  proc.kill("SIGKILL");
77644
78425
  this.kernel = void 0;
77645
- return {
77646
- isError: true,
77647
- output: `Python execution timed out after ${Math.round(timeoutMs / 1e3)}s (kernel restarted).\n${truncateOutput$1(merged)}`
77648
- };
78426
+ return restarted();
77649
78427
  }
77650
78428
  if (!await this.syncKernel(proc)) {
77651
78429
  proc.kill("SIGKILL");
77652
78430
  this.kernel = void 0;
77653
- return {
77654
- isError: true,
77655
- output: `Python execution timed out after ${Math.round(timeoutMs / 1e3)}s (kernel restarted).\n${truncateOutput$1(merged)}`
77656
- };
78431
+ return restarted();
77657
78432
  }
77658
78433
  await this.drainStderrQuiet(proc);
77659
78434
  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();
77660
78435
  this.kernelStderrOffset = this.kernelStderr.length;
77661
- const timedOutMerged = [merged, interruptStderr].filter((part) => part.length > 0).join("\n");
78436
+ const tailMerged = [merged, interruptStderr].filter((part) => part.length > 0).join("\n");
78437
+ if (aborted) return {
78438
+ isError: true,
78439
+ output: `Interrupted by ${interruptedBy$1(signal)}; kernel state preserved.${tailMerged.length > 0 ? `\n${truncateOutput$1(tailMerged)}` : ""}`
78440
+ };
77662
78441
  return {
77663
78442
  isError: true,
77664
- output: `Python execution timed out after ${Math.round(timeoutMs / 1e3)}s (kernel interrupted; state preserved).\n${truncateOutput$1(timedOutMerged)}`
78443
+ output: `Python execution timed out after ${Math.round(timeoutMs / 1e3)}s (kernel interrupted; state preserved).\n${truncateOutput$1(tailMerged)}`
77665
78444
  };
77666
78445
  }
77667
- const isError = merged.includes("Traceback") || merged.toLowerCase().includes("error:");
77668
78446
  const finalOutput = truncateOutput$1(merged);
77669
78447
  return {
77670
- isError,
78448
+ isError: hadErrorMarker,
77671
78449
  output: finalOutput.length > 0 ? finalOutput : "(no output)"
77672
78450
  };
77673
78451
  } finally {
@@ -77703,15 +78481,14 @@ except BaseException as __e:
77703
78481
  }
77704
78482
  /**
77705
78483
  * Round-trips a sync marker through the (interrupted) kernel: writes
77706
- * `print('__SCREAM_SYNC__')` and waits for it to appear on stdout. Any
78484
+ * `print('<sync marker>')` and waits for it to appear on stdout. Any
77707
78485
  * residual queued output (a DONE marker written before the timeout, a
77708
78486
  * partially-flushed traceback) is consumed by the same read loop, so the
77709
78487
  * next execution starts from a clean marker state. Returns true when the
77710
78488
  * marker came back — the REPL is idle and reusable.
77711
78489
  */
77712
78490
  async syncKernel(proc, timeoutMs = 3e3) {
77713
- const SYNC_MARKER = "__SCREAM_SYNC__";
77714
- if (!proc.stdin.write(`print('${SYNC_MARKER}')\n`)) await new Promise((resolve) => {
78491
+ if (!proc.stdin.write(`print('${this.syncMarker}')\n`)) await new Promise((resolve) => {
77715
78492
  const onError = () => resolve();
77716
78493
  proc.stdin.once("close", onError);
77717
78494
  proc.stdin.once("error", onError);
@@ -77721,7 +78498,7 @@ except BaseException as __e:
77721
78498
  resolve();
77722
78499
  });
77723
78500
  });
77724
- const { found } = await this.readUntilMarker(proc.stdout, SYNC_MARKER, timeoutMs);
78501
+ const { found } = await this.readUntilMarker(proc.stdout, this.syncMarker, timeoutMs);
77725
78502
  return found;
77726
78503
  }
77727
78504
  /** Waits (bounded) for stderr to go quiet so tracebacks settle before the
@@ -77851,7 +78628,7 @@ var ScriptTool = class {
77851
78628
  signal: ctx.signal,
77852
78629
  store: this.store.get("scriptStore") ?? {}
77853
78630
  });
77854
- if (!result.ok) return await this.failedResult(result.error, result.output, state);
78631
+ if (!result.ok) return await this.failedResult(result.error, result.output, state, parsed.options);
77855
78632
  this.commitStoreWrites(result.storeWrites);
77856
78633
  return await this.successResult(result.output, result.value, state, parsed.options);
77857
78634
  } finally {
@@ -77914,7 +78691,7 @@ var ScriptTool = class {
77914
78691
  async successResult(output, value, state, options) {
77915
78692
  const textParts = output.filter((item) => item.type === "text").map((item) => item.text);
77916
78693
  if (value !== void 0) textParts.push(valueText(value));
77917
- const { shown } = await truncateOutput(textParts.join("\n"), (options.maxOutputTokens ?? DEFAULT_MAX_OUTPUT_TOKENS) * CHARS_PER_TOKEN);
78694
+ const { shown } = await truncateOutput(textParts.join("\n"), outputBudgetChars(options));
77918
78695
  const parts = [];
77919
78696
  if (shown.length > 0) parts.push({
77920
78697
  type: "text",
@@ -77933,11 +78710,11 @@ var ScriptTool = class {
77933
78710
  ...state.records.length > 0 ? { nestedCalls: state.records } : {}
77934
78711
  };
77935
78712
  }
77936
- async failedResult(error, output, state) {
78713
+ async failedResult(error, output, state, options) {
77937
78714
  const lines = [`Script failed (${error.kind}): ${error.message}`];
77938
78715
  const partial = output.filter((item) => item.type === "text").map((item) => item.text).join("\n");
77939
78716
  if (partial.length > 0) {
77940
- const { shown } = await truncateOutput(partial, DEFAULT_MAX_OUTPUT_TOKENS * CHARS_PER_TOKEN);
78717
+ const { shown } = await truncateOutput(partial, outputBudgetChars(options));
77941
78718
  lines.push("", "Partial output before the failure:", shown);
77942
78719
  }
77943
78720
  if (state.records.length > 0) lines.push(`(${state.records.length} nested call${state.records.length === 1 ? "" : "s"} ran before the failure)`);
@@ -77969,6 +78746,15 @@ function valueText(value) {
77969
78746
  }
77970
78747
  }
77971
78748
  /**
78749
+ * The script's effective output budget in characters: the `@options.max_output_tokens`
78750
+ * annotation when the script carries one, the tool default otherwise. The success path
78751
+ * and the failure path both read this single value, so the annotation cannot apply to
78752
+ * only one of them.
78753
+ */
78754
+ function outputBudgetChars(options) {
78755
+ return (options.maxOutputTokens ?? DEFAULT_MAX_OUTPUT_TOKENS) * CHARS_PER_TOKEN;
78756
+ }
78757
+ /**
77972
78758
  * Truncates to the character budget, keeping half the budget on each side
77973
78759
  * (matching the reference implementation). The full text is spilled to a
77974
78760
  * temp file and its path reported in the shown output.
@@ -78141,16 +78927,23 @@ var bash_default = "Execute a `{{ SHELL_NAME }}` command. Use this for shell sem
78141
78927
  */
78142
78928
  const pendingTasks = /* @__PURE__ */ new Map();
78143
78929
  const MAX_PENDING = 10;
78144
- function createBackgroundTask(command, completion) {
78930
+ function createBackgroundTask(command, completion, handles) {
78145
78931
  const id = randomUUID().slice(0, 8);
78146
78932
  const task = {
78147
78933
  command,
78148
78934
  startedAt: Date.now(),
78149
- completion
78935
+ completion,
78936
+ kill: handles.kill,
78937
+ pid: handles.pid,
78938
+ ownerId: handles.ownerId
78150
78939
  };
78151
78940
  if (pendingTasks.size >= MAX_PENDING) {
78152
78941
  const oldest = pendingTasks.keys().next().value;
78153
- if (oldest !== void 0) pendingTasks.delete(oldest);
78942
+ if (oldest !== void 0) {
78943
+ const evicted = pendingTasks.get(oldest);
78944
+ pendingTasks.delete(oldest);
78945
+ if (evicted !== void 0) evicted.kill();
78946
+ }
78154
78947
  }
78155
78948
  pendingTasks.set(id, task);
78156
78949
  task.completion.then((result) => {
@@ -78181,6 +78974,26 @@ function drainCompletedBackgroundTasks() {
78181
78974
  }
78182
78975
  return completed;
78183
78976
  }
78977
+ /**
78978
+ * Kill the parked commands one owner is responsible for and clear them.
78979
+ *
78980
+ * Strictly owner-matched, `undefined` included: the registry is module-level
78981
+ * while sessions are not, so a session teardown must reach exactly the tasks
78982
+ * its own agents parked (every agent's Bash tool stamps its session id — see
78983
+ * `Session.instantiateAgent`) and never another session's. The undefined match
78984
+ * is a real case, not a wildcard: `SessionOptions.id` is optional, so a session
78985
+ * created without an id stamps no owner, and matching it against "no owner" is
78986
+ * what keeps that session's close() from executing a foreign session's command.
78987
+ * Process-wide cleanup is `killAllPendingBackgroundTasks`, which only a real
78988
+ * exit path may call.
78989
+ */
78990
+ function stopAllPendingBackgroundTasks(ownerId) {
78991
+ for (const [id, task] of pendingTasks) {
78992
+ if (task.ownerId !== ownerId) continue;
78993
+ task.kill();
78994
+ pendingTasks.delete(id);
78995
+ }
78996
+ }
78184
78997
  //#endregion
78185
78998
  //#region ../../packages/agent-core/src/tools/builtin/shell/bash.ts
78186
78999
  /**
@@ -78231,6 +79044,20 @@ const SIGTERM_GRACE_MS = 5e3;
78231
79044
  * declaring completion (grandchildren may hold the pipe open). */
78232
79045
  const STREAM_GRACE_MS = 500;
78233
79046
  /**
79047
+ * Who fired the abort that settled an interrupted Bash call.
79048
+ *
79049
+ * Bash settles its own abort result (the batch loop never sees this tool throw),
79050
+ * so the signal's reason is the only evidence of attribution available here.
79051
+ * The loop builds that reason (`loop/tool-call.ts`): a parent agent's interject
79052
+ * arrives as a `ParentInterjectError`, anything else — a user stop, a turn
79053
+ * deadline, a host cancel — keeps the historical user wording. Reporting a
79054
+ * parent's interject as "the user" would send the child looking for a stop
79055
+ * instruction that never existed.
79056
+ */
79057
+ function interruptedBy(signal) {
79058
+ return isParentInterject(signal.reason) ? "the parent agent" : "user";
79059
+ }
79060
+ /**
78234
79061
  * Which dialect `env` selected. `shellName` is the probe's answer to "which
78235
79062
  * interpreter is this", so it decides the dialect; `osKind` does not, because
78236
79063
  * Windows carries either dialect depending on whether Git Bash was found.
@@ -78455,6 +79282,13 @@ var BashTool = class {
78455
79282
  shellKind;
78456
79283
  allowBackground;
78457
79284
  availableTools;
79285
+ /**
79286
+ * Session this tool belongs to (the owning agent's session id). A command
79287
+ * that times out is parked in the module-level pending registry under this
79288
+ * key, so closing one session sweeps only its own parked commands instead of
79289
+ * executing every other session's running work.
79290
+ */
79291
+ ownerId;
78458
79292
  constructor(jian, cwd, backgroundManager, options) {
78459
79293
  this.jian = jian;
78460
79294
  this.cwd = cwd;
@@ -78464,6 +79298,7 @@ var BashTool = class {
78464
79298
  this.isWindowsBash = this.isWindows && this.shellKind === "posix";
78465
79299
  this.allowBackground = options?.allowBackground ?? this.backgroundManager !== void 0;
78466
79300
  this.availableTools = options?.availableTools ?? /* @__PURE__ */ new Set();
79301
+ this.ownerId = options?.ownerId;
78467
79302
  const rendered = renderBashDescription(this.jian.osEnv.shellName, this.shellKind === "powershell");
78468
79303
  this.description = this.allowBackground ? rendered : withoutBackgroundDescription(rendered);
78469
79304
  }
@@ -78509,7 +79344,7 @@ var BashTool = class {
78509
79344
  const bgPrefix = completedBg.length > 0 ? completedBg.map((t) => `[Background task ${t.id} completed] Command: ${t.command}\nExit code: ${String(t.exitCode)} (${(t.elapsedMs / 1e3).toFixed(1)}s)\nOutput:\n${t.output}\n---\n`).join("") : "";
78510
79345
  if (signal.aborted) return {
78511
79346
  isError: true,
78512
- output: bgPrefix + "Aborted before command started"
79347
+ output: bgPrefix + (isParentInterject(signal.reason) ? "Interrupted by the parent agent before the command started" : "Aborted before command started")
78513
79348
  };
78514
79349
  if (args.command.length === 0) return {
78515
79350
  isError: true,
@@ -78558,11 +79393,15 @@ var BashTool = class {
78558
79393
  await proc.kill("SIGTERM");
78559
79394
  } catch {}
78560
79395
  const exited = proc.wait().then(() => true).catch(() => true);
78561
- if (!await Promise.race([exited, new Promise((resolve) => {
78562
- setTimeout(() => {
79396
+ let graceTimer;
79397
+ const raced = await Promise.race([exited, new Promise((resolve) => {
79398
+ graceTimer = setTimeout(() => {
78563
79399
  resolve(false);
78564
79400
  }, SIGTERM_GRACE_MS);
78565
- })]) && proc.exitCode === null) try {
79401
+ graceTimer.unref?.();
79402
+ })]);
79403
+ if (graceTimer !== void 0) clearTimeout(graceTimer);
79404
+ if (!raced && proc.exitCode === null) try {
78566
79405
  await proc.kill("SIGKILL");
78567
79406
  } catch {}
78568
79407
  try {
@@ -78614,7 +79453,11 @@ var BashTool = class {
78614
79453
  const taskId = createBackgroundTask(command, completionPromise.then(({ exitCode }) => ({
78615
79454
  exitCode,
78616
79455
  output: builder.toString()
78617
- })));
79456
+ })), {
79457
+ kill: killProc,
79458
+ pid: proc.pid,
79459
+ ownerId: this.ownerId
79460
+ });
78618
79461
  if (onUpdate !== void 0) completionPromise.then(({ exitCode }) => {
78619
79462
  onUpdate({
78620
79463
  kind: "custom",
@@ -78643,7 +79486,10 @@ var BashTool = class {
78643
79486
  };
78644
79487
  }
78645
79488
  const { exitCode } = raceResult;
78646
- if (aborted) return builder.error("Interrupted by user", { brief: "Interrupted by user" });
79489
+ if (aborted) {
79490
+ const attribution = `Interrupted by ${interruptedBy(signal)}`;
79491
+ return builder.error(attribution, { brief: attribution });
79492
+ }
78647
79493
  const isError = exitCode !== 0;
78648
79494
  if (isError && builder.nChars === 0) builder.write(`Process exited with code ${String(exitCode)}`);
78649
79495
  if (!isError) return builder.ok("Command executed successfully.");
@@ -79839,6 +80685,17 @@ var BackgroundManager = class extends BackgroundProcessManager {
79839
80685
  await this.restoreBackgroundTaskNotification(info);
79840
80686
  }
79841
80687
  }
80688
+ /**
80689
+ * Steer a terminal task notification into the owning agent's turn.
80690
+ *
80691
+ * Notifications follow the owner chain: a BackgroundManager belongs to
80692
+ * exactly one agent, and the Agent tool registers a background child in the
80693
+ * manager of the agent that spawned it (AgentTool is built with that agent's
80694
+ * `background`). A grandchild spawned in background by a subagent therefore
80695
+ * notifies its direct owner — the child — which surfaces it in its own turn;
80696
+ * it never skips a level up to the root. TaskList/TaskOutput/TaskStop see the
80697
+ * same boundary, each scoped to the manager's own agent.
80698
+ */
79842
80699
  async notifyBackgroundTask(info) {
79843
80700
  const context = await this.buildBackgroundTaskNotificationContext(info);
79844
80701
  if (context === void 0) return;
@@ -82519,6 +83376,31 @@ function messageFingerprint(message) {
82519
83376
  return hashString(serializeMessage$1(message));
82520
83377
  }
82521
83378
  /**
83379
+ * Fingerprint of the tool declaration table attached to a request.
83380
+ *
83381
+ * The provider prefix is `[system][tool declarations][messages]`, so a table
83382
+ * change — an MCP server reconnecting, `/script` or the python toggle, a
83383
+ * profile switch — invalidates every cached message behind it even though the
83384
+ * message bytes are untouched. Without this hash such a change is invisible to
83385
+ * the message-level prefix comparison.
83386
+ *
83387
+ * Two stability rules:
83388
+ * - **Declaration order is preserved, not normalized.** Order is part of the
83389
+ * provider-visible bytes, so a reorder must register as a change. The
83390
+ * request layer sends `loopTools`, which is name-sorted (`tool/AGENTS.md`).
83391
+ * - **Fields inside a declaration are key-sorted** by {@link stableJson}
83392
+ * (nested parameter schemas included), so the same table assembled by a
83393
+ * different code path — or a schema object built with a different key
83394
+ * insertion order — hashes identically.
83395
+ */
83396
+ function toolDeclarationsFingerprint(tools) {
83397
+ return hashString(tools.map((tool) => stableJson({
83398
+ name: tool.name,
83399
+ description: tool.description ?? null,
83400
+ parameters: tool.parameters ?? null
83401
+ })).join(""));
83402
+ }
83403
+ /**
82522
83404
  * Longest common prefix length by provider-visible bytes. This is the number
82523
83405
  * of leading messages a provider prompt cache could reuse from the previous
82524
83406
  * call. When this is less than the previous call's message count, an early
@@ -82585,6 +83467,14 @@ var ContextMemory = class {
82585
83467
  * cache-break signal rather than a reset.
82586
83468
  */
82587
83469
  lastSentFingerprints = [];
83470
+ /**
83471
+ * Tool declaration fingerprint of the last request
83472
+ * ({@link toolDeclarationsFingerprint}), the half of the provider prefix
83473
+ * that sits *before* the messages. `undefined` until the first LLM-bound
83474
+ * build — no baseline, nothing to compare. Reset together with
83475
+ * {@link lastSentFingerprints} on {@link clear}.
83476
+ */
83477
+ lastSentToolsFingerprint = void 0;
82588
83478
  constructor(agent) {
82589
83479
  this.agent = agent;
82590
83480
  }
@@ -82680,6 +83570,7 @@ var ContextMemory = class {
82680
83570
  this.deferredMessages = [];
82681
83571
  this._messageEdits.clear();
82682
83572
  this.lastSentFingerprints = [];
83573
+ this.lastSentToolsFingerprint = void 0;
82683
83574
  this.agent.injection.onContextClear();
82684
83575
  this.agent.microCompaction.reset();
82685
83576
  this.agent.emitStatusUpdated();
@@ -82899,9 +83790,17 @@ var ContextMemory = class {
82899
83790
  * runs the same detect + compact + project pipeline, then fingerprints
82900
83791
  * the result and logs how much of the prefix survived since the last
82901
83792
  * call. A stable prefix length equal to the previous message count means
82902
- * the provider prompt cache should hit; a smaller value means an early
82903
- * message mutated (compaction summary, micro-compaction truncation, or a
82904
- * projection repair) and the cache broke from that index.
83793
+ * the provider prompt cache should hit; a smaller value means the bytes
83794
+ * ahead of that index changed — an early message mutated (compaction
83795
+ * summary, micro-compaction truncation, a projection repair), or the tool
83796
+ * declaration table that precedes every message was rebuilt.
83797
+ *
83798
+ * `tools` is the declaration table this request will advertise. A caller
83799
+ * that builds the request hands over the exact table it is about to send
83800
+ * (the turn loop does, so a mid-turn `setActiveTools` that the turn's
83801
+ * frozen filter correctly ignores does not register as a false break).
83802
+ * When omitted the live offered table is used, which is what a direct
83803
+ * caller inspects.
82905
83804
  *
82906
83805
  * Unlike the read-only `messages` getter, this path closes any trailing
82907
83806
  * in-flight tool call by synthesizing an error result (synthesizeMissing):
@@ -82909,10 +83808,10 @@ var ContextMemory = class {
82909
83808
  * tool_calls message with no matching tool result (e.g. after a network
82910
83809
  * drop mid-batch).
82911
83810
  */
82912
- messagesForLLM() {
83811
+ messagesForLLM(tools) {
82913
83812
  this.agent.microCompaction.detect();
82914
83813
  const messages = project(this.applyMessageEdits(this.agent.microCompaction.compact(this.history)), { synthesizeMissing: true });
82915
- this.observePrefixStability(messages);
83814
+ this.observePrefixStability(messages, tools);
82916
83815
  return messages;
82917
83816
  }
82918
83817
  /**
@@ -82921,10 +83820,14 @@ var ContextMemory = class {
82921
83820
  * message content is mutated, only the fingerprint baseline used by the
82922
83821
  * next call's comparison.
82923
83822
  */
82924
- observePrefixStability(messages) {
83823
+ observePrefixStability(messages, tools) {
82925
83824
  const prev = this.lastSentFingerprints;
82926
- const stable = stablePrefixLength(prev, messages);
83825
+ const prevTools = this.lastSentToolsFingerprint;
83826
+ const toolsFingerprint = toolDeclarationsFingerprint(tools ?? this.agent.tools.loopTools);
83827
+ const toolDeclarationsChanged = prevTools !== void 0 && prevTools !== toolsFingerprint;
83828
+ const stable = toolDeclarationsChanged ? 0 : stablePrefixLength(prev, messages);
82927
83829
  this.lastSentFingerprints = messages.map(messageFingerprint);
83830
+ this.lastSentToolsFingerprint = toolsFingerprint;
82928
83831
  if (prev.length === 0) return;
82929
83832
  const appended = messages.length - prev.length;
82930
83833
  if (stable >= prev.length) {
@@ -82942,7 +83845,15 @@ var ContextMemory = class {
82942
83845
  prevMessageCount: prev.length,
82943
83846
  currentMessageCount: messages.length,
82944
83847
  appendedSinceLast: appended,
82945
- breakIndex: stable
83848
+ breakIndex: stable,
83849
+ toolDeclarationsChanged
83850
+ });
83851
+ this.agent.records.logRecord({
83852
+ type: "context.prefix_break",
83853
+ breakIndex: stable,
83854
+ prevMessageCount: prev.length,
83855
+ currentMessageCount: messages.length,
83856
+ appendedSinceLast: appended
82946
83857
  });
82947
83858
  }
82948
83859
  appendLoopEvent(event) {
@@ -83937,6 +84848,13 @@ async function findExecutableOnPath(name, pathEnv, platform, isFile) {
83937
84848
  function isUtf8Continuation(byte) {
83938
84849
  return byte >= 128 && byte <= 191;
83939
84850
  }
84851
+ /**
84852
+ * Byte-level UTF-8 decode for `errors: 'ignore'`: drop every byte that is not
84853
+ * part of a valid sequence, while keeping U+FFFD characters the file already
84854
+ * contains. Shared with the streaming `readLines` path so both read a file the
84855
+ * same way, character for character.
84856
+ * @internal
84857
+ */
83940
84858
  function decodeUtf8Ignore(data) {
83941
84859
  let output = "";
83942
84860
  let i = 0;
@@ -83985,6 +84903,12 @@ function decodeUtf8Ignore(data) {
83985
84903
  }
83986
84904
  return output;
83987
84905
  }
84906
+ /**
84907
+ * Byte-level UTF-16LE decode for `errors: 'ignore'`: drop unpaired surrogates
84908
+ * (and a trailing odd byte), keeping every other code unit. Shared with the
84909
+ * streaming `readLines` path.
84910
+ * @internal
84911
+ */
83988
84912
  function decodeUtf16LeIgnore(data) {
83989
84913
  let output = "";
83990
84914
  let i = 0;
@@ -84029,20 +84953,36 @@ function decodeUtf16LeIgnore(data) {
84029
84953
  * byte-to-character mappings so `errors` has no effect.
84030
84954
  * @internal
84031
84955
  */
84032
- function decodeTextWithErrors(data, encoding, errors = "strict") {
84033
- let webLabel;
84956
+ /**
84957
+ * Map Node's BufferEncoding names to Web TextDecoder labels where the two
84958
+ * diverge; `undefined` for the lossless non-UTF encodings that only
84959
+ * `Buffer.toString` covers.
84960
+ * @internal
84961
+ */
84962
+ function utfTextDecoderLabel(encoding) {
84034
84963
  switch (encoding) {
84035
84964
  case "utf-8":
84036
- case "utf8":
84037
- webLabel = "utf-8";
84038
- break;
84965
+ case "utf8": return "utf-8";
84039
84966
  case "utf16le":
84040
84967
  case "ucs2":
84041
- case "ucs-2":
84042
- webLabel = "utf-16le";
84043
- break;
84044
- default: webLabel = void 0;
84968
+ case "ucs-2": return "utf-16le";
84969
+ default: return;
84045
84970
  }
84971
+ }
84972
+ /**
84973
+ * Decode a Buffer into a string with Python-compatible `errors` handling.
84974
+ *
84975
+ * - `'strict'` (default): throw on invalid sequences (via TextDecoder `fatal: true`)
84976
+ * - `'replace'`: substitute each invalid sequence with U+FFFD (TextDecoder default)
84977
+ * - `'ignore'`: drop invalid input sequences while preserving valid U+FFFD characters
84978
+ *
84979
+ * Falls back to `Buffer.toString(encoding)` for encodings TextDecoder does not
84980
+ * support (e.g. `hex`, `base64`, `binary`, `latin1`) — those are lossless
84981
+ * byte-to-character mappings so `errors` has no effect.
84982
+ * @internal
84983
+ */
84984
+ function decodeTextWithErrors(data, encoding, errors = "strict") {
84985
+ const webLabel = utfTextDecoderLabel(encoding);
84046
84986
  if (webLabel === void 0) return data.toString(encoding);
84047
84987
  if (errors === "strict") return new TextDecoder(webLabel, { fatal: true }).decode(data);
84048
84988
  if (errors === "ignore") return webLabel === "utf-8" ? decodeUtf8Ignore(data) : decodeUtf16LeIgnore(data);
@@ -84296,6 +85236,51 @@ function cycleKey(s) {
84296
85236
  if (s.ino === 0) return null;
84297
85237
  return `${String(s.dev)}:${String(s.ino)}`;
84298
85238
  }
85239
+ /**
85240
+ * How many times one *physical* directory (same `dev:ino` key `cycleKey`
85241
+ * produces) may be entered during a single `glob` traversal.
85242
+ *
85243
+ * 2 = the directory's own place in the tree (the canonical descent) plus one
85244
+ * legitimate symlink alias pointing at the same target — the alias-following
85245
+ * behaviour the existing symlink tests pin (T-C2 / T-C6 in
85246
+ * `test/local.test.ts`). The 3rd and every later entry is skipped.
85247
+ *
85248
+ * Why a global bound on top of the path-local cycle set: path-local
85249
+ * detection only stops a directory from appearing twice *on one descent
85250
+ * path*. A symlink farm — pnpm's `node_modules/.pnpm`, where packages alias
85251
+ * each other in a dense graph — still admits an exponential number of
85252
+ * distinct alias paths to the same physical directories, each of which is
85253
+ * re-`readdir`-ed and re-`stat`-ed. That is what turns one `**` walk into
85254
+ * hours of CPU and tens of thousands of stats per second. With this bound
85255
+ * the work is O(MAX_PHYSICAL_REVISITS × physical tree): a physical directory
85256
+ * is traversed at most this many times, however many paths spell it.
85257
+ *
85258
+ * Trees with no revisit are untouched: the first entry of every directory is
85259
+ * always allowed, so a plain walk behaves exactly as it did before.
85260
+ */
85261
+ const MAX_PHYSICAL_REVISITS = 2;
85262
+ /**
85263
+ * Whether a cooperative-cancellation checkpoint should stop the walk.
85264
+ *
85265
+ * Wrapped in a helper rather than inlined as `signal?.aborted === true` so
85266
+ * control-flow narrowing cannot pin `aborted` to `false` after the first
85267
+ * checkpoint in a function body: the flag flips asynchronously, and every
85268
+ * later checkpoint must still be able to observe `true`.
85269
+ */
85270
+ function isAborted(signal) {
85271
+ return signal?.aborted === true;
85272
+ }
85273
+ /**
85274
+ * Record one entry of the physical directory `key` for the current traversal.
85275
+ * Returns false when that directory has already been entered
85276
+ * `MAX_PHYSICAL_REVISITS` times, i.e. when this descent must be skipped.
85277
+ */
85278
+ function enterPhysical(key, entries) {
85279
+ const entered = entries.get(key) ?? 0;
85280
+ if (entered >= MAX_PHYSICAL_REVISITS) return false;
85281
+ entries.set(key, entered + 1);
85282
+ return true;
85283
+ }
84299
85284
  const DEFAULT_PATHEXT = [
84300
85285
  ".COM",
84301
85286
  ".EXE",
@@ -84606,12 +85591,27 @@ var LocalJian = class LocalJian {
84606
85591
  stCtime: isWindowsPlatform(this._platform) ? s.birthtimeMs / 1e3 : s.ctimeMs / 1e3
84607
85592
  };
84608
85593
  }
84609
- async *iterdir(path) {
85594
+ async *iterdir(path, options) {
85595
+ const signal = options?.signal;
85596
+ const maxEntries = options?.maxEntries;
85597
+ if (isAborted(signal)) return;
84610
85598
  const resolved = this._resolvePath(path);
84611
- const entries = await readdir(resolved);
84612
- for (const entry of entries) yield join$1(resolved, entry);
85599
+ const dir = await opendir(resolved);
85600
+ try {
85601
+ let yielded = 0;
85602
+ for await (const entry of dir) {
85603
+ if (isAborted(signal)) return;
85604
+ if (maxEntries !== void 0 && yielded >= maxEntries) return;
85605
+ yield join$1(resolved, entry.name);
85606
+ yielded += 1;
85607
+ }
85608
+ } finally {
85609
+ await dir.close().catch(() => {});
85610
+ }
84613
85611
  }
84614
85612
  async *glob(path, pattern, options) {
85613
+ const signal = options?.signal;
85614
+ if (isAborted(signal)) return;
84615
85615
  const resolved = this._resolvePath(path);
84616
85616
  const caseSensitive = options?.caseSensitive ?? true;
84617
85617
  const excludedEntries = options?.exclude === void 0 || options.exclude.length === 0 ? void 0 : new Set(options.exclude);
@@ -84619,19 +85619,25 @@ var LocalJian = class LocalJian {
84619
85619
  if (!await this._isWithinPhysicalRoots(resolved, physicalAllowedRoots)) return;
84620
85620
  const patternParts = pattern.split("/");
84621
85621
  const initVisited = /* @__PURE__ */ new Set();
85622
+ const physicalEntries = /* @__PURE__ */ new Map();
84622
85623
  try {
84623
85624
  const rootKey = cycleKey(await stat(resolved));
84624
- if (rootKey !== null) initVisited.add(rootKey);
85625
+ if (rootKey !== null) {
85626
+ initVisited.add(rootKey);
85627
+ physicalEntries.set(rootKey, 1);
85628
+ }
84625
85629
  } catch {}
84626
- yield* this._globWalk(resolved, patternParts, caseSensitive, initVisited, physicalAllowedRoots, excludedEntries);
85630
+ yield* this._globWalk(resolved, patternParts, caseSensitive, initVisited, physicalAllowedRoots, excludedEntries, signal, physicalEntries);
84627
85631
  }
84628
- async *_globWalk(basePath, patternParts, caseSensitive, visited, physicalAllowedRoots, exclude) {
85632
+ async *_globWalk(basePath, patternParts, caseSensitive, visited, physicalAllowedRoots, exclude, signal, physicalEntries) {
85633
+ if (isAborted(signal)) return;
84629
85634
  if (!await this._isWithinPhysicalRoots(basePath, physicalAllowedRoots)) return;
84630
85635
  if (patternParts.length === 0) return;
84631
85636
  const [currentPattern, ...remainingParts] = patternParts;
84632
85637
  if (currentPattern === "**") {
84633
- if (remainingParts.length > 0) yield* this._globWalk(basePath, remainingParts, caseSensitive, visited, physicalAllowedRoots, exclude);
85638
+ if (remainingParts.length > 0) yield* this._globWalk(basePath, remainingParts, caseSensitive, visited, physicalAllowedRoots, exclude, signal, physicalEntries);
84634
85639
  else yield basePath;
85640
+ if (isAborted(signal)) return;
84635
85641
  let entries;
84636
85642
  try {
84637
85643
  entries = await readdir(basePath);
@@ -84639,6 +85645,7 @@ var LocalJian = class LocalJian {
84639
85645
  return;
84640
85646
  }
84641
85647
  for (const entry of entries) {
85648
+ if (isAborted(signal)) return;
84642
85649
  if (exclude?.has(entry) === true) continue;
84643
85650
  const fullPath = join$1(basePath, entry);
84644
85651
  if (this._rootDir && !isWithinDirectory(fullPath, this._rootDir, this._platform)) continue;
@@ -84651,11 +85658,13 @@ var LocalJian = class LocalJian {
84651
85658
  if (entryStat.isDirectory()) {
84652
85659
  const key = cycleKey(entryStat);
84653
85660
  if (key !== null && visited.has(key)) continue;
84654
- yield* this._globWalk(fullPath, patternParts, caseSensitive, key !== null ? new Set([...visited, key]) : visited, physicalAllowedRoots, exclude);
85661
+ if (key !== null && !enterPhysical(key, physicalEntries)) continue;
85662
+ yield* this._globWalk(fullPath, patternParts, caseSensitive, key !== null ? new Set([...visited, key]) : visited, physicalAllowedRoots, exclude, signal, physicalEntries);
84655
85663
  } else if (remainingParts.length === 0 && await this._isWithinPhysicalRoots(fullPath, physicalAllowedRoots)) yield fullPath;
84656
85664
  }
84657
85665
  } else {
84658
85666
  const regex = globPatternToRegex(currentPattern ?? "", caseSensitive);
85667
+ if (isAborted(signal)) return;
84659
85668
  let entries;
84660
85669
  try {
84661
85670
  entries = await readdir(basePath);
@@ -84663,6 +85672,7 @@ var LocalJian = class LocalJian {
84663
85672
  return;
84664
85673
  }
84665
85674
  for (const entry of entries) {
85675
+ if (isAborted(signal)) return;
84666
85676
  if (exclude?.has(entry) === true) continue;
84667
85677
  if (!regex.test(entry)) continue;
84668
85678
  const fullPath = join$1(basePath, entry);
@@ -84679,7 +85689,8 @@ var LocalJian = class LocalJian {
84679
85689
  if (entryStat.isDirectory()) {
84680
85690
  const key = cycleKey(entryStat);
84681
85691
  if (key !== null && visited.has(key)) continue;
84682
- yield* this._globWalk(fullPath, remainingParts, caseSensitive, key !== null ? new Set([...visited, key]) : visited, physicalAllowedRoots, exclude);
85692
+ if (key !== null && !enterPhysical(key, physicalEntries)) continue;
85693
+ yield* this._globWalk(fullPath, remainingParts, caseSensitive, key !== null ? new Set([...visited, key]) : visited, physicalAllowedRoots, exclude, signal, physicalEntries);
84683
85694
  }
84684
85695
  }
84685
85696
  }
@@ -84713,22 +85724,133 @@ var LocalJian = class LocalJian {
84713
85724
  return decodeTextWithErrors(await readFile(resolved), encoding, errors);
84714
85725
  }
84715
85726
  async *readLines(path, options) {
84716
- const stream = createReadStream(this._resolvePath(path), {
84717
- encoding: options?.encoding ?? "utf-8",
85727
+ const resolved = this._resolvePath(path);
85728
+ const encoding = options?.encoding ?? "utf-8";
85729
+ const errors = options?.errors ?? "strict";
85730
+ const signal = options?.signal;
85731
+ const webLabel = utfTextDecoderLabel(encoding);
85732
+ if (webLabel !== void 0) {
85733
+ yield* this._readLinesWithTextDecoder(resolved, webLabel, errors, signal);
85734
+ return;
85735
+ }
85736
+ const stream = createReadStream(resolved, {
85737
+ encoding,
84718
85738
  highWaterMark: 64 * 1024
84719
85739
  });
84720
85740
  let remainder = "";
84721
85741
  try {
84722
85742
  for await (const chunk of stream) {
85743
+ if (isAborted(signal)) return;
84723
85744
  const lines = (remainder + chunk).split("\n");
84724
85745
  remainder = lines.pop() ?? "";
84725
- for (const line of lines) yield line + "\n";
85746
+ for (const line of lines) {
85747
+ if (isAborted(signal)) return;
85748
+ yield line + "\n";
85749
+ }
84726
85750
  }
84727
85751
  } finally {
84728
85752
  stream.destroy();
84729
85753
  }
84730
85754
  if (remainder !== "") yield remainder;
84731
85755
  }
85756
+ /**
85757
+ * Streaming read of a UTF-family file. `strict` / `replace` go through a
85758
+ * `TextDecoder`: its state survives chunk boundaries, so a multi-byte
85759
+ * character split at a chunk boundary decodes correctly, a leading BOM is
85760
+ * removed exactly as the whole-file `readText` removes it, and `fatal`
85761
+ * rejects invalid input exactly as decoding the whole file under
85762
+ * `errors: 'strict'` does.
85763
+ *
85764
+ * `ignore` cannot use a `TextDecoder`: non-fatal decoding *replaces*
85765
+ * invalid input with U+FFFD, while `ignore` must drop it (and keep U+FFFD
85766
+ * characters the file already contains). That mode buffers raw bytes and
85767
+ * decodes line by line with the same byte-level decoders the whole-file
85768
+ * `readText` uses, so the two reads agree character for character.
85769
+ */
85770
+ async *_readLinesWithTextDecoder(resolved, webLabel, errors, signal) {
85771
+ if (errors === "ignore") {
85772
+ yield* this._readLinesIgnoreBytes(resolved, webLabel, signal);
85773
+ return;
85774
+ }
85775
+ const decoder = new TextDecoder(webLabel, { fatal: errors === "strict" });
85776
+ const stream = createReadStream(resolved, { highWaterMark: 64 * 1024 });
85777
+ let remainder = "";
85778
+ const splitChunk = (text) => {
85779
+ const lines = (remainder + text).split("\n");
85780
+ remainder = lines.pop() ?? "";
85781
+ return lines;
85782
+ };
85783
+ try {
85784
+ for await (const chunk of stream) {
85785
+ if (isAborted(signal)) return;
85786
+ for (const line of splitChunk(decoder.decode(chunk, { stream: true }))) {
85787
+ if (isAborted(signal)) return;
85788
+ yield line + "\n";
85789
+ }
85790
+ }
85791
+ for (const line of splitChunk(decoder.decode())) {
85792
+ if (isAborted(signal)) return;
85793
+ yield line + "\n";
85794
+ }
85795
+ } finally {
85796
+ stream.destroy();
85797
+ }
85798
+ if (remainder !== "") yield remainder;
85799
+ }
85800
+ /**
85801
+ * `errors: 'ignore'` streaming read for UTF-8 / UTF-16LE.
85802
+ *
85803
+ * Raw bytes are buffered until a line terminator — `0x0a` for UTF-8, the
85804
+ * UTF-16LE code unit `U+000A` (`0a 00` at an even offset) for UTF-16LE —
85805
+ * and the completed line is decoded with `decodeUtf8Ignore` /
85806
+ * `decodeUtf16LeIgnore`. Splitting on those bytes can never cut a valid
85807
+ * sequence: neither encoding contains a line-feed byte inside a multi-byte
85808
+ * character or a surrogate pair, and decoding a line in isolation drops
85809
+ * exactly the bytes the whole-file decode drops. Memory is bounded by the
85810
+ * longest single line, like the `TextDecoder` path.
85811
+ */
85812
+ async *_readLinesIgnoreBytes(resolved, webLabel, signal) {
85813
+ const utf16Le = webLabel === "utf-16le";
85814
+ const decodeLine = utf16Le ? decodeUtf16LeIgnore : decodeUtf8Ignore;
85815
+ const stream = createReadStream(resolved, { highWaterMark: 64 * 1024 });
85816
+ /** Raw byte runs of the line currently being assembled, in order. */
85817
+ let pending = [];
85818
+ /** UTF-16LE only: the odd byte a chunk ended on, rejoined with the next
85819
+ * chunk so the line-feed scan only ever steps over whole code units. */
85820
+ let carry;
85821
+ const concatPending = (tail) => tail === void 0 || tail.length === 0 ? Buffer.concat(pending) : Buffer.concat([...pending, tail]);
85822
+ const findLineBreak = (data, from) => {
85823
+ if (!utf16Le) return data.indexOf(10, from);
85824
+ for (let i = from; i + 1 < data.length; i += 2) if (data[i] === 10 && data[i + 1] === 0) return i;
85825
+ return -1;
85826
+ };
85827
+ try {
85828
+ for await (const chunk of stream) {
85829
+ if (isAborted(signal)) return;
85830
+ let data = carry === void 0 ? chunk : Buffer.concat([carry, chunk]);
85831
+ carry = void 0;
85832
+ if (utf16Le && data.length % 2 === 1) {
85833
+ carry = data.subarray(data.length - 1);
85834
+ data = data.subarray(0, data.length - 1);
85835
+ }
85836
+ let start = 0;
85837
+ for (;;) {
85838
+ const at = findLineBreak(data, start);
85839
+ if (at === -1) break;
85840
+ if (isAborted(signal)) return;
85841
+ const end = at + (utf16Le ? 2 : 1);
85842
+ yield decodeLine(concatPending(data.subarray(start, end)));
85843
+ pending = [];
85844
+ start = end;
85845
+ }
85846
+ if (start < data.length) pending.push(data.subarray(start));
85847
+ }
85848
+ } finally {
85849
+ stream.destroy();
85850
+ }
85851
+ const tail = concatPending(carry);
85852
+ if (tail.length > 0) yield decodeLine(tail);
85853
+ }
84732
85854
  async writeBytes(path, data) {
84733
85855
  await writeFile(this._resolvePath(path), data);
84734
85856
  return data.length;
@@ -84824,6 +85946,22 @@ new AsyncLocalStorage();
84824
85946
  //#region ../../packages/agent-core/src/session/hooks/runner.ts
84825
85947
  const DEFAULT_TIMEOUT_SECONDS = 30;
84826
85948
  const KILL_GRACE_MS = 100;
85949
+ /**
85950
+ * Hook stdout/stderr come from an external process and must not accumulate
85951
+ * without bound. Head+tail retention of 32 KiB keeps a complete prefix and a
85952
+ * useful trailing slice for debugging while capping the string that reaches
85953
+ * HookResult. Outputs below the cap round-trip byte-for-byte, so structured
85954
+ * JSON parsing is unaffected.
85955
+ */
85956
+ const HOOK_STREAM_MAX_CHARS = 24 * 1024;
85957
+ const HOOK_STREAM_TAIL_CHARS = 8 * 1024;
85958
+ function createHookStreamBuffer() {
85959
+ return new ToolResultBuilder({
85960
+ maxChars: HOOK_STREAM_MAX_CHARS,
85961
+ maxTailChars: HOOK_STREAM_TAIL_CHARS,
85962
+ maxLineLength: null
85963
+ });
85964
+ }
84827
85965
  const OptionalStringSchema = z.preprocess((value) => {
84828
85966
  if (value === void 0 || value === null) return void 0;
84829
85967
  if (typeof value === "string") return value;
@@ -84851,8 +85989,8 @@ async function runHook(command, input, options) {
84851
85989
  return allowResult({ stderr: errorMessage$2(error) });
84852
85990
  }
84853
85991
  return new Promise((resolve) => {
84854
- let stdout = "";
84855
- let stderr = "";
85992
+ const stdoutBuf = createHookStreamBuffer();
85993
+ const stderrBuf = createHookStreamBuffer();
84856
85994
  let settled = false;
84857
85995
  const timeoutMs = timeoutSeconds(options.timeout) * 1e3;
84858
85996
  const cleanup = () => {
@@ -84868,16 +86006,16 @@ async function runHook(command, input, options) {
84868
86006
  const timeout = setTimeout(() => {
84869
86007
  killProcess(child);
84870
86008
  settle(allowResult({
84871
- stdout,
84872
- stderr,
86009
+ stdout: stdoutBuf.toString(),
86010
+ stderr: stderrBuf.toString(),
84873
86011
  timedOut: true
84874
86012
  }));
84875
86013
  }, timeoutMs);
84876
86014
  const onAbort = () => {
84877
86015
  killProcess(child);
84878
86016
  settle(allowResult({
84879
- stdout,
84880
- stderr
86017
+ stdout: stdoutBuf.toString(),
86018
+ stderr: stderrBuf.toString()
84881
86019
  }));
84882
86020
  };
84883
86021
  options.signal?.addEventListener("abort", onAbort, { once: true });
@@ -84888,19 +86026,20 @@ async function runHook(command, input, options) {
84888
86026
  child.stdout.setEncoding("utf8");
84889
86027
  child.stderr.setEncoding("utf8");
84890
86028
  child.stdout.on("data", (chunk) => {
84891
- stdout += chunk;
86029
+ stdoutBuf.write(chunk);
84892
86030
  });
84893
86031
  child.stderr.on("data", (chunk) => {
84894
- stderr += chunk;
86032
+ stderrBuf.write(chunk);
84895
86033
  });
84896
86034
  child.on("error", (error) => {
86035
+ stderrBuf.write(errorMessage$2(error));
84897
86036
  settle(allowResult({
84898
- stdout,
84899
- stderr: stderr + errorMessage$2(error)
86037
+ stdout: stdoutBuf.toString(),
86038
+ stderr: stderrBuf.toString()
84900
86039
  }));
84901
86040
  });
84902
86041
  child.on("close", (code) => {
84903
- settle(resultFromExitCode(code ?? 0, stdout, stderr));
86042
+ settle(resultFromExitCode(code ?? 0, stdoutBuf.toString(), stderrBuf.toString()));
84904
86043
  });
84905
86044
  child.stdin.on("error", () => {});
84906
86045
  child.stdin.end(JSON.stringify(input));
@@ -88154,16 +89293,30 @@ const MIGRATIONS = [
88154
89293
  migrateRecord(record) {
88155
89294
  return record;
88156
89295
  }
89296
+ },
89297
+ {
89298
+ sourceVersion: "1.4",
89299
+ targetVersion: "1.5",
89300
+ migrateRecord(record) {
89301
+ return record;
89302
+ }
89303
+ },
89304
+ {
89305
+ sourceVersion: "1.5",
89306
+ targetVersion: "1.6",
89307
+ migrateRecord(record) {
89308
+ return record;
89309
+ }
88157
89310
  }
88158
89311
  ];
88159
89312
  function isNewerWireVersion(readVersion) {
88160
- return compareWireVersions(readVersion, "1.4") > 0;
89313
+ return compareWireVersions(readVersion, "1.6") > 0;
88161
89314
  }
88162
89315
  function resolveWireMigrations(readVersion) {
88163
- if (compareWireVersions(readVersion, "1.4") >= 0) return [];
89316
+ if (compareWireVersions(readVersion, "1.6") >= 0) return [];
88164
89317
  const migrations = [];
88165
89318
  let version = readVersion;
88166
- while (compareWireVersions(version, "1.4") < 0) {
89319
+ while (compareWireVersions(version, "1.6") < 0) {
88167
89320
  const migration = findMigration(version);
88168
89321
  if (migration === void 0) throw new Error(`Missing wire migration for version ${version}`);
88169
89322
  migrations.push(migration);
@@ -88271,7 +89424,7 @@ function headStartsWithAnyPrefix(head, headLength, prefixes) {
88271
89424
  */
88272
89425
  function readSkipFoldedFlag(headerText) {
88273
89426
  try {
88274
- return JSON.parse(headerText).protocol_version === "1.4";
89427
+ return JSON.parse(headerText).protocol_version === "1.6";
88275
89428
  } catch {
88276
89429
  return false;
88277
89430
  }
@@ -89041,11 +90194,14 @@ function restoreAgentRecord(agent, input) {
89041
90194
  agent.wolfpackMode.exit();
89042
90195
  return;
89043
90196
  case "rlm.enter":
89044
- agent.restoreRlm(true);
90197
+ agent.restoreRlm(true, input);
89045
90198
  return;
89046
90199
  case "rlm.exit":
89047
90200
  agent.restoreRlm(false);
89048
90201
  return;
90202
+ case "rlm.settings":
90203
+ agent.restoreRlmSettings(input.maxDepth);
90204
+ return;
89049
90205
  case "goal.create":
89050
90206
  agent.goal.restoreCreate(input);
89051
90207
  return;
@@ -89080,6 +90236,7 @@ function restoreAgentRecord(agent, input) {
89080
90236
  case "context.edit_message":
89081
90237
  agent.context.applyMessageEdit(input.targetId, input.replacement);
89082
90238
  return;
90239
+ case "context.prefix_break": return;
89083
90240
  case "context.snapshot":
89084
90241
  agent.context.restoreJSONSnapshot(input.snapshot);
89085
90242
  agent.fullCompaction.restoreCompactedHistory(input.compactedHistory);
@@ -89126,7 +90283,7 @@ var AgentRecords = class {
89126
90283
  if (this.persistence !== void 0 && !this.metadataInitialized && stamped.type !== "metadata") {
89127
90284
  this.persistence.append({
89128
90285
  type: "metadata",
89129
- protocol_version: "1.4",
90286
+ protocol_version: "1.6",
89130
90287
  created_at: Date.now()
89131
90288
  });
89132
90289
  this.metadataInitialized = true;
@@ -89157,18 +90314,18 @@ var AgentRecords = class {
89157
90314
  this.metadataInitialized = true;
89158
90315
  const readVersion = record.protocol_version;
89159
90316
  if (isNewerWireVersion(readVersion)) {
89160
- warning = `Session wire protocol version ${readVersion} is newer than the current version 1.4. Records will be replayed without migration.`;
90317
+ warning = `Session wire protocol version ${readVersion} is newer than the current version 1.6. Records will be replayed without migration.`;
89161
90318
  shouldRewrite = false;
89162
90319
  } else {
89163
90320
  migrations = resolveWireMigrations(readVersion);
89164
- shouldRewrite = readVersion !== "1.4";
90321
+ shouldRewrite = readVersion !== "1.6";
89165
90322
  }
89166
90323
  buffered = shouldRewrite || warning !== void 0;
89167
90324
  }
89168
90325
  let migratedRecord = migrateWireRecord(record, migrations);
89169
90326
  if (migratedRecord.type === "metadata") migratedRecord = {
89170
90327
  ...migratedRecord,
89171
- protocol_version: "1.4"
90328
+ protocol_version: "1.6"
89172
90329
  };
89173
90330
  if (buffered) {
89174
90331
  replayedRecords.push(migratedRecord);
@@ -96918,13 +98075,16 @@ const TOOL_OUTPUT_NON_TEXT = "Tool returned non-text content.";
96918
98075
  const UNEXECUTED_TOOL_CALL_OUTPUT = "This tool call was not executed: the model response ended before tool execution could start (the provider stream was interrupted). Do not assume the tool ran - re-issue the call if it is still needed.";
96919
98076
  const validators = /* @__PURE__ */ new WeakMap();
96920
98077
  /**
96921
- * Output for an aborted tool call. When the abort carries a user-cancellation
96922
- * reason (the user pressed stop), say so explicitly so the model treats it as a
96923
- * deliberate interruption instead of a system fault to theorise about or retry.
96924
- * Any other abort keeps the neutral wording.
98078
+ * Output for an aborted tool call. The abort's reason decides the attribution:
98079
+ * a user cancellation (the user pressed stop) says so explicitly, and a parent
98080
+ * interject says it was a deliberate redirection to be picked up from the
98081
+ * `[parent_messages]` directive that follows — in both cases so the model
98082
+ * treats it as intentional instead of a system fault to theorise about or
98083
+ * retry. Any other abort keeps the neutral wording.
96925
98084
  */
96926
98085
  function abortedToolOutput(toolName, signal) {
96927
98086
  if (isUserCancellation(signal.reason)) return `The user manually interrupted "${toolName}" (and anything else running at the same time). This was a deliberate user action, not a system error, timeout, or capacity limit. Do not retry automatically or guess at the cause — wait for the user's next instruction.`;
98087
+ if (isParentInterject(signal.reason)) return `The parent agent interrupted "${toolName}" (and anything else running at the same time) with an interjection. This was a deliberate redirection, not a system error, timeout, or capacity limit. Read the [parent_messages] directive that follows and follow it — do not retry this call or wait for the user.`;
96928
98088
  return `Tool "${toolName}" was aborted`;
96929
98089
  }
96930
98090
  /**
@@ -97000,7 +98160,7 @@ async function runToolCallBatch(step, response) {
97000
98160
  signal: AbortSignal.any([step.signal, steerController.signal])
97001
98161
  };
97002
98162
  const abortOnSteer = () => {
97003
- if (step.hasPendingSteer?.() === true && steerController !== void 0 && !steerController.signal.aborted) steerController.abort(userCancellationReason());
98163
+ if (step.hasPendingSteer?.() === true && steerController !== void 0 && !steerController.signal.aborted) steerController.abort(step.pendingSteerKind?.() === "parent-interject" ? parentInterjectReason() : userCancellationReason());
97004
98164
  };
97005
98165
  abortOnSteer();
97006
98166
  const steerPoll = steerController === void 0 ? void 0 : setInterval(() => {
@@ -97639,6 +98799,7 @@ async function executeLoopStep(deps) {
97639
98799
  currentStep,
97640
98800
  stepUuid,
97641
98801
  hasPendingSteer: deps.hasPendingSteer,
98802
+ pendingSteerKind: deps.pendingSteerKind,
97642
98803
  toolCallBlockIndex: { value: 0 }
97643
98804
  };
97644
98805
  await dispatchEvent({
@@ -97987,6 +99148,7 @@ async function runTurn(input) {
97987
99148
  maxRetryAttempts,
97988
99149
  recordUsage: recordStepUsage,
97989
99150
  hasPendingSteer: input.hasPendingSteer,
99151
+ pendingSteerKind: input.pendingSteerKind,
97990
99152
  mediaProjection,
97991
99153
  onStreamingDraft: input.onStreamingDraft
97992
99154
  });
@@ -102413,12 +103575,25 @@ function createGoalGrader(agent) {
102413
103575
  };
102414
103576
  };
102415
103577
  }
103578
+ /** Retained-handle capacity of the rlm bridge. When full, settled handles are
103579
+ * evicted first (FIFO, oldest insertion first); only a pathological map where
103580
+ * every handle is still in flight falls back to dropping the oldest live
103581
+ * handle, whose result then becomes unretrievable. */
103582
+ const RLM_HANDLE_CAPACITY = 128;
103583
+ /** How long a settled handle's result stays retrievable before the next
103584
+ * handle operation lazily evicts it. */
103585
+ const RLM_HANDLE_TTL_MS = 30 * 6e4;
102416
103586
  /**
102417
103587
  * Host bridge handlers for the /rlm python kernel: `rlm.run` spawns a
102418
103588
  * subagent (reusing the subagent host) and returns its handle id;
102419
103589
  * `rlm.result` waits for that subagent's final summary. Handles are kept in
102420
103590
  * a closure map for the lifetime of the ToolManager (one per session).
102421
103591
  *
103592
+ * Handle retention: a settled handle stays readable — rlm_wait can be called
103593
+ * any number of times and always returns the same result — and an entry is
103594
+ * only removed by (1) kernel teardown (`__dispose__`), (2) capacity eviction,
103595
+ * or (3) the lazy TTL sweep. It is never removed by consumption.
103596
+ *
102422
103597
  * Recursion guard: a subagent may only spawn its own rlm() children if its
102423
103598
  * depth is below the cap. Depth is carried on each agent instance (root = 0,
102424
103599
  * every spawned subagent = parent + 1). The cap is per-agent
@@ -102427,8 +103602,27 @@ function createGoalGrader(agent) {
102427
103602
  */
102428
103603
  function createRlmHostHandlers(agent) {
102429
103604
  const handles = /* @__PURE__ */ new Map();
103605
+ /** Lazy TTL sweep, run at the top of every handle operation: settled
103606
+ * handles older than the TTL are dropped so a long session cannot retain
103607
+ * them forever. */
103608
+ const evictExpired = () => {
103609
+ const now = Date.now();
103610
+ for (const [id, entry] of handles) if (entry.settledAt !== void 0 && now - entry.settledAt > RLM_HANDLE_TTL_MS) handles.delete(id);
103611
+ };
103612
+ /** Capacity guard, run before inserting a new handle. Map iteration is
103613
+ * insertion order, so the first settled entry found is the oldest. */
103614
+ const makeRoom = () => {
103615
+ if (handles.size < RLM_HANDLE_CAPACITY) return;
103616
+ for (const [id, entry] of handles) if (entry.settledAt !== void 0) {
103617
+ handles.delete(id);
103618
+ return;
103619
+ }
103620
+ const oldest = handles.keys().next().value;
103621
+ if (oldest !== void 0) handles.delete(oldest);
103622
+ };
102430
103623
  return {
102431
103624
  "rlm.run": async (payload) => {
103625
+ evictExpired();
102432
103626
  const host = agent.subagentHost;
102433
103627
  if (host === void 0) throw new Error("subagent host unavailable");
102434
103628
  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.`);
@@ -102448,11 +103642,13 @@ function createRlmHostHandlers(agent) {
102448
103642
  name,
102449
103643
  controller
102450
103644
  };
103645
+ makeRoom();
102451
103646
  handles.set(handle.agentId, entry);
102452
- handle.completion.then((result) => {
102453
- entry.result = result;
103647
+ handle.completion.then((completion) => {
103648
+ entry.result = completion.result;
103649
+ entry.settledAt = Date.now();
102454
103650
  }, () => {
102455
- entry.result = void 0;
103651
+ entry.settledAt = Date.now();
102456
103652
  });
102457
103653
  return {
102458
103654
  id: handle.agentId,
@@ -102460,16 +103656,12 @@ function createRlmHostHandlers(agent) {
102460
103656
  };
102461
103657
  },
102462
103658
  "rlm.result": async (payload) => {
103659
+ evictExpired();
102463
103660
  const id = String(payload["id"]);
102464
103661
  const entry = handles.get(id);
102465
103662
  if (entry === void 0) throw new Error(`unknown rlm handle: ${id}`);
102466
- if (entry.result !== void 0) {
102467
- handles.delete(id);
102468
- return { result: entry.result };
102469
- }
102470
- const result = (await entry.completion).result;
102471
- handles.delete(id);
102472
- return { result };
103663
+ if (entry.result !== void 0) return { result: entry.result };
103664
+ return { result: (await entry.completion).result };
102473
103665
  },
102474
103666
  __dispose__: async () => {
102475
103667
  for (const { controller } of handles.values()) controller.abort(/* @__PURE__ */ new Error("rlm kernel disposed"));
@@ -102960,6 +104152,13 @@ var ToolManager = class {
102960
104152
  this.lspRegistry = new LspRegistry(jian, this.agent.lspSupervisor);
102961
104153
  this.lspRegistryJian = jian;
102962
104154
  }
104155
+ const previousPython = this.getBuiltinTool("python");
104156
+ let pythonTool;
104157
+ if (previousPython instanceof PythonTool && previousPython.cwd === cwd) pythonTool = previousPython;
104158
+ else {
104159
+ if (previousPython instanceof PythonTool) previousPython.dispose();
104160
+ pythonTool = new PythonTool(cwd, { hostHandlers: createRlmHostHandlers(this.agent) });
104161
+ }
102963
104162
  const allowBackground = this.enabledTools.has("TaskList") && this.enabledTools.has("TaskOutput") && this.enabledTools.has("TaskStop");
102964
104163
  const allowedSpawns = DEFAULT_AGENT_PROFILES[this.agent.config.profileName ?? "agent"]?.spawns;
102965
104164
  const canSpawn = this.agent.subagentHost && (this.agent.type !== "sub" || allowedSpawns !== void 0 && allowedSpawns.length > 0);
@@ -102973,9 +104172,10 @@ var ToolManager = class {
102973
104172
  new GlobTool(jian, workspace),
102974
104173
  new BashTool(jian, cwd, background, {
102975
104174
  allowBackground,
102976
- availableTools: this.enabledTools
104175
+ availableTools: this.enabledTools,
104176
+ ownerId: this.agent.sessionId
102977
104177
  }),
102978
- new PythonTool(cwd, { hostHandlers: createRlmHostHandlers(this.agent) }),
104178
+ pythonTool,
102979
104179
  new ScriptTool(this.toolStore),
102980
104180
  (modelCapabilities.image_in || modelCapabilities.video_in) && new ReadMediaFileTool(jian, workspace, modelCapabilities, videoUploader),
102981
104181
  new EnterPlanModeTool(this.agent),
@@ -103568,6 +104768,20 @@ const TURN_DEFAULTS = {
103568
104768
  //#region ../../packages/agent-core/src/agent/turn/index.ts
103569
104769
  /** Cap on how long the first turn waits for MCP servers to finish loading. */
103570
104770
  const MCP_WAIT_TIMEOUT_MS = 1e4;
104771
+ /**
104772
+ * Whether a buffered steer interrupts an in-flight tool batch — interactive
104773
+ * user steers do, and so does any steer carrying an explicit `interrupt: true`
104774
+ * (the parent host's interject); background/cron/hook steers (no flag) and
104775
+ * auto-drained queue messages (`interrupt: false`) only join at the next step
104776
+ * boundary.
104777
+ *
104778
+ * Single source of truth for the lane: `hasPendingSteer` (does one exist?) and
104779
+ * `pendingSteerKind` (whose is it?) must classify identically, or a new
104780
+ * interrupt lane would interrupt without being attributed.
104781
+ */
104782
+ function isInterruptingSteer(steer) {
104783
+ return steer.interrupt === true || steer.origin.kind === "user" && steer.interrupt !== false;
104784
+ }
103571
104785
  var TurnFlow = class {
103572
104786
  agent;
103573
104787
  steerBuffer = [];
@@ -103964,6 +105178,7 @@ var TurnFlow = class {
103964
105178
  let stopHookContinuationUsed = false;
103965
105179
  const deduper = new ToolCallDeduplicator();
103966
105180
  const turnToolFilter = this.agent.tools.snapshotEnabledTools();
105181
+ const stepTools = { current: void 0 };
103967
105182
  await Promise.race([this.agent.mcp?.waitForInitialLoad(signal) ?? Promise.resolve(), new Promise((resolve) => setTimeout(resolve, MCP_WAIT_TIMEOUT_MS))]);
103968
105183
  while (true) {
103969
105184
  signal.throwIfAborted();
@@ -103974,14 +105189,19 @@ var TurnFlow = class {
103974
105189
  turnId: String(turnId),
103975
105190
  signal,
103976
105191
  llm: this.agent.llm,
103977
- buildMessages: () => this.agent.context.messagesForLLM(),
105192
+ buildMessages: () => this.agent.context.messagesForLLM(stepTools.current),
103978
105193
  dispatchEvent: this.buildDispatchEvent(turnId),
103979
105194
  tools: this.agent.tools.loopTools,
103980
- buildTools: () => this.agent.tools.loopToolsFor(turnToolFilter),
105195
+ buildTools: () => stepTools.current = this.agent.tools.loopToolsFor(turnToolFilter),
103981
105196
  log: this.agent.log,
103982
105197
  maxSteps: loopControl?.maxStepsPerTurn,
103983
105198
  maxRetryAttempts: loopControl?.maxRetriesPerStep,
103984
- hasPendingSteer: () => this.steerBuffer.some((steer) => steer.origin.kind === "user" && steer.interrupt !== false),
105199
+ hasPendingSteer: () => this.steerBuffer.some(isInterruptingSteer),
105200
+ pendingSteerKind: () => {
105201
+ const interrupting = this.steerBuffer.filter(isInterruptingSteer);
105202
+ if (interrupting.some((steer) => steer.origin.kind === "user")) return "user";
105203
+ return interrupting.some((steer) => steer.origin.kind === "system_trigger" && steer.origin.name === "parent_interject") ? "parent-interject" : "user";
105204
+ },
103985
105205
  onStreamingDraft: (text, think) => {
103986
105206
  this.agent.records.logRecord({
103987
105207
  type: "context.stream_draft",
@@ -104633,8 +105853,19 @@ function totalUsage(byModel) {
104633
105853
  //#endregion
104634
105854
  //#region ../../packages/agent-core/src/agent/index.ts
104635
105855
  const SIDE_QUESTION_SYSTEM = "You are a helpful coding assistant answering a quick side question. The user is in the middle of a coding session and needs a fast, concise answer. Keep your response short and focused — this is a side question, not the main task.";
105856
+ /** Wire representation of the RLM recursion cap: `Infinity` (unlimited) is
105857
+ * serialized as null, since JSON has no Infinity. */
105858
+ function rlmMaxDepthForWire(maxDepth) {
105859
+ return Number.isFinite(maxDepth) ? maxDepth : null;
105860
+ }
104636
105861
  var Agent = class {
104637
105862
  type;
105863
+ /** Stable id of this agent inside its session ('main' for the root agent);
105864
+ * status events carry it so consumers can tell main from subagents. */
105865
+ agentId;
105866
+ /** Session that owns this agent; undefined outside a session (see
105867
+ * {@link AgentOptions.sessionId}). */
105868
+ sessionId;
104638
105869
  jian;
104639
105870
  screamConfig;
104640
105871
  homedir;
@@ -104688,6 +105919,8 @@ var Agent = class {
104688
105919
  resolveRuntimeSystemPrompt;
104689
105920
  constructor(options) {
104690
105921
  this.type = options.type ?? "main";
105922
+ this.agentId = options.agentId ?? "main";
105923
+ this.sessionId = options.sessionId;
104691
105924
  this.jian = options.jian;
104692
105925
  this.screamConfig = options.config;
104693
105926
  this.homedir = options.homedir;
@@ -104731,7 +105964,7 @@ var Agent = class {
104731
105964
  this.cron = this.type === "sub" ? null : new CronManager(this);
104732
105965
  this.goal = new GoalMode(this);
104733
105966
  const screamHomeDir = options.screamHomeDir;
104734
- this.memoStore = screamHomeDir ? new MemoryMemoStore(screamHomeDir, this.log) : void 0;
105967
+ this.memoStore = screamHomeDir ? sharedMemoStore(screamHomeDir, this.log) : void 0;
104735
105968
  this.memoStoreReady = this.initMemoStore(screamHomeDir);
104736
105969
  this.knowledgeStore = screamHomeDir !== void 0 && this.type === "main" ? sharedKnowledgeStore(screamHomeDir) : void 0;
104737
105970
  this.knowledgeStoreReady = this.knowledgeStore !== void 0 && screamHomeDir !== void 0 ? sharedKnowledgeStoreReady(screamHomeDir).catch((error) => {
@@ -104786,7 +106019,7 @@ var Agent = class {
104786
106019
  this.log.error("memory legacy migration failed", error);
104787
106020
  }
104788
106021
  try {
104789
- this.memoStore.setEmbeddingEngine(this.sharedEmbeddingEngine);
106022
+ if (!this.memoStore.getEmbeddingEngine()) this.memoStore.setEmbeddingEngine(this.sharedEmbeddingEngine);
104790
106023
  } catch (error) {
104791
106024
  this.log.warn("embedding engine init failed; falling back to keyword search", error);
104792
106025
  }
@@ -104824,11 +106057,14 @@ var Agent = class {
104824
106057
  return this.rlmMaxDepth;
104825
106058
  }
104826
106059
  setRlmMaxDepth(maxDepth) {
104827
- if (!Number.isFinite(maxDepth) || maxDepth <= 0) {
104828
- this.rlmMaxDepth = Infinity;
104829
- return;
104830
- }
104831
- this.rlmMaxDepth = Math.trunc(maxDepth);
106060
+ const normalized = normalizeRlmMaxDepth(maxDepth);
106061
+ if (normalized === this.rlmMaxDepth) return;
106062
+ this.rlmMaxDepth = normalized;
106063
+ this.records.logRecord({
106064
+ type: "rlm.settings",
106065
+ maxDepth: rlmMaxDepthForWire(this.rlmMaxDepth)
106066
+ });
106067
+ this.emitStatusUpdated();
104832
106068
  }
104833
106069
  /** Disposes the persistent python kernel (if any) and resets RLM mode.
104834
106070
  * Called on session close so no orphaned kernel process is left behind.
@@ -104860,7 +106096,10 @@ var Agent = class {
104860
106096
  if (withoutPython.length !== current.length) this.tools.setActiveTools(withoutPython);
104861
106097
  this.tools.getBuiltinTool("python")?.dispose?.();
104862
106098
  }
104863
- this.records.logRecord({ type: enabled ? "rlm.enter" : "rlm.exit" });
106099
+ this.records.logRecord(enabled ? {
106100
+ type: "rlm.enter",
106101
+ maxDepth: rlmMaxDepthForWire(this.rlmMaxDepth)
106102
+ } : { type: "rlm.exit" });
104864
106103
  this.emitStatusUpdated();
104865
106104
  }
104866
106105
  /** Returns whether RLM (persistent python mode) is currently enabled. */
@@ -104904,7 +106143,11 @@ var Agent = class {
104904
106143
  this.rlmEnabled = true;
104905
106144
  const current = this.tools.getActiveTools();
104906
106145
  if (!current.includes("python")) this.tools.setActiveTools([...current, "python"]);
104907
- this.records.logRecord({ type: "rlm.enter" });
106146
+ this.records.logRecord({
106147
+ type: "rlm.enter",
106148
+ depth: this.rlmDepth,
106149
+ maxDepth: rlmMaxDepthForWire(this.rlmMaxDepth)
106150
+ });
104908
106151
  }
104909
106152
  /** Capability mode this agent runs under; `all` for main agents and for
104910
106153
  * subagents the parent did not restrict. The RLM python bridge forwards it
@@ -104919,10 +106162,14 @@ var Agent = class {
104919
106162
  getCapabilityMode() {
104920
106163
  return this.capabilityMode;
104921
106164
  }
104922
- /** Restores RLM mode from a persisted record during replay. Does not log
104923
- * a new record and does not emit a status update (both are suppressed while
104924
- * records are restoring). */
104925
- restoreRlm(enabled) {
106165
+ /** Restores RLM mode from a persisted record during replay, including the
106166
+ * recursion bookkeeping the enter payload carries since wire v1.6 (missing
106167
+ * fields mean an older wire — the defaults stay). Does not log a new record
106168
+ * and does not emit a status update (both are suppressed while records are
106169
+ * restoring). */
106170
+ restoreRlm(enabled, payload = {}) {
106171
+ if (payload.depth !== void 0) this.setRlmDepth(payload.depth);
106172
+ if (payload.maxDepth !== void 0) this.restoreRlmSettings(payload.maxDepth);
104926
106173
  this.rlmEnabled = enabled;
104927
106174
  const current = this.tools.getActiveTools();
104928
106175
  if (enabled) {
@@ -104932,6 +106179,11 @@ var Agent = class {
104932
106179
  if (withoutPython.length !== current.length) this.tools.setActiveTools(withoutPython);
104933
106180
  }
104934
106181
  }
106182
+ /** Restores the RLM recursion cap from a persisted `rlm.settings` record
106183
+ * during replay. Silent on purpose: no new record, no status event. */
106184
+ restoreRlmSettings(maxDepth) {
106185
+ this.rlmMaxDepth = maxDepth === null ? Infinity : normalizeRlmMaxDepth(maxDepth);
106186
+ }
104935
106187
  get generate() {
104936
106188
  return async (provider, systemPrompt, tools, history, callbacks, options) => {
104937
106189
  if (options?.auth !== void 0) {
@@ -105359,6 +106611,7 @@ var Agent = class {
105359
106611
  const model = this.config.model;
105360
106612
  this.emitEvent({
105361
106613
  type: "agent.status.updated",
106614
+ agentId: this.agentId,
105362
106615
  model,
105363
106616
  thinkingLevel: this.config.thinkingLevel,
105364
106617
  contextTokens,
@@ -105368,6 +106621,7 @@ var Agent = class {
105368
106621
  planStrategy: this.planMode.isActive ? this.planMode.strategy : void 0,
105369
106622
  wolfpackMode: this.wolfpackMode.isActive,
105370
106623
  rlmEnabled: this.rlmEnabled,
106624
+ rlmMaxDepth: rlmMaxDepthForWire(this.rlmMaxDepth),
105371
106625
  permission: this.permission.mode,
105372
106626
  usage
105373
106627
  });
@@ -109677,13 +110931,12 @@ var SubagentMessageBus = class {
109677
110931
  /**
109678
110932
  * Number of messages still deliverable for `agentId`. Expired ones are not
109679
110933
  * counted: `poll` drops them, so counting them would make the host spend a
109680
- * delivery turn on a message that can never arrive.
110934
+ * delivery turn on a message that can never arrive. Counting is also an
110935
+ * ordinary bus access, so it reclaims mail that has expired by now.
109681
110936
  */
109682
110937
  activeCount(agentId) {
109683
- const queue = this.mailboxes.get(agentId)?.queue;
109684
- if (queue === void 0) return 0;
109685
- const now = Date.now();
109686
- return queue.reduce((count, m) => m.deadline > now ? count + 1 : count, 0);
110938
+ this.reclaimExpired(Date.now());
110939
+ return this.mailboxes.get(agentId)?.queue.length ?? 0;
109687
110940
  }
109688
110941
  /**
109689
110942
  * Queue a message for a subagent. Pure mailbox logic: ownership, liveness
@@ -109691,7 +110944,9 @@ var SubagentMessageBus = class {
109691
110944
  * on acceptance, the queue depth seen by the recipient at poll time.
109692
110945
  */
109693
110946
  send(msg) {
109694
- if (Date.now() > msg.deadline) return { status: "deadline_elapsed" };
110947
+ const now = Date.now();
110948
+ this.reclaimExpired(now);
110949
+ if (now > msg.deadline) return { status: "deadline_elapsed" };
109695
110950
  if (byteLength(msg.text) > msg.byteLimit) return {
109696
110951
  status: "saturated",
109697
110952
  reason: "bytes"
@@ -109717,19 +110972,52 @@ var SubagentMessageBus = class {
109717
110972
  };
109718
110973
  }
109719
110974
  /**
110975
+ * Drop every queued message in every mailbox (session teardown). Id and seq
110976
+ * counters stay monotonic so identifiers issued before the clear are never
110977
+ * reissued.
110978
+ */
110979
+ clear() {
110980
+ this.mailboxes.clear();
110981
+ }
110982
+ /** Mailbox entries currently pinned in memory (diagnostics / assertions).
110983
+ * Reading does not reclaim: an entry whose mail has all expired is freed by
110984
+ * the next send/activeCount/poll, not by this getter. */
110985
+ get mailboxCount() {
110986
+ return this.mailboxes.size;
110987
+ }
110988
+ /**
110989
+ * Drop mail that can no longer be delivered and free the mailbox slot it
110990
+ * pinned. A message is deliverable only until its deadline; past it, `poll`
110991
+ * would discard it rather than hand it to the child, so keeping it would pin
110992
+ * memory for a run that has already ended. Called on every ordinary bus
110993
+ * access — send, activeCount, poll — which is what keeps the mailbox map
110994
+ * bounded now that a finished child's terminal no longer purges its mailbox.
110995
+ */
110996
+ reclaimExpired(now) {
110997
+ for (const [agentId, mailbox] of this.mailboxes) {
110998
+ const live = mailbox.queue.filter((m) => m.deadline > now);
110999
+ if (live.length === 0) this.mailboxes.delete(agentId);
111000
+ else if (live.length !== mailbox.queue.length) mailbox.queue = live;
111001
+ }
111002
+ }
111003
+ /**
109720
111004
  * Deliver all pending, unexpired messages for `agentId`. Steer messages are
109721
111005
  * always dequeued before queue messages; within an operation class, arrival
109722
111006
  * order is preserved via the monotonically increasing `seq` (this is a stable
109723
- * two-pass collection, not a sort). Messages past their deadline are dropped.
111007
+ * two-pass collection, not a sort). Messages past their deadline are
111008
+ * reclaimed, never delivered.
111009
+ *
111010
+ * The mailbox entry is removed once its queue is emptied: a polled-out (or
111011
+ * fully expired) mailbox must not pin a Map slot.
109724
111012
  */
109725
111013
  poll(agentId) {
109726
- const mailbox = this.mailboxes.get(agentId);
109727
- if (mailbox === void 0 || mailbox.queue.length === 0) return [];
109728
111014
  const now = Date.now();
109729
- const live = mailbox.queue.filter((m) => m.deadline > now);
109730
- mailbox.queue = [];
109731
- const steers = live.filter((m) => m.operation === "steer");
109732
- const queues = live.filter((m) => m.operation === "queue");
111015
+ this.reclaimExpired(now);
111016
+ const mailbox = this.mailboxes.get(agentId);
111017
+ if (mailbox === void 0) return [];
111018
+ this.mailboxes.delete(agentId);
111019
+ const steers = mailbox.queue.filter((m) => m.operation === "steer");
111020
+ const queues = mailbox.queue.filter((m) => m.operation === "queue");
109733
111021
  steers.sort((a, b) => a.seq - b.seq);
109734
111022
  queues.sort((a, b) => a.seq - b.seq);
109735
111023
  return [...steers, ...queues];
@@ -109785,6 +111073,39 @@ function union(...sets) {
109785
111073
  for (const s of sets) for (const v of s) out.add(v);
109786
111074
  return out;
109787
111075
  }
111076
+ /** Strictness order: earlier = narrower (fewer tools permitted). */
111077
+ const CAPABILITY_ORDER = [
111078
+ "read-only",
111079
+ "read-write",
111080
+ "execute",
111081
+ "all"
111082
+ ];
111083
+ /**
111084
+ * The narrower of two capability modes (`read-only` < `read-write` < `execute`
111085
+ * < `all`). Resume uses this to clamp: a resumed agent keeps the tool set it
111086
+ * already has (its profile is not rebuilt), so its contract may be re-applied
111087
+ * or tightened, never widened.
111088
+ */
111089
+ function narrowerCapability(a, b) {
111090
+ return CAPABILITY_ORDER.indexOf(a) <= CAPABILITY_ORDER.indexOf(b) ? a : b;
111091
+ }
111092
+ /**
111093
+ * Infer the strictest capability mode that already permits `activeTools` — the
111094
+ * first mode whose `filterToolsForCapability` pass removes nothing, else `all`.
111095
+ *
111096
+ * Used when resuming a session recorded before `AgentMeta.capabilityMode`
111097
+ * existed: filtering is pure subtraction, so a tool set that survives
111098
+ * `read-only` unchanged is evidence the agent was never granted more than
111099
+ * that. The inference can only be too *narrow* (a set that happens to contain
111100
+ * only allowed tools), which is the safe direction for a capability clamp.
111101
+ */
111102
+ function inferCapabilityFromTools(activeTools) {
111103
+ for (const mode of CAPABILITY_ORDER) {
111104
+ if (mode === "all") break;
111105
+ if (filterToolsForCapability(activeTools, mode).length === activeTools.length) return mode;
111106
+ }
111107
+ return "all";
111108
+ }
109788
111109
  //#endregion
109789
111110
  //#region ../../packages/agent-core/src/session/summary-continuation.md
109790
111111
  var summary_continuation_default = "Your previous response was too brief. Please provide a more comprehensive summary that includes:\n\n1. Specific technical details and implementations\n2. Detailed findings and analysis\n3. All important information that the parent agent should know";
@@ -109809,14 +111130,25 @@ const SUMMARY_CONTINUATION_ATTEMPTS = 1;
109809
111130
  */
109810
111131
  const MAX_PARENT_MESSAGE_DELIVERY_TURNS = 2;
109811
111132
  const HOOK_TEXT_PREVIEW_LENGTH = 500;
111133
+ /**
111134
+ * Upper bound on how long a finished child's resource release may hold back the
111135
+ * `completion` its callers await. A healthy release finishes in well under a
111136
+ * second; the bound only bites when a language server has stopped answering,
111137
+ * where the wait is capped only by the LSP request timeout and every caller —
111138
+ * the foreground Agent tool call, TaskStop, rlm_wait — is parked with it, so
111139
+ * the parent turn looks hung.
111140
+ */
111141
+ const CHILD_RELEASE_BOUND_MS = 5e3;
109812
111142
  const SUBAGENT_MAX_TOKENS_ERROR = "Subagent turn failed before completing its final summary: reason=max_tokens";
109813
111143
  /**
109814
111144
  * Render parent messages as the block a child sees. Both delivery paths share
109815
111145
  * this — the mid-run steer and the turn-start injection — so a message looks
109816
- * identical to the child however it arrives.
111146
+ * identical to the child however it arrives. Anything that is not a plain
111147
+ * `queue` message renders as a `[directive]` (steer, and the interject that
111148
+ * degrades to a mailbox steer).
109817
111149
  */
109818
111150
  function formatParentMessagesBlock(messages) {
109819
- return `[parent_messages]\n${messages.map((m) => m.operation === "steer" ? `[directive] ${m.text}` : `[message] ${m.text}`).join("\n\n")}`;
111151
+ return `[parent_messages]\n${messages.map((m) => m.operation === "queue" ? `[message] ${m.text}` : `[directive] ${m.text}`).join("\n\n")}`;
109820
111152
  }
109821
111153
  var SessionSubagentHost = class {
109822
111154
  session;
@@ -109867,16 +111199,14 @@ var SessionSubagentHost = class {
109867
111199
  runInBackground: options.runInBackground,
109868
111200
  structured: options.outputSchema !== void 0
109869
111201
  });
109870
- const completion = this.runChild(parent, id, agent, profile.name, {
111202
+ const run = this.runChild(parent, id, agent, profile.name, {
109871
111203
  ...options,
109872
111204
  signal: controller.signal
109873
- }, () => this.configureChild(parent, agent, profile, options.capabilityMode)).finally(() => {
109874
- unlinkAbortSignal();
109875
- this.activeChildren.delete(id);
109876
- this.childRequestCounts.delete(id);
109877
- this.childRequestSeen.delete(id);
109878
- this.parentMessageSeen.delete(id);
109879
- });
111205
+ }, () => this.configureChild(parent, agent, profile, id, options.capabilityMode));
111206
+ const completion = this.withChildTerminalRelease(parent, agent, {
111207
+ childId: id,
111208
+ unlinkAbortSignal
111209
+ }, run);
109880
111210
  return {
109881
111211
  agentId: id,
109882
111212
  profileName: profile.name,
@@ -109888,7 +111218,8 @@ var SessionSubagentHost = class {
109888
111218
  options.signal.throwIfAborted();
109889
111219
  const parent = this.session.agents.get(this.ownerAgentId);
109890
111220
  if (parent === void 0) throw new Error(`Parent agent "${this.ownerAgentId}" was not found`);
109891
- const child = this.session.agents.get(agentId);
111221
+ let child = this.session.agents.get(agentId);
111222
+ if (child === void 0 && typeof this.session.ensureAgent === "function") child = await this.session.ensureAgent(agentId).catch(() => void 0);
109892
111223
  if (child === void 0) throw new Error(`Agent instance "${agentId}" was not found`);
109893
111224
  const metadata = this.session.metadata.agents[agentId];
109894
111225
  if (metadata?.type !== "sub") throw new Error(`Agent instance "${agentId}" is not a subagent`);
@@ -109902,30 +111233,33 @@ var SessionSubagentHost = class {
109902
111233
  runInBackground: options.runInBackground,
109903
111234
  structured: options.outputSchema !== void 0
109904
111235
  });
111236
+ const run = this.runChild(parent, agentId, child, profileName, {
111237
+ ...options,
111238
+ signal: controller.signal
111239
+ }, () => {
111240
+ const binding = this.resolveModelBinding(profileName);
111241
+ const modelAlias = this.resolveValidModelAlias(parent, binding);
111242
+ const thinkingLevel = this.resolveThinkingLevel(parent, binding, parent.config.thinkingLevel);
111243
+ child.config.update({
111244
+ modelAlias,
111245
+ thinkingLevel
111246
+ });
111247
+ const baseline = metadata.capabilityMode ?? inferCapabilityFromTools(child.tools.getActiveTools());
111248
+ const requested = options.capabilityMode;
111249
+ const effective = requested === void 0 ? baseline : narrowerCapability(requested, baseline);
111250
+ if (effective !== "all") child.tools.setActiveTools(filterToolsForCapability(child.tools.getActiveTools(), effective));
111251
+ child.setCapabilityMode(effective);
111252
+ this.session.markAgentCapability(agentId, effective);
111253
+ return Promise.resolve();
111254
+ });
109905
111255
  return {
109906
111256
  agentId,
109907
111257
  profileName,
109908
111258
  resumed: true,
109909
- completion: this.runChild(parent, agentId, child, profileName, {
109910
- ...options,
109911
- signal: controller.signal
109912
- }, () => {
109913
- const binding = this.resolveModelBinding(profileName);
109914
- const modelAlias = this.resolveValidModelAlias(parent, binding);
109915
- const thinkingLevel = this.resolveThinkingLevel(parent, binding, parent.config.thinkingLevel);
109916
- child.config.update({
109917
- modelAlias,
109918
- thinkingLevel
109919
- });
109920
- if (options.capabilityMode !== void 0 && options.capabilityMode !== "all") child.tools.setActiveTools(filterToolsForCapability(child.tools.getActiveTools(), options.capabilityMode));
109921
- return Promise.resolve();
109922
- }).finally(() => {
109923
- unlinkAbortSignal();
109924
- this.activeChildren.delete(agentId);
109925
- this.childRequestCounts.delete(agentId);
109926
- this.childRequestSeen.delete(agentId);
109927
- this.parentMessageSeen.delete(agentId);
109928
- })
111259
+ completion: this.withChildTerminalRelease(parent, child, {
111260
+ childId: agentId,
111261
+ unlinkAbortSignal
111262
+ }, run)
109929
111263
  };
109930
111264
  }
109931
111265
  /**
@@ -109953,10 +111287,110 @@ var SessionSubagentHost = class {
109953
111287
  return this.session.agents.get(agentId)?.config.profileName;
109954
111288
  }
109955
111289
  /**
111290
+ * Chain a child run's terminal step: release the child's own resources, then
111291
+ * evict it (bookkeeping, mailbox slot, live Agent reference).
111292
+ *
111293
+ * Both halves have to happen here rather than in `Session.close()`: the child
111294
+ * leaves `session.agents` in this step, so a teardown that walks that map can
111295
+ * no longer reach its LSP servers or its RLM kernel. `run` is awaited inside
111296
+ * the `try` so a run that threw (failure, stop) releases exactly like a
111297
+ * completed one, and the release finishes before the caller's `completion`
111298
+ * settles — awaiting the run also awaits its process teardown (bounded by
111299
+ * `CHILD_RELEASE_BOUND_MS`, see `releaseChildResourcesBounded`).
111300
+ */
111301
+ async withChildTerminalRelease(parent, child, terminal, run) {
111302
+ try {
111303
+ return await run;
111304
+ } finally {
111305
+ terminal.unlinkAbortSignal();
111306
+ await this.releaseChildResourcesBounded(parent, child, terminal.childId);
111307
+ this.activeChildren.delete(terminal.childId);
111308
+ this.childRequestCounts.delete(terminal.childId);
111309
+ this.childRequestSeen.delete(terminal.childId);
111310
+ this.parentMessageSeen.delete(terminal.childId);
111311
+ this.session.removeAgent?.(terminal.childId);
111312
+ if (terminal.childId !== "main") this.session.agents.delete(terminal.childId);
111313
+ }
111314
+ }
111315
+ /**
111316
+ * `releaseChildResources`, awaited for at most `CHILD_RELEASE_BOUND_MS`.
111317
+ *
111318
+ * The healthy path is unchanged: the release is still awaited to completion,
111319
+ * so the "released before evict" ordering holds. The bound only matters for a
111320
+ * child whose language server stopped answering — `LspClient.stop()` waits
111321
+ * for its `shutdown` request, which is capped only by the 120s request
111322
+ * timeout, so an unresponsive server used to hold every caller awaiting
111323
+ * `completion` (foreground Agent call, TaskStop, rlm_wait) for ~125s.
111324
+ *
111325
+ * Past the bound the release keeps running in the background — the process is
111326
+ * still reaped, just not before the caller gets its result — and the wait is
111327
+ * reported once so the stall is visible instead of silent.
111328
+ */
111329
+ async releaseChildResourcesBounded(parent, child, childId) {
111330
+ const release = this.releaseChildResources(parent, child).catch((error) => {
111331
+ parent.log.warn("Failed to release subagent resources", {
111332
+ childId,
111333
+ error: String(error)
111334
+ });
111335
+ });
111336
+ let timer;
111337
+ const bound = new Promise((resolve) => {
111338
+ timer = setTimeout(() => {
111339
+ resolve("bound");
111340
+ }, CHILD_RELEASE_BOUND_MS);
111341
+ });
111342
+ try {
111343
+ if (await Promise.race([release.then(() => "released"), bound]) === "bound") parent.log.warn(`subagent ${childId}: resource release exceeded ${CHILD_RELEASE_BOUND_MS}ms and continues in the background (a language server is likely unresponsive); completion is not held back by it`);
111344
+ } finally {
111345
+ if (timer !== void 0) clearTimeout(timer);
111346
+ }
111347
+ }
111348
+ /**
111349
+ * Stop the resources a finished child owns itself. `Session.close()` is the
111350
+ * only other place that calls `disposeLsp()` / disposes an RLM kernel, and it
111351
+ * iterates `session.agents` — but the terminal `finally` evicts every child
111352
+ * from that map in the same turn, so between the two the child's tsserver and
111353
+ * its python kernel become unreachable while still running: one leaked
111354
+ * process set per subagent run, not per session. Called from the run
111355
+ * `finally`, so all exit paths (completion, failure, stop) release.
111356
+ *
111357
+ * Ownership is per agent, never inherited by reference: each agent's own
111358
+ * `ToolManager.initializeBuiltinTools` constructs its `LspRegistry` and its
111359
+ * `PythonTool` (own kernel process, own rlm() handle table), so disposing the
111360
+ * child's instances cannot reach the parent's. The python tool is
111361
+ * additionally guarded by a reference check against the parent's instance, so
111362
+ * a kernel that ever became inherited-by-reference instead of child-owned
111363
+ * would survive here rather than be killed by mistake.
111364
+ *
111365
+ * The kernel is torn down directly instead of through `Agent.disposeRlm()`:
111366
+ * disposeRlm records `rlm.exit` and drops `python` from the tool set, which
111367
+ * would make a later resume replay this child as RLM-disabled even though the
111368
+ * parent it inherited the mode from is still running RLM. Killing the kernel
111369
+ * leaves no orphan process while keeping the child's mode on its records.
111370
+ */
111371
+ async releaseChildResources(parent, child) {
111372
+ try {
111373
+ await child.tools.disposeLsp();
111374
+ } catch (error) {
111375
+ parent.log.warn("Failed to dispose subagent lsp servers", { error: String(error) });
111376
+ }
111377
+ const python = child.tools.getBuiltinTool("python");
111378
+ if (python === void 0 || python === parent.tools.getBuiltinTool("python")) return;
111379
+ try {
111380
+ python.dispose?.();
111381
+ } catch (error) {
111382
+ parent.log.warn("Failed to dispose subagent rlm kernel", { error: String(error) });
111383
+ }
111384
+ }
111385
+ /**
109956
111386
  * Send a directed message to a subagent owned by this parent. Ownership is
109957
111387
  * verified against the session metadata before anything is enqueued; a
109958
111388
  * message addressed to a foreign or unknown agent is refused as
109959
111389
  * `not_owned`/`not_found`.
111390
+ *
111391
+ * `steer` redirects a running child without disturbing its in-flight tools;
111392
+ * `interject` does the same but additionally interrupts the tool batch in
111393
+ * flight, so a child stuck in a long wait reads the correction immediately.
109960
111394
  */
109961
111395
  sendMessage(toAgentId, operation, text, overrides) {
109962
111396
  const metadata = this.session.metadata.agents[toAgentId];
@@ -109965,7 +111399,7 @@ var SessionSubagentHost = class {
109965
111399
  const child = this.session.agents.get(toAgentId);
109966
111400
  const record = this.activeChildren.get(toAgentId);
109967
111401
  if (child === void 0 || record === void 0) return { status: "not_active" };
109968
- const message = buildSubagentMessage(this.ownerAgentId, toAgentId, operation, text, overrides);
111402
+ const message = buildSubagentMessage(this.ownerAgentId, toAgentId, operation === "queue" ? "queue" : "steer", text, overrides);
109969
111403
  const byteLimit = overrides?.byteLimit ?? 16384;
109970
111404
  if (subagentMessageBytes(text) > byteLimit) return {
109971
111405
  status: "saturated",
@@ -109981,18 +111415,18 @@ var SessionSubagentHost = class {
109981
111415
  status: "accepted",
109982
111416
  duplicate: true
109983
111417
  };
109984
- if (operation === "steer" && !record.structured && child.turn.hasActiveTurn && child.turn.steerQueueLength < 4) {
111418
+ if ((operation === "steer" || operation === "interject") && !record.structured && child.turn.hasActiveTurn && child.turn.steerQueueLength < 4) {
109985
111419
  child.turn.steer([{
109986
111420
  type: "text",
109987
111421
  text: formatParentMessagesBlock([message])
109988
111422
  }], {
109989
111423
  kind: "system_trigger",
109990
- name: "parent_message"
109991
- });
111424
+ name: operation === "interject" ? "parent_interject" : "parent_message"
111425
+ }, operation === "interject" ? { interrupt: true } : void 0);
109992
111426
  seen.add(dedupeKey);
109993
111427
  return {
109994
111428
  status: "accepted",
109995
- delivery: "mid-run"
111429
+ delivery: operation === "interject" ? "interjected" : "mid-run"
109996
111430
  };
109997
111431
  }
109998
111432
  const out = this.bus.send(message);
@@ -110201,7 +111635,7 @@ var SessionSubagentHost = class {
110201
111635
  throw error;
110202
111636
  }
110203
111637
  }
110204
- async configureChild(parent, child, profile, capabilityMode) {
111638
+ async configureChild(parent, child, profile, childId, capabilityMode) {
110205
111639
  const binding = this.resolveModelBinding(profile.name);
110206
111640
  const modelAlias = this.resolveValidModelAlias(parent, binding);
110207
111641
  const thinkingLevel = this.resolveThinkingLevel(parent, binding, parent.config.thinkingLevel);
@@ -110218,6 +111652,7 @@ var SessionSubagentHost = class {
110218
111652
  child.tools.setActiveTools(filtered);
110219
111653
  }
110220
111654
  child.setCapabilityMode(capabilityMode ?? "all");
111655
+ this.session.markAgentCapability(childId, capabilityMode ?? "all");
110221
111656
  }
110222
111657
  resolveModelBinding(profileName) {
110223
111658
  const bindings = this.modelBindings?.();
@@ -110319,6 +111754,63 @@ function lastAssistantText$1(agent) {
110319
111754
  return "";
110320
111755
  }
110321
111756
  //#endregion
111757
+ //#region ../../packages/agent-core/src/session/dispose-registry.ts
111758
+ /**
111759
+ * Session teardown registry.
111760
+ *
111761
+ * `Session.close()` used to run its cleanups as a hand-maintained inline
111762
+ * sequence. Every new resource had to be remembered and spliced into that
111763
+ * sequence by hand, and omissions (the shell tool's pending-task map, the
111764
+ * message bus, ...) stayed invisible until they leaked. This registry makes
111765
+ * the close-out list explicit and inspectable: each step registers once at
111766
+ * session construction under a stable name, `names()` exposes the checklist
111767
+ * (tests freeze it so a dropped or renamed registration fails immediately),
111768
+ * and `disposeAll()` releases the steps in reverse registration order — last
111769
+ * registered, first released — without letting one failure skip the rest.
111770
+ */
111771
+ var SessionDisposables = class {
111772
+ items = [];
111773
+ disposed = false;
111774
+ /**
111775
+ * Register a teardown step under a unique name.
111776
+ *
111777
+ * Duplicate names are rejected rather than silently overwriting: two owners
111778
+ * for one checklist slot is a registration bug, and an overwrite would hide
111779
+ * it. Registration after `disposeAll()` is rejected for the same reason —
111780
+ * the step could never run, which is exactly the silent-omission failure
111781
+ * this registry exists to prevent.
111782
+ */
111783
+ add(name, fn) {
111784
+ if (this.disposed) throw new Error(`Cannot register disposable "${name}" after disposeAll() has run`);
111785
+ if (this.items.some((item) => item.name === name)) throw new Error(`Disposable "${name}" is already registered`);
111786
+ this.items.push({
111787
+ name,
111788
+ fn
111789
+ });
111790
+ }
111791
+ /** Registered names in registration order — the close-out checklist. */
111792
+ names() {
111793
+ return this.items.map((item) => item.name);
111794
+ }
111795
+ /**
111796
+ * Run every registered step in reverse registration order. A throwing step
111797
+ * never blocks the steps after it; every failure is collected and returned
111798
+ * as one `AggregateError` (`null` when all steps succeeded). Idempotent:
111799
+ * once disposed, further calls run nothing and report no errors.
111800
+ */
111801
+ async disposeAll() {
111802
+ if (this.disposed) return null;
111803
+ this.disposed = true;
111804
+ const errors = [];
111805
+ for (const item of this.items.toReversed()) try {
111806
+ await item.fn();
111807
+ } catch (error) {
111808
+ errors.push(error);
111809
+ }
111810
+ return errors.length === 0 ? null : new AggregateError(errors, `${String(errors.length)} session disposal step(s) failed`);
111811
+ }
111812
+ };
111813
+ //#endregion
110322
111814
  //#region ../../packages/agent-core/src/session/index.ts
110323
111815
  const BACKGROUND_KEEP_ALIVE_ON_EXIT_ENV = "SCREAM_CODE_BACKGROUND_KEEP_ALIVE_ON_EXIT";
110324
111816
  var Session$1 = class {
@@ -110332,6 +111824,10 @@ var Session$1 = class {
110332
111824
  hookEngine;
110333
111825
  /** Core-wide supervisor tracking this session's LSP children. */
110334
111826
  lspSupervisor;
111827
+ /** Close-out checklist that drives `close()` (see `SessionDisposables`). */
111828
+ disposables = new SessionDisposables();
111829
+ /** Session-level parent→child message bus shared by every subagent host. */
111830
+ subagentMessages = new SubagentMessageBus();
110335
111831
  agentIdCounter = 0;
110336
111832
  skillsReady;
110337
111833
  mcpReady;
@@ -110373,6 +111869,52 @@ var Session$1 = class {
110373
111869
  this.mcpReady = this.loadMcpServers().catch((error) => {
110374
111870
  this.emitInitialMcpLoadError(error);
110375
111871
  });
111872
+ this.registerDisposables();
111873
+ }
111874
+ /**
111875
+ * The session's close-out checklist. Every resource `close()` tears down
111876
+ * registers here at construction — `disposables.names()` is the inspectable
111877
+ * list, frozen by test/session/close-resources.test.ts so a step cannot be
111878
+ * silently forgotten again.
111879
+ *
111880
+ * Registration order is the checklist order; `disposeAll()` releases in
111881
+ * reverse (last registered first released), so `message-bus` is released
111882
+ * first and the session log sink last: every teardown step above it (LSP,
111883
+ * MCP, ...) can still report what it did through `log` instead of writing
111884
+ * into a closed sink. `session-finalize` is registered directly after `log`
111885
+ * for the same reason — it is released immediately before the sink closes,
111886
+ * once every resource teardown above it has run.
111887
+ */
111888
+ registerDisposables() {
111889
+ this.disposables.add("log", async () => {
111890
+ await this.logHandle?.close();
111891
+ });
111892
+ this.disposables.add("session-finalize", async () => {
111893
+ try {
111894
+ await this.flushMetadata();
111895
+ } finally {
111896
+ await this.triggerSessionEnd("exit");
111897
+ }
111898
+ });
111899
+ this.disposables.add("background-pending", async () => {
111900
+ stopAllPendingBackgroundTasks(this.options.id);
111901
+ await this.stopBackgroundTasksOnExit();
111902
+ });
111903
+ this.disposables.add("cron", async () => {
111904
+ await Promise.allSettled(Array.from(this.agents.values(), async (agent) => agent.cron?.stop()));
111905
+ });
111906
+ this.disposables.add("rlm", () => {
111907
+ for (const agent of this.agents.values()) agent.disposeRlm?.();
111908
+ });
111909
+ this.disposables.add("lsp", async () => {
111910
+ await Promise.all(Array.from(this.agents.values(), (agent) => agent.tools.disposeLsp()));
111911
+ });
111912
+ this.disposables.add("mcp", async () => {
111913
+ await this.mcp.shutdown();
111914
+ });
111915
+ this.disposables.add("message-bus", () => {
111916
+ this.subagentMessages.clear();
111917
+ });
110376
111918
  }
110377
111919
  async createMain() {
110378
111920
  const { agent } = await this.createAgent({ type: "main" }, DEFAULT_AGENT_PROFILES["agent"]);
@@ -110425,21 +111967,13 @@ var Session$1 = class {
110425
111967
  });
110426
111968
  }
110427
111969
  }
111970
+ let disposeError = null;
110428
111971
  try {
110429
111972
  for (const agent of this.agents.values()) if (agent.turn.hasActiveTurn) agent.turn.cancel();
110430
- await Promise.allSettled(Array.from(this.agents.values(), async (agent) => agent.cron?.stop()));
110431
- for (const agent of this.agents.values()) agent.disposeRlm?.();
110432
- await this.stopBackgroundTasksOnExit();
110433
- await this.flushMetadata();
110434
- await this.triggerSessionEnd("exit");
110435
111973
  } finally {
110436
- try {
110437
- const failed = (await Promise.allSettled([...Array.from(this.agents.values(), (agent) => agent.tools.disposeLsp()), this.mcp.shutdown()])).find((result) => result.status === "rejected");
110438
- if (failed !== void 0) throw failed.reason;
110439
- } finally {
110440
- await this.logHandle?.close();
110441
- }
111974
+ disposeError = await this.disposables.disposeAll();
110442
111975
  }
111976
+ if (disposeError !== null) throw disposeError;
110443
111977
  }
110444
111978
  async stopBackgroundTasksOnExit() {
110445
111979
  if (resolveConfigValue({
@@ -110539,6 +112073,48 @@ var Session$1 = class {
110539
112073
  this.writeMetadataPromise = this.writeMetadataPromise.then(() => write(), () => write());
110540
112074
  return this.writeMetadataPromise;
110541
112075
  }
112076
+ /**
112077
+ * Record a subagent's runtime capability contract in session metadata so it
112078
+ * survives a process restart (see AgentMeta.capabilityMode). Every spawned
112079
+ * child records a mode from its very first write — `all` included, since it
112080
+ * is a real contract, not an absent one. A missing field therefore means
112081
+ * exactly one thing: a session written before this field existed, which
112082
+ * resume resolves by inferring the contract from the live tool set.
112083
+ */
112084
+ markAgentCapability(agentId, mode) {
112085
+ const meta = this.metadata.agents[agentId];
112086
+ if (meta === void 0) return;
112087
+ if (meta.capabilityMode === mode) return;
112088
+ this.metadata.agents[agentId] = {
112089
+ ...meta,
112090
+ capabilityMode: mode
112091
+ };
112092
+ this.writeMetadata().catch((error) => {
112093
+ this.log.error("failed to write session metadata after a capability change", error);
112094
+ });
112095
+ }
112096
+ /**
112097
+ * Drop a finished subagent's live instance so its context/tools no longer
112098
+ * pin memory on the session map. Metadata and wire records stay so a later
112099
+ * `ensureAgent` (subagent resume) can re-hydrate the same conversation.
112100
+ * Never drops the main agent.
112101
+ */
112102
+ removeAgent(id) {
112103
+ if (id === "main") return;
112104
+ this.agents.delete(id);
112105
+ }
112106
+ /**
112107
+ * Return the live instance for `id`, re-hydrating from persisted records
112108
+ * when `removeAgent` dropped it after a finished run. No-op for agents that
112109
+ * are still resident.
112110
+ */
112111
+ async ensureAgent(id) {
112112
+ const existing = this.agents.get(id);
112113
+ if (existing !== void 0) return existing;
112114
+ const agent = this.ensureResumeAgentInstantiated(id, this.metadata.agents);
112115
+ await agent.resume();
112116
+ return agent;
112117
+ }
110542
112118
  async readMetadata() {
110543
112119
  let text;
110544
112120
  try {
@@ -110677,7 +112253,9 @@ var Session$1 = class {
110677
112253
  const cwd = parentAgent?.config.cwd ?? this.options.jian.getcwd();
110678
112254
  return new Agent({
110679
112255
  ...config,
112256
+ agentId: id,
110680
112257
  type,
112258
+ sessionId: this.options.id,
110681
112259
  jian: this.options.jian.withCwd(cwd),
110682
112260
  toolServices: this.options.toolServices,
110683
112261
  config: this.options.config,
@@ -110688,7 +112266,7 @@ var Session$1 = class {
110688
112266
  rpc: proxyWithExtraPayload(this.rpc, { agentId: id }),
110689
112267
  modelProvider: this.options.providerManager,
110690
112268
  hookEngine: config.hookEngine ?? this.hookEngine,
110691
- subagentHost: config.subagentHost ?? new SessionSubagentHost(this, id, this.backgroundTaskTimeoutMs(), this.options.subagentModelBindings),
112269
+ subagentHost: config.subagentHost ?? new SessionSubagentHost(this, id, this.backgroundTaskTimeoutMs(), this.options.subagentModelBindings, this.subagentMessages),
110692
112270
  ownerHost: type === "sub" ? parentAgent?.subagentHost : void 0,
110693
112271
  mcp: this.mcp,
110694
112272
  permission: this.permissionOptions(parentAgentId, config.permission),
@@ -125156,7 +126734,7 @@ var OpenAlexPaperSource = class {
125156
126734
  };
125157
126735
  //#endregion
125158
126736
  //#region ../../packages/agent-core/src/session/export/manifest.ts
125159
- const WIRE_PROTOCOL_VERSION = "1.4";
126737
+ const WIRE_PROTOCOL_VERSION = "1.6";
125160
126738
  function buildExportManifest(args) {
125161
126739
  return {
125162
126740
  sessionId: args.summary.id,
@@ -129181,8 +130759,9 @@ var Session = class {
129181
130759
  enabled
129182
130760
  });
129183
130761
  }
129184
- /** Sets the maximum RLM recursion depth (default 1). Depth 1 means the root
129185
- * kernel may spawn rlm() children but they cannot spawn grandchildren. */
130762
+ /** Sets the maximum RLM recursion depth. The default is unlimited
130763
+ * (`Infinity`); 0 also means unlimited. Depth 1 means the root kernel may
130764
+ * spawn rlm() children but they cannot spawn grandchildren. */
129186
130765
  async setRlmMaxDepth(maxDepth) {
129187
130766
  this.ensureOpen();
129188
130767
  await this.rpc.setRlmMaxDepth({
@@ -131253,6 +132832,38 @@ function resolveConnectCatalogRequest(args) {
131253
132832
  };
131254
132833
  }
131255
132834
  //#endregion
132835
+ //#region src/tui/utils/image-config.ts
132836
+ function getImageConfigPath() {
132837
+ return join(resolveScreamHome(), "image-config.json");
132838
+ }
132839
+ /** Placeholder rules mirrored from the ImageGenerate tool's `isPlaceholder`. */
132840
+ function isPlaceholder(value) {
132841
+ const trimmed = value.trim();
132842
+ return trimmed.length === 0 || trimmed.includes("replace-with") || trimmed.startsWith("<") && trimmed.endsWith(">");
132843
+ }
132844
+ /**
132845
+ * Best-effort read of an existing image config (never throws, never exposes
132846
+ * the key). The `usable` predicate mirrors the ImageGenerate tool's
132847
+ * `loadConfig` (packages/agent-core/src/tools/builtin/image/image-generate.ts):
132848
+ * a config the tool would reject must never be advertised as "configured".
132849
+ */
132850
+ async function readExistingImageConfig() {
132851
+ try {
132852
+ const text = await readFile(getImageConfigPath(), "utf8");
132853
+ const parsed = JSON.parse(text);
132854
+ const urlRaw = typeof parsed["url"] === "string" ? parsed["url"] : "";
132855
+ const legacyRaw = typeof parsed["base_url"] === "string" ? parsed["base_url"] : "";
132856
+ const url = !isPlaceholder(urlRaw) ? urlRaw.trim() : !isPlaceholder(legacyRaw) ? `${legacyRaw.trim().replace(/\/+$/, "")}/images/generations` : "";
132857
+ const model = typeof parsed["model"] === "string" ? parsed["model"].trim() : "";
132858
+ const apiKeyRaw = typeof parsed["api_key"] === "string" ? parsed["api_key"] : "";
132859
+ if (url.length > 0 && model.length > 0 && !isPlaceholder(apiKeyRaw)) return {
132860
+ provider: typeof parsed["provider"] === "string" ? parsed["provider"] : "openai-compatible",
132861
+ model,
132862
+ url
132863
+ };
132864
+ } catch {}
132865
+ }
132866
+ //#endregion
131256
132867
  //#region src/tui/components/dialogs/api-key-input-dialog.ts
131257
132868
  function getFooter() {
131258
132869
  return t("apikey.footer");
@@ -131499,6 +133110,11 @@ var ModelSelectorComponent = class extends Container {
131499
133110
  lines.push(this.renderVideoControl(selected.model));
131500
133111
  lines.push(this.renderAudioControl(selected.model));
131501
133112
  }
133113
+ if (this.opts.imageGeneration !== void 0) {
133114
+ const imageGeneration = this.opts.imageGeneration;
133115
+ lines.push("");
133116
+ lines.push(chalk.hex(colors.textMuted)(imageGeneration.configured && imageGeneration.model !== void 0 ? t("model.image_gen_configured", { model: imageGeneration.model }) : t("model.image_gen_missing")));
133117
+ }
131502
133118
  lines.push("");
131503
133119
  if (view.page.pageCount > 1) lines.push(chalk.hex(colors.textMuted)(` Page ${String(view.page.page + 1)}/${String(view.page.pageCount)}`));
131504
133120
  lines.push(chalk.hex(colors.primary)("─".repeat(width)));
@@ -132033,26 +133649,6 @@ const IMAGE_PROVIDER_PRESETS = [
132033
133649
  model: "gpt-image-2"
132034
133650
  }
132035
133651
  ];
132036
- function getImageConfigPath() {
132037
- return join(resolveScreamHome(), "image-config.json");
132038
- }
132039
- /** Best-effort read of an existing image config (never throws, never exposes the key). */
132040
- async function readExistingImageConfig() {
132041
- try {
132042
- const text = await readFile(getImageConfigPath(), "utf8");
132043
- const parsed = JSON.parse(text);
132044
- const urlDirect = typeof parsed["url"] === "string" ? parsed["url"].trim() : "";
132045
- const legacyBase = typeof parsed["base_url"] === "string" ? parsed["base_url"].trim() : "";
132046
- const url = urlDirect.length > 0 ? urlDirect : legacyBase.length > 0 ? `${legacyBase.replace(/\/+$/, "")}/images/generations` : "";
132047
- const model = typeof parsed["model"] === "string" ? parsed["model"].trim() : "";
132048
- const apiKey = typeof parsed["api_key"] === "string" ? parsed["api_key"].trim() : "";
132049
- if (url.length > 0 && !url.includes("replace-with") && !(url.startsWith("<") && url.endsWith(">")) && model.length > 0 && apiKey.length > 0 && !apiKey.includes("replace-with") && !(apiKey.startsWith("<") && apiKey.endsWith(">"))) return {
132050
- provider: typeof parsed["provider"] === "string" ? parsed["provider"] : "openai-compatible",
132051
- model,
132052
- url
132053
- };
132054
- } catch {}
132055
- }
132056
133652
  /**
132057
133653
  * Normalize the URL exactly as the user typed it: trim whitespace and
132058
133654
  * trailing slashes only. Nothing is ever appended or completed — the full
@@ -133511,7 +135107,14 @@ const DEFAULT_SUBAGENT_TYPES = Object.freeze(subagentRoster().map((entry) => ent
133511
135107
  const REQUESTING_WINDOW_MS = 5e3;
133512
135108
  var SubagentSlots = class {
133513
135109
  slots = /* @__PURE__ */ new Map();
135110
+ /** Live per-agentId records. Released on termination (a session must not keep
135111
+ * one entry per spawned agent forever); resume detection falls back to
135112
+ * {@link endedInstances}. */
133514
135113
  instances = /* @__PURE__ */ new Map();
135114
+ /** Terminated records, oldest first — the only reason a per-agentId record
135115
+ * outlives its run is that a resume of it should still show 返工中. Capped by
135116
+ * {@link ENDED_INSTANCE_WINDOW}. */
135117
+ endedInstances = /* @__PURE__ */ new Map();
133515
135118
  /** Live instance count per type: a slot only returns to idle when the LAST
133516
135119
  * instance of that type terminates (WolfPack spawns several coders at once
133517
135120
  * and they share one visual slot). */
@@ -133531,6 +135134,7 @@ var SubagentSlots = class {
133531
135134
  reset() {
133532
135135
  this.slots.clear();
133533
135136
  this.instances.clear();
135137
+ this.endedInstances.clear();
133534
135138
  this.runningByType.clear();
133535
135139
  this.messagingByCall.clear();
133536
135140
  this.messagingPrev.clear();
@@ -133550,6 +135154,17 @@ var SubagentSlots = class {
133550
135154
  this.setState(instance.type, "reworking", agentId, description === void 0 ? void 0 : `resume: ${description}`);
133551
135155
  return;
133552
135156
  }
135157
+ const ended = this.endedInstances.get(agentId);
135158
+ if (ended !== void 0) {
135159
+ this.endedInstances.delete(agentId);
135160
+ this.instances.set(agentId, {
135161
+ type: ended.type,
135162
+ generation: ended.generation + 1
135163
+ });
135164
+ this.runningByType.set(ended.type, (this.runningByType.get(ended.type) ?? 0) + 1);
135165
+ this.setState(ended.type, "reworking", agentId, description === void 0 ? void 0 : `resume: ${description}`);
135166
+ return;
135167
+ }
133553
135168
  this.instances.set(agentId, {
133554
135169
  type: subagentType,
133555
135170
  generation: 1
@@ -133576,6 +135191,8 @@ var SubagentSlots = class {
133576
135191
  const instance = this.instances.get(agentId);
133577
135192
  if (instance === void 0) return;
133578
135193
  if (this.requesting.get(instance.type)?.agentId === agentId) this.requesting.delete(instance.type);
135194
+ this.instances.delete(agentId);
135195
+ this.rememberEnded(agentId, instance);
133579
135196
  const remaining = (this.runningByType.get(instance.type) ?? 1) - 1;
133580
135197
  if (remaining > 0) {
133581
135198
  this.runningByType.set(instance.type, remaining);
@@ -133587,6 +135204,17 @@ var SubagentSlots = class {
133587
135204
  this.runningByType.delete(instance.type);
133588
135205
  this.setState(instance.type, "idle", void 0, void 0);
133589
135206
  }
135207
+ /** Move a terminated record into the resume window, FIFO-capped: the oldest
135208
+ * record drops out so the window cannot grow with the session. */
135209
+ rememberEnded(agentId, instance) {
135210
+ this.endedInstances.delete(agentId);
135211
+ this.endedInstances.set(agentId, instance);
135212
+ while (this.endedInstances.size > 200) {
135213
+ const oldest = this.endedInstances.keys().next().value;
135214
+ if (oldest === void 0) break;
135215
+ this.endedInstances.delete(oldest);
135216
+ }
135217
+ }
133590
135218
  /**
133591
135219
  * Routed subagent activity (agentId !== main):
133592
135220
  * - kind 'tool' → working (tool call in flight)
@@ -133977,15 +135605,12 @@ function tierFor(intensity) {
133977
135605
  if (intensity >= TIER_MID) return "mid";
133978
135606
  return "low";
133979
135607
  }
133980
- const shimmerDefaultCache = /* @__PURE__ */ new WeakMap();
133981
135608
  function defaultPalette(colors) {
133982
- const p = {
135609
+ return {
133983
135610
  low: colors.textDim,
133984
135611
  mid: colors.textMuted,
133985
135612
  high: colors.primary
133986
135613
  };
133987
- shimmerDefaultCache.set(colors, p);
133988
- return p;
133989
135614
  }
133990
135615
  /**
133991
135616
  * Apply a shimmer sweep across `text` using the default three-tier palette
@@ -134273,6 +135898,10 @@ var FooterComponent = class {
134273
135898
  /** Stop the active-status timer. Idempotent and safe during disposal. */
134274
135899
  dispose() {
134275
135900
  this.#stopStatusTimer();
135901
+ if (this.balanceFlashTimer !== void 0) {
135902
+ clearTimeout(this.balanceFlashTimer);
135903
+ this.balanceFlashTimer = void 0;
135904
+ }
134276
135905
  }
134277
135906
  #restartStatusTimer(phase, goalActive) {
134278
135907
  this.#stopStatusTimer();
@@ -134580,6 +136209,10 @@ function parseColorFgBg(value) {
134580
136209
  */
134581
136210
  const dark = {
134582
136211
  yellowGreen: "#79eb00",
136212
+ gold: "#FFD700",
136213
+ coral: "#FF7043",
136214
+ skyBlue: "#9CDCFE",
136215
+ violet: "#C084FC",
134583
136216
  pink400: "#FF6B9D",
134584
136217
  cyan400: "#56D4DD",
134585
136218
  amber400: "#E8A838",
@@ -134596,6 +136229,10 @@ const dark = {
134596
136229
  };
134597
136230
  const light = {
134598
136231
  yellowGreen700: "#4B7A06",
136232
+ gold: "#A16207",
136233
+ orange: "#C2410C",
136234
+ blue: "#1565C0",
136235
+ violet: "#7C3AED",
134599
136236
  pink700: "#C2185B",
134600
136237
  cyan800: "#006978",
134601
136238
  gray900: "#1A1A1A",
@@ -134612,14 +136249,19 @@ const darkColors = {
134612
136249
  accent: dark.pink400,
134613
136250
  planMode: dark.cyan400,
134614
136251
  fusionPlanMode: dark.amber500,
134615
- wolfpackMode: "#C084FC",
134616
- rlmMode: "#FFD700",
136252
+ wolfpackMode: dark.violet,
136253
+ rlmMode: dark.gold,
136254
+ permissionManual: dark.yellowGreen,
136255
+ permissionYolo: dark.gold,
136256
+ permissionAuto: dark.coral,
136257
+ permissionAsk: dark.skyBlue,
136258
+ permissionBot: dark.violet,
134617
136259
  text: dark.gray100,
134618
136260
  textStrong: dark.gray50,
134619
136261
  textDim: dark.gray500,
134620
136262
  textMuted: dark.gray600,
134621
136263
  mdLink: "#56B6C2",
134622
- mdCodeBlock: "#9CDCFE",
136264
+ mdCodeBlock: dark.skyBlue,
134623
136265
  mdCodeBlockBorder: "#5C6370",
134624
136266
  mdCodeBlockBg: "#16191f",
134625
136267
  mdQuote: "#7F848E",
@@ -134647,14 +136289,19 @@ const lightColors = {
134647
136289
  accent: light.pink700,
134648
136290
  planMode: light.cyan800,
134649
136291
  fusionPlanMode: light.amber700,
134650
- wolfpackMode: "#7C3AED",
134651
- rlmMode: "#A16207",
136292
+ wolfpackMode: light.violet,
136293
+ rlmMode: light.gold,
136294
+ permissionManual: light.yellowGreen700,
136295
+ permissionYolo: light.gold,
136296
+ permissionAuto: light.orange,
136297
+ permissionAsk: light.blue,
136298
+ permissionBot: light.violet,
134652
136299
  text: light.gray900,
134653
136300
  textStrong: light.gray900,
134654
136301
  textDim: light.gray700,
134655
136302
  textMuted: light.gray600,
134656
136303
  mdLink: "#007A8A",
134657
- mdCodeBlock: "#1565C0",
136304
+ mdCodeBlock: light.blue,
134658
136305
  mdCodeBlockBorder: "#848484",
134659
136306
  mdCodeBlockBg: "#f7f9fb",
134660
136307
  mdQuote: "#616161",
@@ -135478,9 +137125,16 @@ async function loadManagedUsageReport(host) {
135478
137125
  * not hammer the vendor endpoint.
135479
137126
  */
135480
137127
  const BALANCE_CACHE_MS = 6e4;
137128
+ /** Upper bound on cached providers — bounds the map when users hop between
137129
+ * many provider/model combinations in one session. */
137130
+ const BALANCE_CACHE_MAX_ENTRIES = 32;
135481
137131
  const cache$1 = /* @__PURE__ */ new Map();
135482
137132
  /** Monotonic id so an out-of-order lookup can never overwrite a newer one. */
135483
137133
  let latestRequestId = 0;
137134
+ /** Drop entries whose TTL has elapsed so the map never holds dead weight. */
137135
+ function pruneExpiredBalances(now) {
137136
+ for (const [key, entry] of cache$1) if (now - entry.at >= BALANCE_CACHE_MS) cache$1.delete(key);
137137
+ }
135484
137138
  function resolveApiKey(providerName) {
135485
137139
  const provider = readConfigFile(join(getDataDir(), "config.toml")).providers?.[providerName];
135486
137140
  if (!provider) return void 0;
@@ -135521,9 +137175,19 @@ async function loadBalance(providerName) {
135521
137175
  async function getProviderBalanceForModel(model) {
135522
137176
  const providerName = model.split("/")[0] ?? "";
135523
137177
  if (providerName.length === 0) return null;
137178
+ const now = Date.now();
135524
137179
  const cached = cache$1.get(providerName);
135525
- if (cached !== void 0 && Date.now() - cached.at < BALANCE_CACHE_MS) return cached.balance;
137180
+ if (cached !== void 0) {
137181
+ if (now - cached.at < BALANCE_CACHE_MS) return cached.balance;
137182
+ cache$1.delete(providerName);
137183
+ }
135526
137184
  const balance = await loadBalance(providerName);
137185
+ pruneExpiredBalances(Date.now());
137186
+ while (cache$1.size >= BALANCE_CACHE_MAX_ENTRIES) {
137187
+ const oldest = cache$1.keys().next();
137188
+ if (oldest.done === true) break;
137189
+ cache$1.delete(oldest.value);
137190
+ }
135527
137191
  cache$1.set(providerName, {
135528
137192
  balance,
135529
137193
  at: Date.now()
@@ -135815,7 +137479,8 @@ async function handleScriptCommand(host, args) {
135815
137479
  }
135816
137480
  }
135817
137481
  /** /rlm-max-depth [N] — query or set the maximum RLM recursion depth.
135818
- * N=0 or no limit means unlimited (the default). */
137482
+ * N=0 or no limit means unlimited (the default). Validation is shared with
137483
+ * the core setter and the web REST route (agent-core session/rlm-settings). */
135819
137484
  async function handleRlmMaxDepthCommand(host, args) {
135820
137485
  const session = host.session;
135821
137486
  if (session === void 0) {
@@ -135824,18 +137489,19 @@ async function handleRlmMaxDepthCommand(host, args) {
135824
137489
  }
135825
137490
  const arg = args.trim();
135826
137491
  if (arg.length === 0) {
135827
- host.showStatus("RLM max depth: run /rlm-max-depth <N> (0 or blank = unlimited) to set it.");
137492
+ const current = host.state.appState.rlmMaxDepth;
137493
+ host.showStatus(`RLM max depth: ${current === null ? "unlimited" : current}`);
135828
137494
  return;
135829
137495
  }
135830
- const parsed = Number(arg);
135831
- if (!Number.isInteger(parsed) || parsed < 0) {
135832
- host.showError(`Invalid rlm-max-depth: ${arg} (expected a non-negative integer, 0 = unlimited).`);
137496
+ const parsed = parseRlmMaxDepthArg(arg);
137497
+ if (!parsed.ok) {
137498
+ host.showError(`Invalid rlm-max-depth: ${arg} (${parsed.reason}).`);
135833
137499
  return;
135834
137500
  }
135835
137501
  try {
135836
- await session.setRlmMaxDepth(parsed);
135837
- if (parsed === 0) host.showStatus("RLM recursion depth set to unlimited.", host.state.theme.colors.success);
135838
- else host.showStatus(`RLM max depth set to ${parsed}.`, host.state.theme.colors.success);
137502
+ await session.setRlmMaxDepth(parsed.value);
137503
+ if (parsed.value === 0) host.showStatus("RLM recursion depth set to unlimited.", host.state.theme.colors.success);
137504
+ else host.showStatus(`RLM max depth set to ${parsed.value}.`, host.state.theme.colors.success);
135839
137505
  } catch (error) {
135840
137506
  const msg = formatErrorMessage(error);
135841
137507
  host.showError(`Failed to set rlm max depth: ${msg}`);
@@ -135954,14 +137620,14 @@ async function handleModelCommand(host, args) {
135954
137620
  const alias = trimmed;
135955
137621
  if (alias.length === 0) {
135956
137622
  await refreshModelsForPicker(host);
135957
- showModelPicker(host);
137623
+ await showModelPicker(host);
135958
137624
  return;
135959
137625
  }
135960
137626
  if (host.state.appState.availableModels[alias] === void 0) {
135961
137627
  host.showError(`Unknown model alias: ${alias}`);
135962
137628
  return;
135963
137629
  }
135964
- showModelPicker(host, alias);
137630
+ await showModelPicker(host, alias);
135965
137631
  }
135966
137632
  /**
135967
137633
  * Reload provider/model config before showing the model picker so models
@@ -136021,11 +137687,12 @@ async function applyEditorChoice(host, value) {
136021
137687
  host.setAppState({ editorCommand });
136022
137688
  host.showStatus(value.length > 0 ? `Editor set to "${value}".` : "编辑器设置为自动检测 ($VISUAL / $EDITOR)。");
136023
137689
  }
136024
- function showModelPicker(host, selectedValue = host.state.appState.model) {
137690
+ async function showModelPicker(host, selectedValue = host.state.appState.model) {
136025
137691
  if (Object.entries(host.state.appState.availableModels).length === 0) {
136026
137692
  host.showNotice("未配置模型", "运行 /config 自定义模型配置。");
136027
137693
  return;
136028
137694
  }
137695
+ const imageConfig = await readExistingImageConfig();
136029
137696
  host.mountEditorReplacement(new ModelSelectorComponent({
136030
137697
  models: host.state.appState.availableModels,
136031
137698
  currentValue: host.state.appState.model,
@@ -136033,6 +137700,10 @@ function showModelPicker(host, selectedValue = host.state.appState.model) {
136033
137700
  currentThinkingLevel: host.state.appState.thinkingLevel,
136034
137701
  colors: host.state.theme.colors,
136035
137702
  searchable: true,
137703
+ imageGeneration: imageConfig === void 0 ? { configured: false } : {
137704
+ configured: true,
137705
+ model: imageConfig.model
137706
+ },
136036
137707
  onSelect: ({ alias, thinkingLevel }) => {
136037
137708
  host.restoreEditor();
136038
137709
  performModelSwitch(host, alias, thinkingLevel);
@@ -137082,7 +138753,7 @@ async function guidedGoalSetup(host) {
137082
138753
  host.showNotice(t("goal.storm_breaker"), t("goal.conflict_loop"));
137083
138754
  return;
137084
138755
  }
137085
- const { TextInputDialogComponent } = await import("./text-input-dialog-DcjQLJFz.mjs");
138756
+ const { TextInputDialogComponent } = await import("./text-input-dialog-DZoGkbYZ.mjs");
137086
138757
  const initialDesc = await promptText(host, TextInputDialogComponent, {
137087
138758
  title: t("goal.setup_title_initial"),
137088
138759
  subtitle: t("goal.setup_desc_hint"),
@@ -137103,7 +138774,7 @@ async function guidedGoalSetup(host) {
137103
138774
  await showGoalConfigWizard(host, session, confirmed.trim() || objective, false);
137104
138775
  }
137105
138776
  async function showGoalConfigWizard(host, session, objective, replace) {
137106
- const { TextInputDialogComponent } = await import("./text-input-dialog-DcjQLJFz.mjs");
138777
+ const { TextInputDialogComponent } = await import("./text-input-dialog-DZoGkbYZ.mjs");
137107
138778
  const turnInput = await promptNumber(host, TextInputDialogComponent, {
137108
138779
  title: t("goal.wizard_title", { objective }),
137109
138780
  subtitle: t("goal.budget_turns_hint"),
@@ -140293,24 +141964,259 @@ function trimTrailingEmptyLines(lines) {
140293
141964
  return lines.slice(0, end);
140294
141965
  }
140295
141966
  /**
140296
- * Returns the tail of `text` whose UTF-8 byte length is at most `maxBytes`.
141967
+ * Returns the tail of `text` whose UTF-8 byte length is at most `maxBytes`
141968
+ * (plus one byte when a torn sequence's introducer ESC sits immediately before
141969
+ * the cut and the sequence's remainder provably closes — keeping the ESC makes
141970
+ * the sequence whole and hides its fragment), with the head aligned to a
141971
+ * boundary no ANSI escape sequence spans.
140297
141972
  * Iterates by Unicode code points so multi-byte characters and surrogate
140298
- * pairs are never split.
141973
+ * pairs are never split, then hands the cut to `safeTailStart`: success output
141974
+ * keeps its ANSI codes (only error output is stripped), and a raw byte cut
141975
+ * could land inside a sequence, leaving a fragment (`m`, `0m`, `[31`) that
141976
+ * `sanitizeShellOutput` cannot recognize because its ESC byte was cut away.
140299
141977
  */
140300
141978
  function truncateTailBytes(text, maxBytes) {
140301
141979
  if (Buffer.byteLength(text, "utf8") <= maxBytes) return text;
140302
141980
  const chars = Array.from(text);
140303
- let start = chars.length;
140304
141981
  let bytes = 0;
141982
+ let units = 0;
140305
141983
  for (let i = chars.length - 1; i >= 0; i--) {
140306
141984
  const char = chars[i];
140307
141985
  if (char === void 0) continue;
140308
141986
  const charBytes = Buffer.byteLength(char, "utf8");
140309
141987
  if (bytes + charBytes > maxBytes) break;
140310
141988
  bytes += charBytes;
140311
- start = i;
141989
+ units += char.length;
141990
+ }
141991
+ return text.slice(safeTailStart(text, text.length - units));
141992
+ }
141993
+ const ESC = "\x1B";
141994
+ /**
141995
+ * Largest share of a retained tail the boundary rules may discard.
141996
+ *
141997
+ * Both rules trade content for a head that is whole by construction: rule 1
141998
+ * restarts after the next `\n` and drops the torn line, and rule 2 skips the
141999
+ * remainder of a torn sequence. The trade only pays while the tail keeps a
142000
+ * (non-strict) majority — with one 5000-char line ahead, a 100-byte budget
142001
+ * would come back with the few bytes after it, and the reader asked for the
142002
+ * newest output, not for a tidy head. Past the bound the raw cut keeps the
142003
+ * content: a string payload stays visible, and a torn CSI or two-byte escape
142004
+ * run keeps the fragment it left in view, because skipping that remainder
142005
+ * would cost more than the fragment it hides — unless the introducer ESC sits
142006
+ * immediately before the head *and* the sequence provably closes, where one
142007
+ * unit of look-back keeps the sequence whole and the fragment never surfaces.
142008
+ * An unclosed run keeps the raw cut: with no terminator to hand the sanitizer,
142009
+ * backing onto its ESC would surface a bare ESC the sanitizer cannot strip.
142010
+ */
142011
+ const MAX_TAIL_DROP_SHARE = .5;
142012
+ /**
142013
+ * Move a tail cut point forward to a boundary no ANSI escape sequence spans.
142014
+ *
142015
+ * A raw `slice(-tail)` can land inside a sequence (`…\u001b[31|m…`), leaving a
142016
+ * fragment (`m`, `0m`, `[31`) whose ESC byte was cut away —
142017
+ * `sanitizeShellOutput` only recognizes complete sequences, so the fragment
142018
+ * renders as text. Two provable rules keep the head clean:
142019
+ *
142020
+ * 1. Line alignment: a CSI sequence cannot contain `\n` (LF is not a
142021
+ * parameter, intermediate or final byte), so a cut that lands mid-line
142022
+ * restarts after the next `\n`. The only loss is the torn partial line,
142023
+ * whose head cannot be trusted; every escape after the newline is whole.
142024
+ * Bounded by `MAX_TAIL_DROP_SHARE`, so one huge line cannot eat the budget.
142025
+ * 2. Torn-sequence skip: when the retained text is one line (no `\n` after
142026
+ * the cut), or an OSC/DCS/PM/APC payload spans the newline the cut aligned
142027
+ * to, the dropped prefix is inspected for a sequence still open at the cut
142028
+ * — a run of CSI body bytes (0x20–0x3F) reaching back to `ESC [`, an `ESC`
142029
+ * whose intermediate bytes were torn (`ESC ( B`), or a string-sequence
142030
+ * opener the cut landed inside. The head then skips exactly the bytes a
142031
+ * terminal would consume for that sequence, so no visible text is dropped —
142032
+ * but only while that skip stays within `MAX_TAIL_DROP_SHARE` of the tail.
142033
+ * A CSI body or escape-intermediate run can be arbitrarily long, so past
142034
+ * the bound the head keeps the newest bytes and the fragment the raw cut
142035
+ * left with them — or one unit earlier, onto an introducer ESC sitting
142036
+ * immediately before the head, so the whole sequence survives and is
142037
+ * stripped (`withinDropShare`) — but only when the skip proved the sequence
142038
+ * closed: an unclosed run keeps the raw cut, because with no terminator to
142039
+ * hand the sanitizer the ESC would surface bare and swallow what follows
142040
+ * it. So does the head for a string that never ends or ended ahead of the
142041
+ * cut. A sliver of the tail is worse than a fragment.
142042
+ *
142043
+ * `cut` is always a code-point boundary here (the byte loop above works in
142044
+ * code points), so unlike its manager-side twin there is no surrogate half to
142045
+ * drop, and `stringPayloadHead`'s clamp steps back onto a code point as well;
142046
+ * the one-unit look-back onto an introducer ESC (`withinDropShare`) — taken
142047
+ * only for a skip that proved its sequence closed — is a code point boundary
142048
+ * too. Mirrors `safeTailStart` in
142049
+ * `packages/agent-core/src/tools/background/manager.ts` — app code cannot
142050
+ * import agent-core directly, so the rule lives in both places.
142051
+ */
142052
+ function safeTailStart(text, cut) {
142053
+ if (cut <= 0 || cut >= text.length) return cut;
142054
+ const tail = text.length - cut;
142055
+ let start = cut;
142056
+ if (text[start - 1] !== "\n") {
142057
+ const newline = text.indexOf("\n", start);
142058
+ if (newline !== -1 && newline + 1 - cut <= tail * MAX_TAIL_DROP_SHARE) start = newline + 1;
142059
+ }
142060
+ return skipOpenEscape(text, start);
142061
+ }
142062
+ /** CSI parameter (0x30–0x3F) / intermediate (0x20–0x2F) bytes — everything before the final byte. */
142063
+ function isCsiBody(code) {
142064
+ return code !== void 0 && code >= 32 && code <= 63;
142065
+ }
142066
+ /** CSI final byte (0x40–0x7E) — the sequence ends here. */
142067
+ function isCsiFinal(code) {
142068
+ return code !== void 0 && code >= 64 && code <= 126;
142069
+ }
142070
+ /** OSC / DCS / PM / APC introducers — string sequences ended by BEL or ST. */
142071
+ function isStringIntro(intro) {
142072
+ return intro === "]" || intro === "P" || intro === "^" || intro === "_";
142073
+ }
142074
+ /** ESC-intermediate byte (0x20–0x2F) of a two-byte escape sequence. */
142075
+ function isEscapeIntermediate(code) {
142076
+ return code !== void 0 && code >= 32 && code <= 47;
142077
+ }
142078
+ /** Final byte (0x30–0x7E) that closes a two-byte escape sequence. */
142079
+ function isEscapeFinal(code) {
142080
+ return code !== void 0 && code >= 48 && code <= 126;
142081
+ }
142082
+ /** True when every code point in the half-open range `[from, to)` is an ESC intermediate (0x20–0x2F). */
142083
+ function allEscapeIntermediates(text, from, to) {
142084
+ for (let i = from; i < to; i += 1) if (!isEscapeIntermediate(text.codePointAt(i))) return false;
142085
+ return true;
142086
+ }
142087
+ /**
142088
+ * When the dropped prefix proves an escape sequence is still open at `cut`,
142089
+ * return the index after its remainder; otherwise return `cut` unchanged.
142090
+ */
142091
+ function skipOpenEscape(text, cut) {
142092
+ let run = cut;
142093
+ while (run > 0 && isCsiBody(text.codePointAt(run - 1))) run -= 1;
142094
+ if (run >= 2 && text[run - 1] === "[" && text[run - 2] === ESC) {
142095
+ const skip = skipCsiRest(text, cut);
142096
+ return withinDropShare(text, cut, skip.end, skip.closed);
142097
+ }
142098
+ if (text[run - 1] === ESC && allEscapeIntermediates(text, run, cut)) return skipEscapeRest(text, cut);
142099
+ const open = text.lastIndexOf(ESC, cut - 1);
142100
+ if (open !== -1 && isStringIntro(text[open + 1])) return stringSequenceHead(text, open, cut);
142101
+ return cut;
142102
+ }
142103
+ /**
142104
+ * Keep a provable skip only while it stays within `MAX_TAIL_DROP_SHARE` of the
142105
+ * tail that starts at `from`: the skipped bytes are exactly what the reader
142106
+ * loses from the tail it asked for. A CSI body or escape-intermediate run can
142107
+ * be arbitrarily long, so past the bound the raw cut is worth more than the
142108
+ * clean head — except when the sequence's introducer ESC sits immediately
142109
+ * before the head (`…\u001b|[31m…`) *and* `closed` says the skip found the
142110
+ * sequence's terminator, where one unit of look-back keeps the sequence whole
142111
+ * instead: the sanitizer strips it and the visible text is the skip's, for
142112
+ * that one unit rather than a sliver of the tail. An unclosed run must keep
142113
+ * the raw cut even with that ESC one unit ahead: nothing terminates it, so
142114
+ * look-back would hand the reader a bare ESC whose sequence run a terminal
142115
+ * swallows along with the bytes behind it.
142116
+ */
142117
+ function withinDropShare(text, from, target, closed) {
142118
+ if (target - from <= (text.length - from) * MAX_TAIL_DROP_SHARE) return target;
142119
+ return closed && from > 0 && text[from - 1] === ESC ? from - 1 : from;
142120
+ }
142121
+ /**
142122
+ * The rest of a CSI sequence whose body was torn at `cut`, with the closure
142123
+ * its skip proved: `closed` holds only when a final byte (0x40–0x7E) was
142124
+ * actually found, so a parameter run that reaches the end of `text` — or stops
142125
+ * on a byte no CSI can end with — reports the sequence as open.
142126
+ */
142127
+ function skipCsiRest(text, cut) {
142128
+ let end = cut;
142129
+ while (end < text.length && isCsiBody(text.codePointAt(end))) end += 1;
142130
+ const closed = end < text.length && isCsiFinal(text.codePointAt(end));
142131
+ return {
142132
+ end: closed ? end + 1 : end,
142133
+ closed
142134
+ };
142135
+ }
142136
+ /** Index after the escape sequence whose introducer ESC sits at `cut - 1`. */
142137
+ function skipEscapeRest(text, cut) {
142138
+ const intro = text[cut];
142139
+ if (intro === "[") {
142140
+ const skip = skipCsiRest(text, cut + 1);
142141
+ return withinDropShare(text, cut, skip.end, skip.closed);
142142
+ }
142143
+ if (isStringIntro(intro)) return stringSequenceHead(text, cut - 1, cut + 1);
142144
+ let end = cut;
142145
+ while (end < text.length && isEscapeIntermediate(text.codePointAt(end))) end += 1;
142146
+ const closed = end < text.length && isEscapeFinal(text.codePointAt(end));
142147
+ return withinDropShare(text, cut, closed ? end + 1 : end, closed);
142148
+ }
142149
+ /**
142150
+ * Head index for a cut that lands inside the string sequence opened at `open`.
142151
+ *
142152
+ * `stringEnd` gives the byte a terminal stops at; skipping to it hides the
142153
+ * payload's remainder, which is what a terminal shows — but only while that
142154
+ * costs at most `MAX_TAIL_DROP_SHARE` of the tail. A long OSC 8 URL, or any
142155
+ * payload longer than the budget, would otherwise hand the reader a sliver of
142156
+ * the tail it asked for; a payload whose extent exceeds the budget stays
142157
+ * visible instead. A cut the string already ended ahead of needs no skip, and
142158
+ * an unterminated one must not swallow the tail (`stringPayloadHead`).
142159
+ */
142160
+ function stringSequenceHead(text, open, cut) {
142161
+ const drop = stringEnd(text, open + 2) - cut;
142162
+ if (drop >= 0 && drop <= (text.length - cut) * MAX_TAIL_DROP_SHARE) return cut + drop;
142163
+ return stringPayloadHead(text, open, cut);
142164
+ }
142165
+ /**
142166
+ * Head index that keeps a string sequence's payload visible, moving the cut
142167
+ * only past the opener's own bytes when the cut landed inside them.
142168
+ *
142169
+ * A terminal swallows an unterminated payload, but a viewer must never render
142170
+ * a blank body: a shell card whose output opens with an unterminated OSC — a
142171
+ * command killed mid-title, a truncated write — would otherwise show nothing
142172
+ * at all. The head stays at `cut` unless the cut landed inside the opener
142173
+ * (`\u001b]0;` would leave `0;` in front of the payload), and never reaches
142174
+ * `text.length` — the clamp to the last unit steps back onto the pair's first
142175
+ * half when that unit is a trailing surrogate pair's low half (defensive here:
142176
+ * `truncateTailBytes` cuts on a code point boundary, so the clamp cannot land
142177
+ * between a pair's halves in practice) — so this rule never empties the tail;
142178
+ * the tail comes back empty only when the byte budget fits none of the last
142179
+ * character.
142180
+ */
142181
+ function stringPayloadHead(text, open, cut) {
142182
+ const head = Math.min(Math.max(cut, stringPayloadStart(text, open)), text.length - 1);
142183
+ const code = text.codePointAt(head);
142184
+ return head > 0 && code !== void 0 && code >= 56320 && code <= 57343 ? head - 1 : head;
142185
+ }
142186
+ /**
142187
+ * Index after the byte that ends the string sequence whose payload starts at
142188
+ * `from` — its BEL or `ESC \` terminator, or a CAN (0x18) / SUB (0x1A), which
142189
+ * a terminal reads as "string over, resume normal parsing". `-1` when the
142190
+ * string never ends.
142191
+ */
142192
+ function stringEnd(text, from) {
142193
+ for (let i = from; i < text.length; i += 1) {
142194
+ const code = text.codePointAt(i);
142195
+ if (code === 7) return i + 1;
142196
+ if (code === 27 && text[i + 1] === "\\") return i + 2;
142197
+ if (code === 24 || code === 26) return i + 1;
142198
+ }
142199
+ return -1;
142200
+ }
142201
+ /**
142202
+ * Index where the payload of the string sequence opened at `open` starts: for
142203
+ * OSC after `ESC ] Ps ;`, for DCS/PM/APC after their parameter and
142204
+ * intermediate bytes and the final byte that opens the payload.
142205
+ */
142206
+ function stringPayloadStart(text, open) {
142207
+ let i = open + 2;
142208
+ if (text[open + 1] === "]") {
142209
+ while (i < text.length && isOscParam(text.codePointAt(i))) i += 1;
142210
+ if (text[i] === ";") i += 1;
142211
+ return i;
140312
142212
  }
140313
- return chars.slice(start).join("");
142213
+ while (i < text.length && isCsiBody(text.codePointAt(i))) i += 1;
142214
+ const final = text.codePointAt(i);
142215
+ return i < text.length && isCsiFinal(final) ? i + 1 : i;
142216
+ }
142217
+ /** OSC numeric parameter byte (0x30–0x39) — the `Ps` before the `;` separator. */
142218
+ function isOscParam(code) {
142219
+ return code !== void 0 && code >= 48 && code <= 57;
140314
142220
  }
140315
142221
  /**
140316
142222
  * Component that renders tool output with wrap-aware line truncation.
@@ -142535,6 +144441,7 @@ async function handleRevokeCommand(host, args = "") {
142535
144441
  const preservedEntries = entries.slice(lastUserIndex).filter((entry) => !isRevokeContextEntry(entry));
142536
144442
  entries.splice(lastUserIndex, entries.length - lastUserIndex, ...preservedEntries);
142537
144443
  if (entries.length === 0) renderWelcome(host);
144444
+ host.state.ui.resetTextSelection();
142538
144445
  host.state.ui.requestRender();
142539
144446
  }
142540
144447
  function parseRevokeCount(args) {
@@ -145219,12 +147126,14 @@ function showValuePicker(host, key) {
145219
147126
  onSelect: (value) => {
145220
147127
  const parsed = Number(value);
145221
147128
  if (!Number.isFinite(parsed)) return;
147129
+ const resized = parsed !== getActivityLineValue(key);
145222
147130
  setActivityLineValue(key, parsed);
145223
147131
  host.restoreEditor();
145224
147132
  host.showStatus(t("blockrows.saved", {
145225
147133
  label: t(setting.label),
145226
147134
  value: String(parsed)
145227
147135
  }), host.state.theme.colors.success);
147136
+ if (resized) host.state.ui.resetTextSelection();
145228
147137
  repaintTranscript(host);
145229
147138
  },
145230
147139
  onCancel: () => {
@@ -145807,8 +147716,10 @@ function buildOptions() {
145807
147716
  }
145808
147717
  /** Apply a choice: redraw what is already on screen, restate the capability. */
145809
147718
  async function applyDisplay(host, value) {
147719
+ const previous = getMermaidDisplay();
145810
147720
  setMermaidDisplay(value);
145811
147721
  host.showStatus(t("mermaid.applied", { mode: optionLabel(value) }), value === "off" ? host.state.theme.colors.textDim : host.state.theme.colors.success);
147722
+ if (previous !== value && (previous === "off" || value === "off")) host.state.ui.resetTextSelection();
145812
147723
  repaintTranscript(host);
145813
147724
  await syncDiagramCapabilityPrompt(host.session);
145814
147725
  }
@@ -148880,4 +150791,4 @@ async function handleBuiltInSlashCommand(host, name, args) {
148880
150791
  }
148881
150792
  }
148882
150793
  //#endregion
148883
- export { handleExportDebugZipCommand as $, encodeWorkDirKey as $n, lerpGradient as $t, highlightLines as A, embeddingFailureDetail as An, isTurnElapsedEnabled as At, ActivityGroupComponent as B, detectShellEnvironment as Bn, ENABLE_TERMINAL_THEME_REPORTING as Bt, handleCcCommand as C, getLlmNotSetMessage as Cn, clearInfoPanelState as Ct, ToolCallComponent as D, sortSlashCommands as Dn, createEditorTheme as Dt, UserMessageComponent as E, BUILTIN_SLASH_COMMANDS as En, resolveThemeSync as Et, StatusMessageComponent as F, PULSE_WAVE_FRAMES as Fn, detectTerminalTheme as Ft, AgentGroupComponent as G, CLI_COMMAND_NAME as Gn, QUERY_TERMINAL_THEME as Gt, estimateTokens as H, getInputHistoryFile as Hn, OSC11_RESPONSE as Ht, replaceTabs as I, TuiConfigParseError as In, parseOsc11BackgroundTheme as It, getBreathingFrame as J, PRODUCT_NAME as Jn, TERMINAL_THEME_DARK as Jt, WelcomeComponent as K, CLI_UI_MODE as Kn, TERMINAL_FOCUS_IN as Kt, sanitizeShellOutput as L, TuiLikePreferencesSchema as Ln, DISABLE_TERMINAL_FOCUS_REPORTING as Lt, CachedContainer as M, hasEmbeddingModelCache as Mn, createThemeStyles as Mt, ThinkingComponent as N, startManualEmbeddingDownload as Nn, contrastTextHex as Nt, renderDiffLines as O, isExperimentalFlagEnabled as On, createMarkdownTheme as Ot, NoticeMessageComponent as P, PIXEL_PULSE_FRAMES as Pn, getColorPalette as Pt, refineGoal as Q, ScreamHarness as Qn, FooterComponent as Qt, SkillActivationComponent as R, loadTuiConfig as Rn, DISABLE_TERMINAL_THEME_REPORTING as Rt, readJsonlFile as S, getCtrlDHint as Sn, supportsBalance as St, handleRevokeCommand as T, buildSkillSlashCommands as Tn, showUsage as Tt, getSharedSpeedTracker as U, getLogDir as Un, OSC11_RESPONSE_PREFIX as Ut, charsForTokenBudget as V, getDataDir as Vn, OSC11_QUERY as Vt, BackgroundAgentStatusComponent as W, detectInstallSource as Wn, OSC11_RESPONSE_PREFIX_NO_ESC as Wt, CompactionComponent as X, fetchCatalog as Xn, isBusy as Xt, resetBreathingClock as Y, DEFAULT_CATALOG_URL as Yn, TERMINAL_THEME_LIGHT as Yt, clearGoalState as Z, saveCatalogCache as Zn, isStreaming as Zt, handleUpdateCommand as _, EXIT_CONFIRM_WINDOW_MS as _n, handleYoloCommand as _t, syncDiagramCapabilityPrompt as a, printableChar as an, isRealUserPrompt as ar, toTerminalHyperlink as at, readUpdateCache as b, TIP_ROTATION_INTERVAL_MS as bn, showSettingsSelector as bt, disposeChildren as c, appendStreamingArgsPreview as cn, log as cr, handleAutoCommand as ct, isPlanExpandable as d, isTodoItemShape as dn, isOrphanedToolCallError as dr, handleEditorCommand as dt, SubagentSlots as en, normalizeWorkDir as er, handleExportMdCommand as et, MoonLoader as f, parseStreamingArgs as fn, ErrorCodes as fr, handleFusionPlanCommand as ft, handleMcpCommand as g, EMPTY_SESSION_HINT_URL as gn, handleWolfpackCommand as gt, handleChannelCommand as h, truncateErrorMessage as hn, handleThemeCommand as ht, buildRoleAdditionalText as i, handleLogoutCommand as in, MCP_OAUTH_AUTHORIZATION_URL_TOOL_UPDATE as ir, isInternalMessage as it, langFromPath as j, getKnowledgeStore as jn, toggleEmptySessionHint as jt, renderDiffLinesClustered as k, setExperimentalFlags as kn, isEmptySessionHintDismissed as kt, hasDispose as l, argsRecord as ln, resolveGlobalLogPath as lr, handleBotCommand as lt, handleMemoryCommand as m, stringValue as mn, handlePlanCommand as mt, clearEvalPanelState as n, handleSearchCommand as nn, readSessionIndex as nr, handleInitCommand as nt, handleExtensionCommand as o, STATUS_BULLET as on, resolveScreamHome as or, changeThinkingLevel as ot, formatMemoryMemoForInjection as p, serializeToolResultOutput as pn, SCREAM_ERROR_INFO as pr, handleModelCommand as pt, BREATHE_CYCLE_MS as q, CLI_USER_AGENT_PRODUCT as qn, TERMINAL_FOCUS_OUT as qt, openUrl as r, handleConnectCommand as rn, removeSessionIndexEntry as rr, handleTitleCommand as rt, handleSkillCommand as s, appendSessionHeaderHint as sn, flushDiagnosticLogs as sr, getModelCycleLevel as st, dispatchInput as t, handleTraceCommand as tn, appendSessionIndexEntry as tr, handleForkCommand as tt, isExpandable as u, formatErrorMessage as un, isScreamError as ur, handleCompactCommand as ut, selectUpdateTarget as v, MAIN_AGENT_ID$1 as vn, showModelPicker as vt, getDaemonInstructions as w, getNoActiveSessionMessage as wn, showStatusReport as wt, appendJsonlLine as x, getCtrlCHint as xn, refreshProviderBalance as xt, refreshUpdateCache as y, SESSION_TIPS as yn, showPermissionPicker as yt, AssistantMessageComponent as z, saveTuiConfig as zn, ENABLE_TERMINAL_FOCUS_REPORTING as zt };
150794
+ export { handleExportDebugZipCommand as $, encodeWorkDirKey as $n, lerpGradient as $t, highlightLines as A, embeddingFailureDetail as An, isTurnElapsedEnabled as At, ActivityGroupComponent as B, detectShellEnvironment as Bn, ENABLE_TERMINAL_THEME_REPORTING as Bt, handleCcCommand as C, getLlmNotSetMessage as Cn, clearInfoPanelState as Ct, ToolCallComponent as D, sortSlashCommands as Dn, createEditorTheme as Dt, UserMessageComponent as E, BUILTIN_SLASH_COMMANDS as En, resolveThemeSync as Et, StatusMessageComponent as F, PULSE_WAVE_FRAMES as Fn, detectTerminalTheme as Ft, AgentGroupComponent as G, CLI_COMMAND_NAME as Gn, QUERY_TERMINAL_THEME as Gt, estimateTokens as H, getInputHistoryFile as Hn, OSC11_RESPONSE as Ht, replaceTabs as I, TuiConfigParseError as In, parseOsc11BackgroundTheme as It, getBreathingFrame as J, PRODUCT_NAME as Jn, TERMINAL_THEME_DARK as Jt, WelcomeComponent as K, CLI_UI_MODE as Kn, TERMINAL_FOCUS_IN as Kt, sanitizeShellOutput as L, TuiLikePreferencesSchema as Ln, DISABLE_TERMINAL_FOCUS_REPORTING as Lt, CachedContainer as M, hasEmbeddingModelCache as Mn, createThemeStyles as Mt, ThinkingComponent as N, startManualEmbeddingDownload as Nn, contrastTextHex as Nt, renderDiffLines as O, isExperimentalFlagEnabled as On, createMarkdownTheme as Ot, NoticeMessageComponent as P, PIXEL_PULSE_FRAMES as Pn, getColorPalette as Pt, refineGoal as Q, ScreamHarness as Qn, FooterComponent as Qt, SkillActivationComponent as R, loadTuiConfig as Rn, DISABLE_TERMINAL_THEME_REPORTING as Rt, readJsonlFile as S, getCtrlDHint as Sn, supportsBalance as St, handleRevokeCommand as T, buildSkillSlashCommands as Tn, showUsage as Tt, getSharedSpeedTracker as U, getLogDir as Un, OSC11_RESPONSE_PREFIX as Ut, charsForTokenBudget as V, getDataDir as Vn, OSC11_QUERY as Vt, BackgroundAgentStatusComponent as W, detectInstallSource as Wn, OSC11_RESPONSE_PREFIX_NO_ESC as Wt, CompactionComponent as X, fetchCatalog as Xn, isBusy as Xt, resetBreathingClock as Y, DEFAULT_CATALOG_URL as Yn, TERMINAL_THEME_LIGHT as Yt, clearGoalState as Z, saveCatalogCache as Zn, isStreaming as Zt, handleUpdateCommand as _, EXIT_CONFIRM_WINDOW_MS as _n, handleYoloCommand as _t, syncDiagramCapabilityPrompt as a, printableChar as an, isRealUserPrompt as ar, toTerminalHyperlink as at, readUpdateCache as b, TIP_ROTATION_INTERVAL_MS as bn, showSettingsSelector as bt, disposeChildren as c, appendStreamingArgsPreview as cn, flushDiagnosticLogs as cr, handleAutoCommand as ct, isPlanExpandable as d, isTodoItemShape as dn, isScreamError as dr, handleEditorCommand as dt, SubagentSlots as en, normalizeWorkDir as er, handleExportMdCommand as et, MoonLoader as f, parseStreamingArgs as fn, isOrphanedToolCallError as fr, handleFusionPlanCommand as ft, handleMcpCommand as g, EMPTY_SESSION_HINT_URL as gn, handleWolfpackCommand as gt, handleChannelCommand as h, truncateErrorMessage as hn, handleThemeCommand as ht, buildRoleAdditionalText as i, handleLogoutCommand as in, MCP_OAUTH_AUTHORIZATION_URL_TOOL_UPDATE as ir, isInternalMessage as it, langFromPath as j, getKnowledgeStore as jn, toggleEmptySessionHint as jt, renderDiffLinesClustered as k, setExperimentalFlags as kn, isEmptySessionHintDismissed as kt, hasDispose as l, argsRecord as ln, log as lr, handleBotCommand as lt, handleMemoryCommand as m, stringValue as mn, SCREAM_ERROR_INFO as mr, handlePlanCommand as mt, clearEvalPanelState as n, handleSearchCommand as nn, readSessionIndex as nr, handleInitCommand as nt, handleExtensionCommand as o, STATUS_BULLET as on, parseRlmMaxDepthArg as or, changeThinkingLevel as ot, formatMemoryMemoForInjection as p, serializeToolResultOutput as pn, ErrorCodes as pr, handleModelCommand as pt, BREATHE_CYCLE_MS as q, CLI_USER_AGENT_PRODUCT as qn, TERMINAL_FOCUS_OUT as qt, openUrl as r, handleConnectCommand as rn, removeSessionIndexEntry as rr, handleTitleCommand as rt, handleSkillCommand as s, appendSessionHeaderHint as sn, resolveScreamHome as sr, getModelCycleLevel as st, dispatchInput as t, handleTraceCommand as tn, appendSessionIndexEntry as tr, handleForkCommand as tt, isExpandable as u, formatErrorMessage as un, resolveGlobalLogPath as ur, handleCompactCommand as ut, selectUpdateTarget as v, MAIN_AGENT_ID$1 as vn, showModelPicker as vt, getDaemonInstructions as w, getNoActiveSessionMessage as wn, showStatusReport as wt, appendJsonlLine as x, getCtrlCHint as xn, refreshProviderBalance as xt, refreshUpdateCache as y, SESSION_TIPS as yn, showPermissionPicker as yt, AssistantMessageComponent as z, saveTuiConfig as zn, ENABLE_TERMINAL_FOCUS_REPORTING as zt };