scream-code 0.17.5 → 0.17.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -56,6 +56,7 @@ When Scream wants to modify files or run commands, an approval panel pops up —
56
56
  - **Persistent Memory** — structured pain-point memory with FTS5 full-text + tag + vector retrieval, shared across sessions.
57
57
  - **Local Knowledge Graph** — SAG-based visual knowledge base for multi-hop reasoning; import your own docs anytime.
58
58
  - **RLM Mode** — persistent Python workspace for long-running tasks, with unlimited recursive sub-agents.
59
+ - **Script Mode** — on by default: the model can run JavaScript in a sandbox that calls the other tools for it, so batch work (dozens of files/pages, filter-then-report) finishes in one round trip — only the script's summary enters the conversation; `/script` toggles it.
59
60
  - **Session Trace** — `/trace` exports any session as a self-contained, offline interactive HTML timeline.
60
61
  - **Context Search** — `/search` (Ctrl+Shift+F) full-screen keyword search over the conversation.
61
62
  - **High Cache Hit Rate** — per-session HitR in the status bar, tuned across compatible protocols to maximize cache hits without compromising quality.
@@ -65,7 +66,7 @@ When Scream wants to modify files or run commands, an approval panel pops up —
65
66
 
66
67
  ### Slash Commands
67
68
 
68
- Type `/` in the input to browse. All 54 commands:
69
+ Type `/` in the input to browse. All 55 commands:
69
70
 
70
71
  <details>
71
72
  <summary>Full command reference (click to expand)</summary>
@@ -81,6 +82,7 @@ Type `/` in the input to browse. All 54 commands:
81
82
  | `/wolfpack` (`/wp`) | Toggle wolfpack mode — auto-approve + batch concurrency |
82
83
  | `/rlm` | RLM mode: persistent Python workspace, unlimited recursive subagents |
83
84
  | `/rlm-max-depth [N]` | Set RLM recursion depth limit |
85
+ | `/script` | Script mode: run JavaScript in a sandbox that batches tool calls (on by default) |
84
86
  | `/plan` | Toggle plan mode |
85
87
  | `/fusionplan` (`/fp`) | Fusion plan mode (multi-agent parallel planning) |
86
88
  | `/btw` | Quick question without interrupting the conversation |
@@ -5,8 +5,8 @@ const __filename = __cjsShimFileURLToPath(import.meta.url);
5
5
  const __dirname = __cjsShimDirname(__filename);
6
6
  import "./suppress-sqlite-warning-C2VB0doZ.mjs";
7
7
  import { k as MemoryMemoStore } from "./src-BhZujafw.mjs";
8
- import { $n as encodeWorkDirKey, $t as lerpGradient, A as highlightLines, An as embeddingFailureDetail, At as isTurnElapsedEnabled, B as ActivityGroupComponent, Bn as detectShellEnvironment, Bt as ENABLE_TERMINAL_THEME_REPORTING, Cn as getLlmNotSetMessage, Ct as clearInfoPanelState, D as ToolCallComponent, Dn as sortSlashCommands, Dt as createEditorTheme, E as UserMessageComponent, En as BUILTIN_SLASH_COMMANDS, Et as resolveThemeSync, F as StatusMessageComponent, Fn as PULSE_WAVE_FRAMES, Ft as detectTerminalTheme, G as AgentGroupComponent, Gn as CLI_COMMAND_NAME, Gt as QUERY_TERMINAL_THEME, H as estimateTokens, Hn as getInputHistoryFile, Ht as OSC11_RESPONSE, I as replaceTabs, In as TuiConfigParseError, It as parseOsc11BackgroundTheme, J as getBreathingFrame, Jn as PRODUCT_NAME, Jt as TERMINAL_THEME_DARK, K as WelcomeComponent, Kn as CLI_UI_MODE, Kt as TERMINAL_FOCUS_IN, L as sanitizeShellOutput, Ln as TuiLikePreferencesSchema, Lt as DISABLE_TERMINAL_FOCUS_REPORTING, M as CachedContainer, Mn as hasEmbeddingModelCache, Mt as createThemeStyles, N as ThinkingComponent, Nn as startManualEmbeddingDownload, Nt as contrastTextHex, O as renderDiffLines, On as isExperimentalFlagEnabled, Ot as createMarkdownTheme, P as NoticeMessageComponent, Pn as PIXEL_PULSE_FRAMES, Pt as getColorPalette, Q as refineGoal, Qn as ScreamHarness, Qt as FooterComponent, R as SkillActivationComponent, Rn as loadTuiConfig, Rt as DISABLE_TERMINAL_THEME_REPORTING, S as readJsonlFile, Sn as getCtrlDHint, St as supportsBalance, Tn as buildSkillSlashCommands, U as getSharedSpeedTracker, Un as getLogDir, Ut as OSC11_RESPONSE_PREFIX, V as charsForTokenBudget, Vn as getDataDir, Vt as OSC11_QUERY, W as BackgroundAgentStatusComponent, Wn as detectInstallSource, Wt as OSC11_RESPONSE_PREFIX_NO_ESC, X as CompactionComponent, Xn as fetchCatalog, Xt as isBusy, Y as resetBreathingClock, Yn as DEFAULT_CATALOG_URL, Yt as TERMINAL_THEME_LIGHT, Z as clearGoalState, Zn as saveCatalogCache, Zt as isStreaming, _n as EXIT_CONFIRM_WINDOW_MS, a as syncDiagramCapabilityPrompt, an as printableChar, ar as isRealUserPrompt, at as toTerminalHyperlink, b as readUpdateCache, bn as TIP_ROTATION_INTERVAL_MS, c as disposeChildren, cn as appendStreamingArgsPreview, cr as log, d as isPlanExpandable, dn as isTodoItemShape, dr as isOrphanedToolCallError, en as SubagentSlots, er as normalizeWorkDir, f as MoonLoader, fn as parseStreamingArgs, fr as ErrorCodes, ft as handleFusionPlanCommand, gn as EMPTY_SESSION_HINT_URL, hn as truncateErrorMessage, i as buildRoleAdditionalText, it as isInternalMessage, j as langFromPath, jn as getKnowledgeStore, jt as toggleEmptySessionHint, k as renderDiffLinesClustered, kn as setExperimentalFlags, kt as isEmptySessionHintDismissed, l as hasDispose, ln as argsRecord, lr as resolveGlobalLogPath, mn as stringValue, mt as handlePlanCommand, n as clearEvalPanelState, nr as readSessionIndex, on as STATUS_BULLET, or as resolveScreamHome, ot as changeThinkingLevel, p as formatMemoryMemoForInjection, pn as serializeToolResultOutput, pr as SCREAM_ERROR_INFO, q as BREATHE_CYCLE_MS, qn as CLI_USER_AGENT_PRODUCT, qt as TERMINAL_FOCUS_OUT, r as openUrl, rr as removeSessionIndexEntry, sn as appendSessionHeaderHint, sr as flushDiagnosticLogs, st as getModelCycleLevel, t as dispatchInput, tr as appendSessionIndexEntry, u as isExpandable, un as formatErrorMessage, ur as isScreamError, v as selectUpdateTarget, vn as MAIN_AGENT_ID, w as getDaemonInstructions, wn as getNoActiveSessionMessage, x as appendJsonlLine, xn as getCtrlCHint, xt as refreshProviderBalance, y as refreshUpdateCache, yn as SESSION_TIPS, z as AssistantMessageComponent, zn as saveTuiConfig, zt as ENABLE_TERMINAL_FOCUS_REPORTING } from "./dispatch-DCzx4R71.mjs";
9
- import { a as setLocale, i as getLocale, o as t } from "./text-input-dialog-BoxTq4a3.mjs";
8
+ import { $n as encodeWorkDirKey, $t as lerpGradient, A as highlightLines, An as embeddingFailureDetail, At as isTurnElapsedEnabled, B as ActivityGroupComponent, Bn as detectShellEnvironment, Bt as ENABLE_TERMINAL_THEME_REPORTING, Cn as getLlmNotSetMessage, Ct as clearInfoPanelState, D as ToolCallComponent, Dn as sortSlashCommands, Dt as createEditorTheme, E as UserMessageComponent, En as BUILTIN_SLASH_COMMANDS, Et as resolveThemeSync, F as StatusMessageComponent, Fn as PULSE_WAVE_FRAMES, Ft as detectTerminalTheme, G as AgentGroupComponent, Gn as CLI_COMMAND_NAME, Gt as QUERY_TERMINAL_THEME, H as estimateTokens, Hn as getInputHistoryFile, Ht as OSC11_RESPONSE, I as replaceTabs, In as TuiConfigParseError, It as parseOsc11BackgroundTheme, J as getBreathingFrame, Jn as PRODUCT_NAME, Jt as TERMINAL_THEME_DARK, K as WelcomeComponent, Kn as CLI_UI_MODE, Kt as TERMINAL_FOCUS_IN, L as sanitizeShellOutput, Ln as TuiLikePreferencesSchema, Lt as DISABLE_TERMINAL_FOCUS_REPORTING, M as CachedContainer, Mn as hasEmbeddingModelCache, Mt as createThemeStyles, N as ThinkingComponent, Nn as startManualEmbeddingDownload, Nt as contrastTextHex, O as renderDiffLines, On as isExperimentalFlagEnabled, Ot as createMarkdownTheme, P as NoticeMessageComponent, Pn as PIXEL_PULSE_FRAMES, Pt as getColorPalette, Q as refineGoal, Qn as ScreamHarness, Qt as FooterComponent, R as SkillActivationComponent, Rn as loadTuiConfig, Rt as DISABLE_TERMINAL_THEME_REPORTING, S as readJsonlFile, Sn as getCtrlDHint, St as supportsBalance, Tn as buildSkillSlashCommands, U as getSharedSpeedTracker, Un as getLogDir, Ut as OSC11_RESPONSE_PREFIX, V as charsForTokenBudget, Vn as getDataDir, Vt as OSC11_QUERY, W as BackgroundAgentStatusComponent, Wn as detectInstallSource, Wt as OSC11_RESPONSE_PREFIX_NO_ESC, X as CompactionComponent, Xn as fetchCatalog, Xt as isBusy, Y as resetBreathingClock, Yn as DEFAULT_CATALOG_URL, Yt as TERMINAL_THEME_LIGHT, Z as clearGoalState, Zn as saveCatalogCache, Zt as isStreaming, _n as EXIT_CONFIRM_WINDOW_MS, a as syncDiagramCapabilityPrompt, an as printableChar, ar as isRealUserPrompt, at as toTerminalHyperlink, b as readUpdateCache, bn as TIP_ROTATION_INTERVAL_MS, c as disposeChildren, cn as appendStreamingArgsPreview, cr as log, d as isPlanExpandable, dn as isTodoItemShape, dr as isOrphanedToolCallError, en as SubagentSlots, er as normalizeWorkDir, f as MoonLoader, fn as parseStreamingArgs, fr as ErrorCodes, ft as handleFusionPlanCommand, gn as EMPTY_SESSION_HINT_URL, hn as truncateErrorMessage, i as buildRoleAdditionalText, it as isInternalMessage, j as langFromPath, jn as getKnowledgeStore, jt as toggleEmptySessionHint, k as renderDiffLinesClustered, kn as setExperimentalFlags, kt as isEmptySessionHintDismissed, l as hasDispose, ln as argsRecord, lr as resolveGlobalLogPath, mn as stringValue, mt as handlePlanCommand, n as clearEvalPanelState, nr as readSessionIndex, on as STATUS_BULLET, or as resolveScreamHome, ot as changeThinkingLevel, p as formatMemoryMemoForInjection, pn as serializeToolResultOutput, pr as SCREAM_ERROR_INFO, q as BREATHE_CYCLE_MS, qn as CLI_USER_AGENT_PRODUCT, qt as TERMINAL_FOCUS_OUT, r as openUrl, rr as removeSessionIndexEntry, sn as appendSessionHeaderHint, sr as flushDiagnosticLogs, st as getModelCycleLevel, t as dispatchInput, tr as appendSessionIndexEntry, u as isExpandable, un as formatErrorMessage, ur as isScreamError, v as selectUpdateTarget, vn as MAIN_AGENT_ID, w as getDaemonInstructions, wn as getNoActiveSessionMessage, x as appendJsonlLine, xn as getCtrlCHint, xt as refreshProviderBalance, y as refreshUpdateCache, yn as SESSION_TIPS, z as AssistantMessageComponent, zn as saveTuiConfig, zt as ENABLE_TERMINAL_FOCUS_REPORTING } from "./dispatch-DFFuylzQ.mjs";
9
+ import { a as setLocale, i as getLocale, o as t } from "./text-input-dialog-CD0sakGq.mjs";
10
10
  import { createRequire } from "node:module";
11
11
  import { randomBytes, randomUUID, scryptSync, timingSafeEqual } from "node:crypto";
12
12
  import { access, appendFile, chmod, mkdir, mkdtemp, readFile, readdir, realpath, rename, rm, stat, unlink, writeFile } from "node:fs/promises";
@@ -33,7 +33,7 @@ function optionalBuildString(value) {
33
33
  return typeof value === "string" && value.length > 0 ? value : void 0;
34
34
  }
35
35
  const SCREAM_BUILD_INFO = {
36
- version: optionalBuildString("0.17.5"),
36
+ version: optionalBuildString("0.17.6"),
37
37
  channel: optionalBuildString(""),
38
38
  commit: optionalBuildString(""),
39
39
  buildTarget: optionalBuildString("darwin-arm64")
@@ -14142,7 +14142,7 @@ var ScreamTUI = class {
14142
14142
  const card = new OnboardingCardComponent({
14143
14143
  colors: this.state.theme.colors,
14144
14144
  onConfigure: () => {
14145
- import("./dispatch-DjLS5cab.mjs").then(({ handleConnectCommand }) => handleConnectCommand(this, "")).finally(() => {
14145
+ import("./dispatch-DRdBQorE.mjs").then(({ handleConnectCommand }) => handleConnectCommand(this, "")).finally(() => {
14146
14146
  this.restoreEditor();
14147
14147
  onDone?.();
14148
14148
  });
@@ -6,7 +6,7 @@ const __dirname = __cjsShimDirname(__filename);
6
6
  import { i as __require, o as __toESM, r as __exportAll, t as __commonJSMin } from "./chunk-D90kvbyJ.mjs";
7
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";
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-BoxTq4a3.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";
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";
@@ -38,6 +38,7 @@ import { StringDecoder } from "node:string_decoder";
38
38
  import Pi from "assert";
39
39
  import ro from "node:assert";
40
40
  import { fileURLToPath, pathToFileURL } from "node:url";
41
+ import { CodemodeSandbox, CodemodeSourceError, parseCodemodeSource, renderDeclarations } from "@earendil-works/pi-codemode";
41
42
  import { AsyncLocalStorage } from "node:async_hooks";
42
43
  import * as posixPath from "node:path/posix";
43
44
  import * as win32Path from "node:path/win32";
@@ -76727,7 +76728,7 @@ const PY_DONE_MARKER = "__SCREAM_PY_DONE__";
76727
76728
  const MAX_OUTPUT_CHARS = 5e4;
76728
76729
  const TAIL_CHARS = 2e4;
76729
76730
  const TRUNCATION_MARKER = "[...truncated]";
76730
- function truncateOutput(output) {
76731
+ function truncateOutput$1(output) {
76731
76732
  if (output.length <= MAX_OUTPUT_CHARS) return output;
76732
76733
  return `${output.slice(0, MAX_OUTPUT_CHARS - TAIL_CHARS)}\n${TRUNCATION_MARKER}\n${output.slice(-2e4)}`;
76733
76734
  }
@@ -77096,7 +77097,7 @@ except BaseException as __e:
77096
77097
  this.kernel = void 0;
77097
77098
  return {
77098
77099
  isError: true,
77099
- output: `Python execution timed out after ${Math.round(timeoutMs / 1e3)}s (kernel restarted).\n${truncateOutput(merged)}`
77100
+ output: `Python execution timed out after ${Math.round(timeoutMs / 1e3)}s (kernel restarted).\n${truncateOutput$1(merged)}`
77100
77101
  };
77101
77102
  }
77102
77103
  if (!await this.syncKernel(proc)) {
@@ -77104,7 +77105,7 @@ except BaseException as __e:
77104
77105
  this.kernel = void 0;
77105
77106
  return {
77106
77107
  isError: true,
77107
- output: `Python execution timed out after ${Math.round(timeoutMs / 1e3)}s (kernel restarted).\n${truncateOutput(merged)}`
77108
+ output: `Python execution timed out after ${Math.round(timeoutMs / 1e3)}s (kernel restarted).\n${truncateOutput$1(merged)}`
77108
77109
  };
77109
77110
  }
77110
77111
  await this.drainStderrQuiet(proc);
@@ -77113,11 +77114,11 @@ except BaseException as __e:
77113
77114
  const timedOutMerged = [merged, interruptStderr].filter((part) => part.length > 0).join("\n");
77114
77115
  return {
77115
77116
  isError: true,
77116
- output: `Python execution timed out after ${Math.round(timeoutMs / 1e3)}s (kernel interrupted; state preserved).\n${truncateOutput(timedOutMerged)}`
77117
+ output: `Python execution timed out after ${Math.round(timeoutMs / 1e3)}s (kernel interrupted; state preserved).\n${truncateOutput$1(timedOutMerged)}`
77117
77118
  };
77118
77119
  }
77119
77120
  const isError = merged.includes("Traceback") || merged.toLowerCase().includes("error:");
77120
- const finalOutput = truncateOutput(merged);
77121
+ const finalOutput = truncateOutput$1(merged);
77121
77122
  return {
77122
77123
  isError,
77123
77124
  output: finalOutput.length > 0 ? finalOutput : "(no output)"
@@ -77190,6 +77191,281 @@ except BaseException as __e:
77190
77191
  }
77191
77192
  }
77192
77193
  };
77194
+ //#endregion
77195
+ //#region ../../packages/agent-core/src/tools/builtin/script/script.ts
77196
+ /** Model-facing tool name. */
77197
+ const SCRIPT_TOOL_NAME = "RunScript";
77198
+ /** Declaration budget for the tool description (≈3000 tokens at 4 chars/token). */
77199
+ const DECLARATION_BUDGET_CHARS = 12e3;
77200
+ const CHARS_PER_TOKEN = 4;
77201
+ const DEFAULT_MAX_OUTPUT_TOKENS = 1e4;
77202
+ /** QuickJS heap ceiling per execution. */
77203
+ const SANDBOX_MEMORY_LIMIT_BYTES = 256 * 1024 * 1024;
77204
+ /**
77205
+ * Nested-call *recording* caps, mirroring the reference runner: a call always
77206
+ * runs — only its record degrades. Arguments are dropped from the record once
77207
+ * the byte budgets are exhausted, and records stop once the call cap is hit.
77208
+ */
77209
+ const MAX_NESTED_CALLS = 256;
77210
+ const MAX_NESTED_ARG_BYTES = 8 * 1024;
77211
+ const MAX_NESTED_TOTAL_ARG_BYTES = 32 * 1024;
77212
+ const MAX_NESTED_MESSAGE_CHARS = 500;
77213
+ const ARGS_PREVIEW_CHARS = 160;
77214
+ const SCRIPT_PREAMBLE = [
77215
+ "Run JavaScript in a sandbox whose only capability is calling the tools listed below.",
77216
+ "The tool calls the script makes never enter the conversation — only what the script",
77217
+ "returns through `text(...)` (and `console.*`) is shown.",
77218
+ "",
77219
+ "When to use: several tool calls can be planned up front — batch work across many",
77220
+ "files, chained steps that need no decision in between, or filtering/aggregating",
77221
+ "results before reporting them.",
77222
+ "When not to use: a single tool call is enough, or each next step depends on",
77223
+ "observing the previous result.",
77224
+ "",
77225
+ "Rules:",
77226
+ "- Call tools as `await tools.<Name>({...})`; calls are auto-awaited and resolve to the",
77227
+ " tool's text output. A failing call rejects with an Error carrying the failure text.",
77228
+ "- `store(key, value)` / `load(key)` persist small JSON values across turns (only successful runs commit writes).",
77229
+ "- `image(dataUrlOrBytes)` attaches an image to the result.",
77230
+ "- `exit()` ends the script early.",
77231
+ "- Optional first line: `// @options: {\"max_output_tokens\": 10000, \"timeout_ms\": 60000}`."
77232
+ ].join("\n");
77233
+ const ScriptInputSchema = z.object({ code: z.string().min(1).describe("JavaScript to run. Call the available tools as `tools.<Name>(args)`; only text()/console output reaches the conversation.") });
77234
+ /** Builds the tool description including declarations for the callable tools. */
77235
+ function buildScriptToolDescription(callableTools) {
77236
+ if (callableTools.length === 0) return `${SCRIPT_PREAMBLE}\n\nNo tools are currently available inside the script.`;
77237
+ const full = `${SCRIPT_PREAMBLE}\n\nTool declarations:\n\n${renderDeclarations({ tools: callableTools.map((tool) => ({
77238
+ name: tool.name,
77239
+ description: tool.description,
77240
+ inputSchema: tool.parameters,
77241
+ execute: () => void 0
77242
+ })) })}`;
77243
+ if (full.length <= DECLARATION_BUDGET_CHARS) return full;
77244
+ const names = callableTools.map((tool) => `- ${tool.name}: ${firstLineOf(tool.description)}`).join("\n");
77245
+ return [
77246
+ SCRIPT_PREAMBLE,
77247
+ "",
77248
+ `Tool declarations omitted (${callableTools.length} tools exceed the ${DECLARATION_BUDGET_CHARS}-character budget); call them by their exact names:`,
77249
+ names
77250
+ ].join("\n");
77251
+ }
77252
+ /**
77253
+ * Runs model-written JavaScript in a QuickJS sandbox (worker thread) with the
77254
+ * current step's tools exposed as `tools.<Name>()`. Only the script's own
77255
+ * output enters the conversation; every nested call goes through the regular
77256
+ * tool pipeline (validation, hooks, approval) via `ctx.runNestedToolCall`.
77257
+ */
77258
+ var ScriptTool = class {
77259
+ store;
77260
+ name = SCRIPT_TOOL_NAME;
77261
+ description = buildScriptToolDescription([]);
77262
+ parameters = toInputJsonSchema(ScriptInputSchema);
77263
+ constructor(store) {
77264
+ this.store = store;
77265
+ }
77266
+ resolveExecution(args) {
77267
+ let parsed;
77268
+ let parseError;
77269
+ try {
77270
+ parsed = parseCodemodeSource(args.code);
77271
+ } catch (error) {
77272
+ parseError = error instanceof CodemodeSourceError ? error.message : String(error);
77273
+ }
77274
+ return {
77275
+ description: "Running a script",
77276
+ approvalRule: this.name,
77277
+ execute: async (ctx) => {
77278
+ if (parsed === void 0) return {
77279
+ output: `Script rejected: ${parseError ?? "invalid source"}`,
77280
+ isError: true
77281
+ };
77282
+ return this.run(parsed, ctx);
77283
+ }
77284
+ };
77285
+ }
77286
+ async run(parsed, ctx) {
77287
+ const runner = ctx.runNestedToolCall;
77288
+ if (runner === void 0) return {
77289
+ output: "RunScript is unavailable in this context: nested tool calls are not supported here.",
77290
+ isError: true
77291
+ };
77292
+ const state = {
77293
+ calls: 0,
77294
+ argBytes: 0,
77295
+ records: []
77296
+ };
77297
+ const sandbox = new CodemodeSandbox({
77298
+ tools: runner.tools.map((tool) => this.toSandboxTool(tool, runner, ctx, state)),
77299
+ memoryLimitBytes: SANDBOX_MEMORY_LIMIT_BYTES,
77300
+ timeoutMs: parsed.options.timeoutMs ?? Number.POSITIVE_INFINITY
77301
+ });
77302
+ try {
77303
+ const result = await sandbox.execute(parsed.code, {
77304
+ signal: ctx.signal,
77305
+ store: this.store.get("scriptStore") ?? {}
77306
+ });
77307
+ if (!result.ok) return await this.failedResult(result.error, result.output, state);
77308
+ this.commitStoreWrites(result.storeWrites);
77309
+ return await this.successResult(result.output, result.value, state, parsed.options);
77310
+ } finally {
77311
+ await sandbox.close();
77312
+ }
77313
+ }
77314
+ toSandboxTool(tool, runner, ctx, state) {
77315
+ return {
77316
+ name: tool.name,
77317
+ description: tool.description,
77318
+ inputSchema: tool.parameters,
77319
+ execute: (args) => this.runNested(runner, tool.name, args, ctx, state)
77320
+ };
77321
+ }
77322
+ async runNested(runner, name, args, ctx, state) {
77323
+ state.calls += 1;
77324
+ const callId = `${ctx.toolCallId}/${state.calls}`;
77325
+ let record;
77326
+ if (state.records.length < MAX_NESTED_CALLS) {
77327
+ const serialized = tryStringify(args);
77328
+ const bytes = serialized === void 0 ? 0 : Buffer.byteLength(serialized, "utf8");
77329
+ const overBudget = serialized === void 0 || bytes > MAX_NESTED_ARG_BYTES || state.argBytes + bytes > MAX_NESTED_TOTAL_ARG_BYTES;
77330
+ record = {
77331
+ callId,
77332
+ name,
77333
+ status: "ok",
77334
+ durationMs: 0,
77335
+ ...overBudget ? { incomplete: true } : { argsPreview: previewOf(serialized) }
77336
+ };
77337
+ if (!overBudget) state.argBytes += bytes;
77338
+ state.records.push(record);
77339
+ }
77340
+ const startedAt = Date.now();
77341
+ try {
77342
+ const result = await runner.run({
77343
+ name,
77344
+ args,
77345
+ callId
77346
+ });
77347
+ if (record !== void 0) {
77348
+ record.status = result.isError === true ? "error" : "ok";
77349
+ record.durationMs = Date.now() - startedAt;
77350
+ }
77351
+ return resolveNestedValue(name, result);
77352
+ } catch (error) {
77353
+ if (record !== void 0) {
77354
+ record.status = "error";
77355
+ record.durationMs = Date.now() - startedAt;
77356
+ }
77357
+ throw error;
77358
+ }
77359
+ }
77360
+ commitStoreWrites(writes) {
77361
+ const next = { ...this.store.get("scriptStore") };
77362
+ for (const [key, value] of Object.entries(writes.set)) if (value === void 0) delete next[key];
77363
+ else next[key] = value;
77364
+ for (const key of writes.delete) delete next[key];
77365
+ this.store.set("scriptStore", next);
77366
+ }
77367
+ async successResult(output, value, state, options) {
77368
+ const textParts = output.filter((item) => item.type === "text").map((item) => item.text);
77369
+ if (value !== void 0) textParts.push(valueText(value));
77370
+ const { shown } = await truncateOutput(textParts.join("\n"), (options.maxOutputTokens ?? DEFAULT_MAX_OUTPUT_TOKENS) * CHARS_PER_TOKEN);
77371
+ const parts = [];
77372
+ if (shown.length > 0) parts.push({
77373
+ type: "text",
77374
+ text: shown
77375
+ });
77376
+ for (const item of output) if (item.type === "image") parts.push({
77377
+ type: "image_url",
77378
+ imageUrl: { url: `data:${item.mimeType};base64,${item.data}` }
77379
+ });
77380
+ if (parts.length === 0) parts.push({
77381
+ type: "text",
77382
+ text: "(script produced no output)"
77383
+ });
77384
+ return {
77385
+ output: parts,
77386
+ ...state.records.length > 0 ? { nestedCalls: state.records } : {}
77387
+ };
77388
+ }
77389
+ async failedResult(error, output, state) {
77390
+ const lines = [`Script failed (${error.kind}): ${error.message}`];
77391
+ const partial = output.filter((item) => item.type === "text").map((item) => item.text).join("\n");
77392
+ if (partial.length > 0) {
77393
+ const { shown } = await truncateOutput(partial, DEFAULT_MAX_OUTPUT_TOKENS * CHARS_PER_TOKEN);
77394
+ lines.push("", "Partial output before the failure:", shown);
77395
+ }
77396
+ if (state.records.length > 0) lines.push(`(${state.records.length} nested call${state.records.length === 1 ? "" : "s"} ran before the failure)`);
77397
+ return {
77398
+ output: lines.join("\n"),
77399
+ isError: true
77400
+ };
77401
+ }
77402
+ };
77403
+ /**
77404
+ * Provider-facing results resolve to their text for scripts; failures reject
77405
+ * with an Error so scripts can try/catch (mirroring the reference runner).
77406
+ */
77407
+ function resolveNestedValue(name, result) {
77408
+ const text = typeof result.output === "string" ? result.output : result.output.filter((part) => part.type === "text").map((part) => part.text).join("\n");
77409
+ if (result.isError === true) {
77410
+ const detail = [result.message, text].filter((part) => typeof part === "string" && part.length > 0).join("\n");
77411
+ throw new Error(truncateChars(detail.length > 0 ? detail : `Tool "${name}" failed`, MAX_NESTED_MESSAGE_CHARS));
77412
+ }
77413
+ return text;
77414
+ }
77415
+ /** Renders a script's returned value as text (string as-is, everything else JSON). */
77416
+ function valueText(value) {
77417
+ if (typeof value === "string") return value;
77418
+ try {
77419
+ return JSON.stringify(value, null, 2) ?? String(value);
77420
+ } catch {
77421
+ return String(value);
77422
+ }
77423
+ }
77424
+ /**
77425
+ * Truncates to the character budget, keeping half the budget on each side
77426
+ * (matching the reference implementation). The full text is spilled to a
77427
+ * temp file and its path reported in the shown output.
77428
+ */
77429
+ async function truncateOutput(text, maxChars) {
77430
+ if (text.length <= maxChars) return { shown: text };
77431
+ const headChars = Math.floor(maxChars / 2);
77432
+ const tailChars = maxChars - headChars;
77433
+ const removedChars = text.length - headChars - tailChars;
77434
+ const path = join(tmpdir(), `scream-script-output-${randomBytes(6).toString("hex")}.txt`);
77435
+ let spillNote;
77436
+ try {
77437
+ await writeFile(path, text, "utf8");
77438
+ spillNote = `[Full output: ${path}]`;
77439
+ } catch (error) {
77440
+ spillNote = `[Could not save the full output: ${String(error)}]`;
77441
+ }
77442
+ return { shown: [
77443
+ `Warning: truncated output (original token count: ${Math.ceil(text.length / CHARS_PER_TOKEN)})`,
77444
+ `Total output lines: ${text.split("\n").length}`,
77445
+ "",
77446
+ ...headChars > 0 ? [text.slice(0, headChars)] : [],
77447
+ `… (${Math.ceil(removedChars / CHARS_PER_TOKEN)} tokens truncated) …`,
77448
+ ...tailChars > 0 ? [text.slice(-tailChars)] : [],
77449
+ "",
77450
+ spillNote
77451
+ ].join("\n") };
77452
+ }
77453
+ function tryStringify(args) {
77454
+ try {
77455
+ return JSON.stringify(args ?? {});
77456
+ } catch {
77457
+ return;
77458
+ }
77459
+ }
77460
+ function previewOf(serialized) {
77461
+ return truncateChars((serialized ?? "").replaceAll("\n", " "), ARGS_PREVIEW_CHARS);
77462
+ }
77463
+ function truncateChars(value, max) {
77464
+ return value.length <= max ? value : `${value.slice(0, max)}…`;
77465
+ }
77466
+ function firstLineOf(description) {
77467
+ return truncateChars(description.split("\n", 1)[0] ?? "", 120);
77468
+ }
77193
77469
  /** Families eligible for command-template normalization and segment matching. */
77194
77470
  const COMMAND_FAMILIES = new Set([
77195
77471
  "git checkout",
@@ -79479,22 +79755,26 @@ var DefaultCompactionStrategy = class {
79479
79755
  const reservedSize = this.config.reservedContextSize;
79480
79756
  return reservedSize > 0 && reservedSize < this.maxSize && usedSize + reservedSize >= this.maxSize;
79481
79757
  }
79482
- computeCompactCount(messages, source) {
79758
+ computeCompactCount(messages, source, options) {
79483
79759
  if (source === "manual") {
79484
79760
  for (let i = messages.length - 1; i > 0; i--) if (canSplitAfter(messages, i)) return i + 1;
79485
79761
  return 0;
79486
79762
  }
79487
79763
  let recentMessages = 1;
79764
+ let keptMessages = 0;
79488
79765
  let recentUserMessages = 0;
79489
79766
  let recentSize = 0;
79490
79767
  let bestN;
79491
79768
  for (; recentMessages < messages.length; recentMessages++) {
79492
79769
  const splitIndex = messages.length - recentMessages - 1;
79493
79770
  const m2 = messages[messages.length - recentMessages];
79494
- if (m2.role === "user") recentUserMessages++;
79495
- recentSize += estimateTokensForMessage(m2);
79771
+ if (options?.isRemoved?.(m2) !== true) {
79772
+ keptMessages++;
79773
+ if (m2.role === "user") recentUserMessages++;
79774
+ recentSize += estimateTokensForMessage(m2);
79775
+ }
79496
79776
  if (canSplitAfter(messages, splitIndex)) bestN = splitIndex + 1;
79497
- if ((recentMessages >= this.config.maxRecentMessages || recentUserMessages >= this.config.maxRecentUserMessages || recentSize >= this.config.maxRecentTokens || recentSize >= this.maxSize * this.config.maxRecentSizeRatio) && bestN !== void 0) break;
79777
+ if ((keptMessages >= this.config.maxRecentMessages || recentUserMessages >= this.config.maxRecentUserMessages || recentSize >= this.config.maxRecentTokens || recentSize >= this.maxSize * this.config.maxRecentSizeRatio) && bestN !== void 0) break;
79498
79778
  }
79499
79779
  return bestN ?? 0;
79500
79780
  }
@@ -79782,7 +80062,7 @@ var FullCompaction = class {
79782
80062
  else this.compactionCountInTurn += 1;
79783
80063
  if (this.compactionCountInTurn > this.strategy.maxCompactionPerTurn) return;
79784
80064
  if (this.agent.records.restoring) return;
79785
- const compactedCount = this.strategy.computeCompactCount(this.agent.context.history, data.source);
80065
+ const compactedCount = this.strategy.computeCompactCount(this.agent.context.history, data.source, { isRemoved: (message) => this.agent.context.isMessageRemovedByEdit(message) });
79786
80066
  if (compactedCount === 0) {
79787
80067
  if (data.source === "manual") throw new ScreamError(ErrorCodes.COMPACTION_UNABLE, "No prefix that can be compacted in current history.");
79788
80068
  return;
@@ -79936,7 +80216,7 @@ var FullCompaction = class {
79936
80216
  const originalHistory = [...this.agent.context.history];
79937
80217
  const systemPromptTokens = estimateTokens$1(this.agent.getRuntimeSystemPrompt());
79938
80218
  const toolTokens = estimateTokensForTools(this.agent.tools.loopTools);
79939
- const tokensBefore = systemPromptTokens + toolTokens + estimateTokensForMessages(originalHistory);
80219
+ const tokensBefore = systemPromptTokens + toolTokens + estimateTokensForMessages(this.agent.context.applyMessageEdits(originalHistory));
79940
80220
  const model = this.agent.config.model;
79941
80221
  const isUpdate = extractPreviousSummary(originalHistory) !== null;
79942
80222
  let retryCount = 0;
@@ -79945,7 +80225,7 @@ var FullCompaction = class {
79945
80225
  const delays = retryBackoffDelays(5);
79946
80226
  const summarizeOnce = async (messagesToCompact) => {
79947
80227
  const instruction = isUpdate ? COMPACTION_UPDATE_INSTRUCTION(data.instruction) : COMPACTION_INSTRUCTION(data.instruction);
79948
- const messages = [...project(this.agent.microCompaction.compact(messagesToCompact)), {
80228
+ const messages = [...project(this.agent.context.applyMessageEdits(this.agent.microCompaction.compact(messagesToCompact))), {
79949
80229
  role: "user",
79950
80230
  content: [{
79951
80231
  type: "text",
@@ -79978,7 +80258,7 @@ var FullCompaction = class {
79978
80258
  }
79979
80259
  this.agent.log.warn("compaction overflow at minimum split, falling back to re-summarize", {
79980
80260
  compactedCount,
79981
- tokensBefore: estimateTokensForMessages(messagesToCompact)
80261
+ tokensBefore: estimateTokensForMessages(this.agent.context.applyMessageEdits(messagesToCompact))
79982
80262
  });
79983
80263
  const result = await summarizeWithFallback(messagesToCompact, summarizeOnce);
79984
80264
  summary = result.summary;
@@ -79999,6 +80279,7 @@ var FullCompaction = class {
79999
80279
  return;
80000
80280
  }
80001
80281
  const recent = originalHistory.slice(compactedCount);
80282
+ const projectedRecent = this.agent.context.applyMessageEdits(recent);
80002
80283
  const messagesToCompactForOps = originalHistory.slice(0, compactedCount);
80003
80284
  const fileOps = createFileOps();
80004
80285
  if (this.lastCompactionFiles !== void 0 && extractPreviousSummary(originalHistory) !== null) {
@@ -80009,11 +80290,12 @@ var FullCompaction = class {
80009
80290
  const toolCallHistory = formatToolCallHistory(messagesToCompactForOps);
80010
80291
  const skillCandidateSummary = summary;
80011
80292
  const processedSummary = this.postProcessSummary(summary, fileOps, toolCallHistory, compactedCount);
80012
- const tokensAfter = systemPromptTokens + toolTokens + estimateTokens$1(processedSummary) + estimateTokensForMessages(recent);
80293
+ const tokensAfter = systemPromptTokens + toolTokens + estimateTokens$1(processedSummary) + estimateTokensForMessages(projectedRecent);
80013
80294
  const fileLists = computeFileLists(fileOps);
80014
80295
  const MAX_PERSISTED_FILES = 100;
80015
80296
  const readFiles = fileLists.readFiles.slice(0, MAX_PERSISTED_FILES);
80016
80297
  const modifiedFiles = fileLists.modifiedFiles.slice(0, MAX_PERSISTED_FILES);
80298
+ const firstKeptMessageId = originalHistory[compactedCount]?.id;
80017
80299
  const result = {
80018
80300
  summary: processedSummary,
80019
80301
  compactedCount,
@@ -80021,7 +80303,8 @@ var FullCompaction = class {
80021
80303
  tokensAfter,
80022
80304
  ...readFiles.length > 0 ? { readFiles } : {},
80023
80305
  ...modifiedFiles.length > 0 ? { modifiedFiles } : {},
80024
- ...isUpdate ? { isUpdate: true } : {}
80306
+ ...isUpdate ? { isUpdate: true } : {},
80307
+ ...firstKeptMessageId !== void 0 ? { firstKeptMessageId } : {}
80025
80308
  };
80026
80309
  this.markCompleted();
80027
80310
  this.agent.emitEvent({
@@ -81734,6 +82017,16 @@ var ContextMemory = class {
81734
82017
  pendingToolResultIds = /* @__PURE__ */ new Set();
81735
82018
  deferredMessages = [];
81736
82019
  /**
82020
+ * Persisted projection edits keyed by message id: `null` removes the
82021
+ * message from the projection, an array replaces its content. The raw
82022
+ * history is never modified — every LLM-bound read path funnels through
82023
+ * {@link applyMessageEdits}.
82024
+ */
82025
+ _messageEdits = /* @__PURE__ */ new Map();
82026
+ /** Counter behind {@link assignMessageId}. Never reset: ids must stay
82027
+ * unique for the whole session lifetime, including across clear/undo. */
82028
+ _nextMessageId = 0;
82029
+ /**
81737
82030
  * Per-message fingerprints captured from the last message list handed to
81738
82031
  * the LLM via {@link messagesForLLM}. Used to measure prefix stability
81739
82032
  * across calls: a provider prompt cache only hits when the leading
@@ -81755,7 +82048,8 @@ var ContextMemory = class {
81755
82048
  tokenCountCoveredMessageCount: this.tokenCountCoveredMessageCount,
81756
82049
  openSteps: new Map(this.openSteps),
81757
82050
  pendingToolResultIds: new Set(this.pendingToolResultIds),
81758
- deferredMessages: [...this.deferredMessages]
82051
+ deferredMessages: [...this.deferredMessages],
82052
+ messageEdits: new Map(this._messageEdits)
81759
82053
  };
81760
82054
  }
81761
82055
  restore(snapshot) {
@@ -81765,6 +82059,9 @@ var ContextMemory = class {
81765
82059
  this.openSteps = new Map(snapshot.openSteps);
81766
82060
  this.pendingToolResultIds = new Set(snapshot.pendingToolResultIds);
81767
82061
  this.deferredMessages = [...snapshot.deferredMessages];
82062
+ this._messageEdits = new Map(snapshot.messageEdits ?? []);
82063
+ for (const message of this._history) this.assignMessageId(message);
82064
+ for (const message of this.deferredMessages) this.assignMessageId(message);
81768
82065
  }
81769
82066
  /**
81770
82067
  * JSON-safe snapshot for wire persistence. `openSteps` values are recorded as
@@ -81779,7 +82076,8 @@ var ContextMemory = class {
81779
82076
  tokenCountCoveredMessageCount: this.tokenCountCoveredMessageCount,
81780
82077
  openSteps: [...this.openSteps.entries()].map(([uuid, message]) => [uuid, this._history.indexOf(message)]),
81781
82078
  pendingToolResultIds: [...this.pendingToolResultIds],
81782
- deferredMessages: [...this.deferredMessages]
82079
+ deferredMessages: [...this.deferredMessages],
82080
+ ...this._messageEdits.size > 0 ? { messageEdits: [...this._messageEdits.entries()] } : {}
81783
82081
  };
81784
82082
  }
81785
82083
  /**
@@ -81801,6 +82099,9 @@ var ContextMemory = class {
81801
82099
  this.openSteps = openSteps;
81802
82100
  this.pendingToolResultIds = new Set(snapshot.pendingToolResultIds);
81803
82101
  this.deferredMessages = [...snapshot.deferredMessages];
82102
+ this._messageEdits = new Map(snapshot.messageEdits ?? []);
82103
+ for (const message of this._history) this.assignMessageId(message);
82104
+ for (const message of this.deferredMessages) this.assignMessageId(message);
81804
82105
  }
81805
82106
  appendUserMessage(content, origin = USER_PROMPT_ORIGIN) {
81806
82107
  this.appendMessage({
@@ -81830,6 +82131,7 @@ var ContextMemory = class {
81830
82131
  this.openSteps.clear();
81831
82132
  this.pendingToolResultIds.clear();
81832
82133
  this.deferredMessages = [];
82134
+ this._messageEdits.clear();
81833
82135
  this.lastSentFingerprints = [];
81834
82136
  this.agent.injection.onContextClear();
81835
82137
  this.agent.microCompaction.reset();
@@ -81898,7 +82200,7 @@ var ContextMemory = class {
81898
82200
  type: "context.apply_compaction",
81899
82201
  ...summary
81900
82202
  });
81901
- this._history = [{
82203
+ const summaryMessage = {
81902
82204
  role: "assistant",
81903
82205
  content: [{
81904
82206
  type: "text",
@@ -81906,13 +82208,20 @@ var ContextMemory = class {
81906
82208
  }],
81907
82209
  toolCalls: [],
81908
82210
  origin: { kind: "compaction_summary" }
81909
- }, ...this._history.slice(summary.compactedCount)];
82211
+ };
82212
+ this.assignMessageId(summaryMessage);
82213
+ let cut = summary.compactedCount;
82214
+ if (summary.firstKeptMessageId !== void 0) {
82215
+ const byId = this._history.findIndex((m) => m.id === summary.firstKeptMessageId);
82216
+ if (byId >= 0) cut = byId;
82217
+ }
82218
+ this._history = [summaryMessage, ...this._history.slice(cut)];
81910
82219
  const survivingMessages = new Set(this._history);
81911
82220
  for (const [uuid, message] of this.openSteps) if (!survivingMessages.has(message)) this.openSteps.delete(uuid);
81912
82221
  this.flushDeferredMessagesIfToolExchangeClosed();
81913
82222
  this._tokenCount = summary.tokensAfter;
81914
82223
  this.tokenCountCoveredMessageCount = this._history.length;
81915
- this.agent.injection.onContextCompacted(summary.compactedCount);
82224
+ this.agent.injection.onContextCompacted(cut);
81916
82225
  this.agent.emitStatusUpdated();
81917
82226
  this.agent.microCompaction.reset();
81918
82227
  if (!this.agent.records.restoring) this.agent.records.logRecord({
@@ -81921,6 +82230,100 @@ var ContextMemory = class {
81921
82230
  compactedHistory: [...this.agent.fullCompaction.compactedHistory]
81922
82231
  });
81923
82232
  }
82233
+ /**
82234
+ * Apply persisted projection edits to a message list: messages edited to
82235
+ * `null` are dropped; messages with a replacement get a *copy* carrying the
82236
+ * new content (a fresh object keeps the token-estimate WeakMap from
82237
+ * returning the stale count). Messages without an edit — and every message
82238
+ * when no edits exist — pass through unchanged.
82239
+ *
82240
+ * Only what the model sees is affected: the raw history (UI, export, trace,
82241
+ * resume) keeps the original content.
82242
+ */
82243
+ applyMessageEdits(messages) {
82244
+ if (this._messageEdits.size === 0) return messages;
82245
+ const result = [];
82246
+ for (const message of messages) {
82247
+ const id = message.id;
82248
+ const edit = id === void 0 ? void 0 : this._messageEdits.get(id);
82249
+ if (edit === void 0) result.push(message);
82250
+ else if (edit !== null) result.push({
82251
+ ...message,
82252
+ content: [...edit]
82253
+ });
82254
+ }
82255
+ return result;
82256
+ }
82257
+ /** Whether a persisted edit removes this message from the projection. */
82258
+ isMessageRemovedByEdit(message) {
82259
+ const id = message.id;
82260
+ return id !== void 0 && this._messageEdits.get(id) === null;
82261
+ }
82262
+ /**
82263
+ * Persist a projection edit for the message with `targetId`:
82264
+ * `replacement: null` removes the message from every future provider
82265
+ * request; an array replaces its content. The edit is recorded, so it
82266
+ * replays on resume and survives restarts; the raw history message is never
82267
+ * rewritten.
82268
+ *
82269
+ * Live writes validate strictly (the target must exist and be editable).
82270
+ * During replay the same method runs for records read from the wire and
82271
+ * silently skips targets that no longer exist (e.g. the message was
82272
+ * compacted away before the edit replayed) — a newer wire must never fail
82273
+ * to load.
82274
+ */
82275
+ applyMessageEdit(targetId, replacement) {
82276
+ const index = this._history.findIndex((message) => message.id === targetId);
82277
+ const message = index === -1 ? this.deferredMessages.find((candidate) => candidate.id === targetId) : this._history[index];
82278
+ if (message === void 0) {
82279
+ if (this.agent.records.restoring) return;
82280
+ throw new Error(`context.edit_message: no message with id '${targetId}'`);
82281
+ }
82282
+ if (message.role === "system" || message.origin?.kind === "compaction_summary") {
82283
+ if (this.agent.records.restoring) return;
82284
+ throw new Error(`context.edit_message: message '${targetId}' is not editable`);
82285
+ }
82286
+ const previousEdit = this._messageEdits.get(targetId);
82287
+ const normalized = replacement === null ? null : replacement.map((part) => ({ ...part }));
82288
+ this._messageEdits.set(targetId, normalized);
82289
+ this.agent.records.logRecord({
82290
+ type: "context.edit_message",
82291
+ targetId,
82292
+ replacement: normalized
82293
+ });
82294
+ if (index !== -1 && index < this.tokenCountCoveredMessageCount) {
82295
+ const before = previousEdit === void 0 ? estimateTokensForMessages([message]) : previousEdit === null ? 0 : estimateTokensForMessages([{
82296
+ ...message,
82297
+ content: [...previousEdit]
82298
+ }]);
82299
+ const after = normalized === null ? 0 : estimateTokensForMessages([{
82300
+ ...message,
82301
+ content: normalized
82302
+ }]);
82303
+ this._tokenCount = Math.max(0, this._tokenCount + after - before);
82304
+ }
82305
+ this.agent.emitStatusUpdated();
82306
+ }
82307
+ /**
82308
+ * Assign a stable id the first time a message enters the history (or when
82309
+ * it is restored from the wire). Mutates the message in place — object
82310
+ * identity is load-bearing: open steps, deferred messages and the history
82311
+ * array all reference the same object. Messages that already carry an id
82312
+ * only advance the counter past it.
82313
+ */
82314
+ assignMessageId(message) {
82315
+ const existing = message.id;
82316
+ if (existing !== void 0) {
82317
+ const match = /^m(\d+)$/.exec(existing);
82318
+ if (match !== null) {
82319
+ const n = Number(match[1]);
82320
+ if (n >= this._nextMessageId) this._nextMessageId = n + 1;
82321
+ }
82322
+ return;
82323
+ }
82324
+ message.id = `m${this._nextMessageId}`;
82325
+ this._nextMessageId += 1;
82326
+ }
81924
82327
  data() {
81925
82328
  return {
81926
82329
  history: this.history,
@@ -81932,14 +82335,14 @@ var ContextMemory = class {
81932
82335
  }
81933
82336
  get tokenCountWithPending() {
81934
82337
  const pendingMessages = this._history.slice(this.tokenCountCoveredMessageCount);
81935
- return this._tokenCount + estimateTokensForMessages(project(pendingMessages));
82338
+ return this._tokenCount + estimateTokensForMessages(project(this.applyMessageEdits(pendingMessages)));
81936
82339
  }
81937
82340
  get history() {
81938
82341
  return this._history;
81939
82342
  }
81940
82343
  get messages() {
81941
82344
  this.agent.microCompaction.detect();
81942
- return project(this.agent.microCompaction.compact(this.history));
82345
+ return project(this.applyMessageEdits(this.agent.microCompaction.compact(this.history)));
81943
82346
  }
81944
82347
  /**
81945
82348
  * Build the message list for an LLM call, with prefix-stability
@@ -81961,7 +82364,7 @@ var ContextMemory = class {
81961
82364
  */
81962
82365
  messagesForLLM() {
81963
82366
  this.agent.microCompaction.detect();
81964
- const messages = project(this.agent.microCompaction.compact(this.history), { synthesizeMissing: true });
82367
+ const messages = project(this.applyMessageEdits(this.agent.microCompaction.compact(this.history)), { synthesizeMissing: true });
81965
82368
  this.observePrefixStability(messages);
81966
82369
  return messages;
81967
82370
  }
@@ -82054,6 +82457,7 @@ var ContextMemory = class {
82054
82457
  }
82055
82458
  }
82056
82459
  appendMessage(message) {
82460
+ this.assignMessageId(message);
82057
82461
  this.agent.records.logRecord({
82058
82462
  type: "context.append_message",
82059
82463
  message
@@ -82098,6 +82502,7 @@ var ContextMemory = class {
82098
82502
  return interruptedToolCallIds.length;
82099
82503
  }
82100
82504
  pushHistory(...messages) {
82505
+ for (const message of messages) this.assignMessageId(message);
82101
82506
  this._history.push(...messages);
82102
82507
  for (const message of messages) {
82103
82508
  if (message.origin?.kind === "background_task") this.agent.background.markDeliveredNotification(message.origin);
@@ -85363,7 +85768,11 @@ const WRITE_TOOLS = new Set([
85363
85768
  "Write"
85364
85769
  ]);
85365
85770
  /** Tools that execute commands. */
85366
- const EXECUTE_TOOLS = new Set(["Bash", "python"]);
85771
+ const EXECUTE_TOOLS = new Set([
85772
+ "Bash",
85773
+ "python",
85774
+ "RunScript"
85775
+ ]);
85367
85776
  /** Nesting/coordination tools — only `all` mode keeps them. A restricted
85368
85777
  * child must not be able to spawn an unrestricted grandchild (that would
85369
85778
  * bypass the tool filtering entirely). WolfPack is batch-spawn sugar over
@@ -87370,6 +87779,7 @@ const SNAPSHOT_FOLDED_CONTEXT_TYPES = new Set([
87370
87779
  "context.append_message",
87371
87780
  "context.append_loop_event",
87372
87781
  "context.apply_compaction",
87782
+ "context.edit_message",
87373
87783
  "micro_compaction.apply",
87374
87784
  "full_compaction.complete",
87375
87785
  "request.header"
@@ -88140,9 +88550,9 @@ function asMediaContainer(value) {
88140
88550
  /**
88141
88551
  * Record types whose state is fully captured by a `context.snapshot` record.
88142
88552
  * When a snapshot exists, every one of these that predates it is skipped during
88143
- * replay because the snapshot already holds the folded context memory. Note
88144
- * `micro_compaction.apply` is written with an `as never` cast (it is not part of
88145
- * the AgentRecord union), so it is matched here by its literal type string.
88553
+ * replay because the snapshot already holds the folded context memory —
88554
+ * including `context.edit_message`, whose edits ride inside the snapshot
88555
+ * payload (`messageEdits`).
88146
88556
  *
88147
88557
  * `full_compaction.complete` is included because its only lasting effect is
88148
88558
  * pushing onto `compactedHistory` — a debug trail rendered from the pre-fold
@@ -88244,6 +88654,9 @@ function restoreAgentRecord(agent, input) {
88244
88654
  agent.context.applyCompaction(input);
88245
88655
  recoverMemosFromCompactionSummary(agent, input.summary);
88246
88656
  return;
88657
+ case "context.edit_message":
88658
+ agent.context.applyMessageEdit(input.targetId, input.replacement);
88659
+ return;
88247
88660
  case "context.snapshot":
88248
88661
  agent.context.restoreJSONSnapshot(input.snapshot);
88249
88662
  agent.fullCompaction.restoreCompactedHistory(input.compactedHistory);
@@ -96377,7 +96790,8 @@ async function prepareToolCall(step, call) {
96377
96790
  toolCallId: call.toolCall.id,
96378
96791
  toolName: call.toolName,
96379
96792
  args: call.args,
96380
- rawArguments: call.toolCall.arguments
96793
+ rawArguments: call.toolCall.arguments,
96794
+ nested: step.nested === true
96381
96795
  });
96382
96796
  } catch {
96383
96797
  reminder = null;
@@ -96448,7 +96862,8 @@ async function runPrepareToolExecutionHook(step, call) {
96448
96862
  turnId,
96449
96863
  stepNumber: currentStep,
96450
96864
  signal,
96451
- llm
96865
+ llm,
96866
+ nested: step.nested === true
96452
96867
  });
96453
96868
  } catch (error) {
96454
96869
  if (isAbortError$1(error) || signal.aborted) return {
@@ -96491,7 +96906,8 @@ async function runAuthorizeToolExecutionHook(step, call, args, execution) {
96491
96906
  turnId,
96492
96907
  stepNumber: currentStep,
96493
96908
  signal,
96494
- llm
96909
+ llm,
96910
+ nested: step.nested === true
96495
96911
  });
96496
96912
  } catch (error) {
96497
96913
  if (isAbortError$1(error) || signal.aborted) return {
@@ -96545,7 +96961,8 @@ async function finalizePendingToolResult(step, pendingResult) {
96545
96961
  turnId,
96546
96962
  stepNumber: currentStep,
96547
96963
  signal,
96548
- llm
96964
+ llm,
96965
+ nested: step.nested === true
96549
96966
  }) ?? pendingResult.result, pendingResult.toolName);
96550
96967
  return {
96551
96968
  ...pendingResult,
@@ -96578,6 +96995,10 @@ async function executeTool(step, execution, toolCall, toolName, metadata) {
96578
96995
  toolCallId: toolCall.id,
96579
96996
  metadata,
96580
96997
  signal,
96998
+ runNestedToolCall: {
96999
+ tools: (step.tools ?? []).filter((tool) => tool.name !== toolName),
97000
+ run: (request) => executeNestedToolCall(step, toolName, request)
97001
+ },
96581
97002
  onUpdate: (update) => {
96582
97003
  if (signal.aborted) return;
96583
97004
  dispatchEvent({
@@ -96653,10 +97074,16 @@ function normalizeToolResult(r) {
96653
97074
  const textJoined = r.output.filter((c) => c.type === "text").map((c) => c.text).join("");
96654
97075
  output = textJoined.length > 0 ? textJoined : TOOL_OUTPUT_EMPTY;
96655
97076
  }
97077
+ const nestedCalls = r.isError === true ? void 0 : r.nestedCalls;
97078
+ const nested = nestedCalls !== void 0 ? { nestedCalls } : {};
96656
97079
  return r.isError === true ? {
96657
97080
  output,
96658
- isError: true
96659
- } : { output };
97081
+ isError: true,
97082
+ ...nested
97083
+ } : {
97084
+ output,
97085
+ ...nested
97086
+ };
96660
97087
  }
96661
97088
  function makeToolResult(call, args, result) {
96662
97089
  return {
@@ -96682,6 +97109,7 @@ function makeErrorToolResult(call, args, output) {
96682
97109
  */
96683
97110
  async function dispatchToolCall(step, call, args, displayFields) {
96684
97111
  const { toolCall, toolName } = call;
97112
+ if (step.nested === true) return;
96685
97113
  const index = step.toolCallBlockIndex.value++;
96686
97114
  await step.dispatchEvent({
96687
97115
  type: "block.start",
@@ -96714,6 +97142,34 @@ async function dispatchToolCall(step, call, args, displayFields) {
96714
97142
  blockType: "tool-call"
96715
97143
  });
96716
97144
  }
97145
+ /**
97146
+ * Run a tool call issued by *another tool* (the script sandbox) through the
97147
+ * same pipeline as model-issued calls: preflight validation → prepare and
97148
+ * authorize hooks (host hooks + approval) → `resolveExecution` → execution
97149
+ * with grace timeout → finalize/normalize.
97150
+ *
97151
+ * Only the model-visible recording is suppressed: no `block.start` /
97152
+ * `tool.call` / `tool.result` events are dispatched, so the orchestrating
97153
+ * tool's own result stays the single transcript entry for the whole batch.
97154
+ * Everything else — argument validation, hooks, approval, aborts and the
97155
+ * grace timeout — behaves exactly like a direct call.
97156
+ */
97157
+ async function executeNestedToolCall(step, parentToolName, request) {
97158
+ const tools = (step.tools ?? []).filter((tool) => tool.name !== parentToolName);
97159
+ const toolCall = {
97160
+ type: "function",
97161
+ id: request.callId,
97162
+ name: request.name,
97163
+ arguments: JSON.stringify(request.args ?? {})
97164
+ };
97165
+ const nestedStep = {
97166
+ ...step,
97167
+ tools,
97168
+ nested: true,
97169
+ dispatchEvent: async () => {}
97170
+ };
97171
+ return (await finalizePendingToolResult(nestedStep, await (await (await prepareToolCall(nestedStep, preflightToolCall(tools, toolCall))).task.start()).result)).result;
97172
+ }
96717
97173
  //#endregion
96718
97174
  //#region ../../packages/agent-core/src/loop/turn-step.ts
96719
97175
  /**
@@ -101279,7 +101735,7 @@ function normalizeSourcePath(path) {
101279
101735
  }
101280
101736
  //#endregion
101281
101737
  //#region ../../packages/agent-core/src/profile/default/agent.yaml
101282
- var agent_default = "name: agent\ndescription: Default Scream Code agent\n\nsystemPromptPath: ./system.md\npromptVars:\n roleAdditional: ''\n\ntools:\n - Read\n - Write\n - Edit\n - Grep\n - Glob\n - Bash\n - LSP\n - TaskList\n - TaskOutput\n - TaskStop\n - CronCreate\n - CronList\n - CronDelete\n - CreateGoal\n - GetGoal\n - SetGoalBudget\n - UpdateGoal\n - ReadMediaFile\n - TodoList\n - MemoryLookup\n - MemoryConsolidatePlan\n - MemoryConsolidateApply\n - MemoryWrite\n - KnowledgeLookup\n # Image generation. Like ManagePlugin, registered for main agents only —\n # subagents inherit the name but never receive the tool.\n - ImageGenerate\n - InspectOwnAssets\n - Skill\n - MakeSkillPlan\n - MakeSkillApply\n # Main-agent capability management. Subagent profiles inherit this list, but\n # the tool is only registered for main agents, so it never reaches their\n # model-visible tool set.\n - ManagePlugin\n - WebSearch\n - PaperSearch\n - Agent\n - SendSubagentMessage\n - WolfPack\n\n - FetchURL\n - AskUserQuestion\n - EnterPlanMode\n - FusionPlan\n - ExitPlanMode\n - mcp__*\n\n# Routing text for the parent's `Agent` tool: the description is the \"what\",\n# the profile's own `whenToUse` supplies the \"call me when / do not call me\n# for\" triggers. Keep both short and trigger-shaped so all types stay\n# comparable when the model picks one. This map is the single source of truth:\n# system.md must not restate it.\nsubagents:\n coder:\n description: Edits files and runs commands for non-trivial engineering work, then reports a compact, technically complete summary.\n explore:\n description: Read-only reconnaissance across files; returns a structured map (summary, inventory, architecture) and never edits.\n plan:\n description: Read-only architect; turns a request into a step-by-step, file-level implementation plan with trade-offs.\n verify:\n description: Runs the project's real gates (build, test, lint, typecheck) and reports pass/fail with raw command evidence.\n reviewer:\n description: Adversarial review of a diff; reports bugs, contract violations and integration risk by severity.\n oracle:\n description: Large-scope review of code that already exists — architecture soundness, tech debt, long-term maintainability, dead or redundant code — with evidence and a concrete fix per finding; does not edit unless asked.\n worker:\n description: Office and document automation worker; format conversion, batch file processing and file organization, and never code or prose.\n writer:\n description: Professional writing and document specialist; drafts, rewrites, translates, summarizes and produces document artifacts.\n designer:\n description: 'Visual-layer specialist: UI/UX/TUI design direction, code-grade specs, screen polish, and design audits.'\n";
101738
+ var agent_default = "name: agent\ndescription: Default Scream Code agent\n\nsystemPromptPath: ./system.md\npromptVars:\n roleAdditional: ''\n\ntools:\n - Read\n - Write\n - Edit\n - Grep\n - Glob\n - Bash\n # Script execution mode (enabled by default): runs model-written JavaScript\n # in a QuickJS sandbox and calls the tools above from inside the script.\n # Toggle per session with /script. Main agent only — subagent profiles keep\n # their own restricted tool lists.\n - RunScript\n - LSP\n - TaskList\n - TaskOutput\n - TaskStop\n - CronCreate\n - CronList\n - CronDelete\n - CreateGoal\n - GetGoal\n - SetGoalBudget\n - UpdateGoal\n - ReadMediaFile\n - TodoList\n - MemoryLookup\n - MemoryConsolidatePlan\n - MemoryConsolidateApply\n - MemoryWrite\n - KnowledgeLookup\n # Image generation. Like ManagePlugin, registered for main agents only —\n # subagents inherit the name but never receive the tool.\n - ImageGenerate\n - InspectOwnAssets\n - Skill\n - MakeSkillPlan\n - MakeSkillApply\n # Main-agent capability management. Subagent profiles inherit this list, but\n # the tool is only registered for main agents, so it never reaches their\n # model-visible tool set.\n - ManagePlugin\n - WebSearch\n - PaperSearch\n - Agent\n - SendSubagentMessage\n - WolfPack\n\n - FetchURL\n - AskUserQuestion\n - EnterPlanMode\n - FusionPlan\n - ExitPlanMode\n - mcp__*\n\n# Routing text for the parent's `Agent` tool: the description is the \"what\",\n# the profile's own `whenToUse` supplies the \"call me when / do not call me\n# for\" triggers. Keep both short and trigger-shaped so all types stay\n# comparable when the model picks one. This map is the single source of truth:\n# system.md must not restate it.\nsubagents:\n coder:\n description: Edits files and runs commands for non-trivial engineering work, then reports a compact, technically complete summary.\n explore:\n description: Read-only reconnaissance across files; returns a structured map (summary, inventory, architecture) and never edits.\n plan:\n description: Read-only architect; turns a request into a step-by-step, file-level implementation plan with trade-offs.\n verify:\n description: Runs the project's real gates (build, test, lint, typecheck) and reports pass/fail with raw command evidence.\n reviewer:\n description: Adversarial review of a diff; reports bugs, contract violations and integration risk by severity.\n oracle:\n description: Large-scope review of code that already exists — architecture soundness, tech debt, long-term maintainability, dead or redundant code — with evidence and a concrete fix per finding; does not edit unless asked.\n worker:\n description: Office and document automation worker; format conversion, batch file processing and file organization, and never code or prose.\n writer:\n description: Professional writing and document specialist; drafts, rewrites, translates, summarizes and produces document artifacts.\n designer:\n description: 'Visual-layer specialist: UI/UX/TUI design direction, code-grade specs, screen polish, and design audits.'\n";
101283
101739
  //#endregion
101284
101740
  //#region ../../packages/agent-core/src/profile/default/coder.yaml
101285
101741
  var coder_default = "extends: agent\nname: coder\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.\nwhenToUse: >-\n USE WHEN: the task needs files edited or commands run in the workspace, spans more than ~3 files, or is mechanical multi-file propagation (rename, migration, type sweep). NOT FOR: read-only investigation (explore), planning (plan), verification-only requests on code you did not change (verify), reviewing a diff (reviewer).\ntools:\n - ContactParent\n - Bash\n - Read\n - ReadMediaFile\n - Glob\n - Grep\n - Write\n - Edit\n - LSP\n - WebSearch\n - PaperSearch\n - FetchURL\n - MemoryLookup\n - MemoryConsolidatePlan\n - MemoryConsolidateApply\n - mcp__*\n";
@@ -102309,7 +102765,22 @@ var ToolManager = class {
102309
102765
  loopToolsFor(filter) {
102310
102766
  const mcpNames = [...this.mcpTools.keys()].filter((name) => filter.mcpPatterns.some((pattern) => import_picomatch.default.isMatch(name, pattern)));
102311
102767
  const hideGoalMutationTools = this.agent.goal.getGoal().goal === null;
102312
- return uniq([...filter.names, ...mcpNames]).toSorted((a, b) => a.localeCompare(b)).filter((name) => !(hideGoalMutationTools && (name === "SetGoalBudget" || name === "UpdateGoal" || name === "WriteGoalNote"))).map((name) => this.userTools.get(name) ?? this.mcpTools.get(name)?.tool ?? this.builtinTools.get(name)).filter((tool) => !!tool);
102768
+ const tools = uniq([...filter.names, ...mcpNames]).toSorted((a, b) => a.localeCompare(b)).filter((name) => !(hideGoalMutationTools && (name === "SetGoalBudget" || name === "UpdateGoal" || name === "WriteGoalNote"))).map((name) => this.userTools.get(name) ?? this.mcpTools.get(name)?.tool ?? this.builtinTools.get(name)).filter((tool) => !!tool);
102769
+ return this.decorateScriptOffering(tools);
102770
+ }
102771
+ /**
102772
+ * The script sandbox advertises exactly the tools a script may call, so its
102773
+ * description is rebuilt against this step's offered set (the class-level
102774
+ * description is the empty-set fallback). Matches the built-in instance by
102775
+ * identity on purpose: a user tool that happens to reuse the name must not
102776
+ * be re-decorated or lose its own description.
102777
+ */
102778
+ decorateScriptOffering(tools) {
102779
+ const scriptTool = this.builtinTools.get(SCRIPT_TOOL_NAME);
102780
+ if (scriptTool === void 0 || !tools.includes(scriptTool)) return tools;
102781
+ const callable = tools.filter((tool) => tool !== scriptTool);
102782
+ const decorated = Object.assign(Object.create(Object.getPrototypeOf(scriptTool)), scriptTool, { description: buildScriptToolDescription(callable) });
102783
+ return tools.map((tool) => tool === scriptTool ? decorated : tool);
102313
102784
  }
102314
102785
  *toolInfos() {
102315
102786
  for (const tool of this.builtinTools.values()) yield {
@@ -102364,6 +102835,7 @@ var ToolManager = class {
102364
102835
  availableTools: this.enabledTools
102365
102836
  }),
102366
102837
  new PythonTool(cwd, { hostHandlers: createRlmHostHandlers(this.agent) }),
102838
+ new ScriptTool(this.toolStore),
102367
102839
  (modelCapabilities.image_in || modelCapabilities.video_in) && new ReadMediaFileTool(jian, workspace, modelCapabilities, videoUploader),
102368
102840
  new EnterPlanModeTool(this.agent),
102369
102841
  new ExitPlanModeTool(this.agent),
@@ -102535,7 +103007,8 @@ function mapLoopEvent(event, turnId) {
102535
103007
  output: event.result.output,
102536
103008
  isError: event.result.isError,
102537
103009
  display: event.result.isError === true ? void 0 : event.result.display,
102538
- message: event.result.message
103010
+ message: event.result.message,
103011
+ nestedCalls: event.result.isError === true ? void 0 : event.result.nestedCalls
102539
103012
  };
102540
103013
  case "turn.interrupted":
102541
103014
  if (event.activeStep === void 0) return void 0;
@@ -103480,6 +103953,7 @@ var TurnFlow = class {
103480
103953
  return { continue: false };
103481
103954
  },
103482
103955
  prepareToolExecution: async (ctx) => {
103956
+ if (ctx.nested === true) return void 0;
103483
103957
  const cached = deduper.checkSameStep(ctx.toolCall.id, ctx.toolCall.name, ctx.args);
103484
103958
  if (cached !== null) return { syntheticResult: cached };
103485
103959
  if (ctx.toolCall.name === "Bash" && typeof ctx.args.command === "string") {
@@ -103495,9 +103969,9 @@ var TurnFlow = class {
103495
103969
  authorizeToolExecution: async (ctx) => {
103496
103970
  return this.agent.permission.beforeToolCall(ctx);
103497
103971
  },
103498
- onToolCallRejected: async ({ toolCallId, toolName, args, rawArguments }) => deduper.registerSkipped(toolCallId, toolName, args, rawArguments),
103972
+ onToolCallRejected: async ({ toolCallId, toolName, args, rawArguments, nested }) => nested === true ? void 0 : deduper.registerSkipped(toolCallId, toolName, args, rawArguments),
103499
103973
  finalizeToolResult: async (ctx) => {
103500
- const finalResult = await deduper.finalizeResult(ctx.toolCall.id, ctx.toolCall.name, ctx.args, ctx.result);
103974
+ const finalResult = ctx.nested === true ? ctx.result : await deduper.finalizeResult(ctx.toolCall.id, ctx.toolCall.name, ctx.args, ctx.result);
103501
103975
  const { isError, output } = finalResult;
103502
103976
  this.agent.sessionMemory.recordToolExecution(ctx.toolCall.name, summarizeToolArgs(ctx.args), isError === true, ctx.stepNumber);
103503
103977
  this.recordWorkingSetPaths(ctx.toolCall.name, ctx.args, Number(ctx.turnId));
@@ -104242,6 +104716,29 @@ var Agent = class {
104242
104716
  return this.rlmEnabled;
104243
104717
  }
104244
104718
  /**
104719
+ * Enables or disables the script execution mode (`undefined` toggles) and
104720
+ * returns the resulting state. On enable, the `RunScript` tool joins the
104721
+ * active tools; on disable it is removed.
104722
+ *
104723
+ * The main agent's profile enables `RunScript` by default, so the agent-level
104724
+ * default (not in the active list) only applies to callers that build their
104725
+ * own tool list. No dedicated enter/exit record is needed (unlike RLM): the
104726
+ * tool list itself is replayed from `tools.set_active_tools`, so resume
104727
+ * restores the state automatically.
104728
+ */
104729
+ setScriptEnabled(enabled) {
104730
+ const current = this.tools.getActiveTools();
104731
+ const next = enabled ?? !current.includes("RunScript");
104732
+ if (next) {
104733
+ if (!current.includes("RunScript")) this.tools.setActiveTools([...current, "RunScript"]);
104734
+ } else {
104735
+ const withoutScript = current.filter((name) => name !== "RunScript");
104736
+ if (withoutScript.length !== current.length) this.tools.setActiveTools(withoutScript);
104737
+ }
104738
+ this.emitStatusUpdated();
104739
+ return next;
104740
+ }
104741
+ /**
104245
104742
  * Lets a subagent inherit RLM mode from its parent at spawn time: mounts
104246
104743
  * the python tool (so the child can keep recursing) and records rlm.enter,
104247
104744
  * mirroring setRlmEnabled(true). The rlm.enter record keeps enter/exit
@@ -104494,6 +104991,7 @@ var Agent = class {
104494
104991
  setRlmEnabled: (payload) => {
104495
104992
  this.setRlmEnabled(payload.enabled);
104496
104993
  },
104994
+ setScriptEnabled: (payload) => this.setScriptEnabled(payload.enabled),
104497
104995
  setRlmMaxDepth: (payload) => {
104498
104996
  this.setRlmMaxDepth(payload.maxDepth);
104499
104997
  },
@@ -125954,6 +126452,9 @@ var SessionAPIImpl = class {
125954
126452
  setRlmEnabled({ agentId, ...payload }) {
125955
126453
  return this.getAgent(agentId).setRlmEnabled(payload);
125956
126454
  }
126455
+ setScriptEnabled({ agentId, ...payload }) {
126456
+ return this.getAgent(agentId).setScriptEnabled(payload);
126457
+ }
125957
126458
  setRlmMaxDepth({ agentId, ...payload }) {
125958
126459
  return this.getAgent(agentId).setRlmMaxDepth(payload);
125959
126460
  }
@@ -126965,6 +127466,9 @@ var ScreamCore = class {
126965
127466
  setRlmEnabled({ sessionId, ...payload }) {
126966
127467
  return this.sessionApi(sessionId).setRlmEnabled(payload);
126967
127468
  }
127469
+ setScriptEnabled({ sessionId, ...payload }) {
127470
+ return this.sessionApi(sessionId).setScriptEnabled(payload);
127471
+ }
126968
127472
  setRlmMaxDepth({ sessionId, ...payload }) {
126969
127473
  return this.sessionApi(sessionId).setRlmMaxDepth(payload);
126970
127474
  }
@@ -128045,6 +128549,13 @@ var SDKRpcClient = class {
128045
128549
  enabled: input.enabled
128046
128550
  });
128047
128551
  }
128552
+ async setScriptEnabled(input) {
128553
+ return (await this.getRpc()).setScriptEnabled({
128554
+ sessionId: input.sessionId,
128555
+ agentId: this.interactiveAgentId,
128556
+ enabled: input.enabled
128557
+ });
128558
+ }
128048
128559
  async setRlmMaxDepth(input) {
128049
128560
  return (await this.getRpc()).setRlmMaxDepth({
128050
128561
  sessionId: input.sessionId,
@@ -128501,6 +129012,20 @@ var Session = class {
128501
129012
  enabled
128502
129013
  });
128503
129014
  }
129015
+ /**
129016
+ * Enables or disables the `/script` execution mode (no argument toggles)
129017
+ * and resolves to the resulting state. When enabled, the `RunScript` tool
129018
+ * (QuickJS sandbox that batches tool calls) joins the active tools; when
129019
+ * disabled it is removed. The main agent starts with it enabled
129020
+ * (`profile/default/agent.yaml`).
129021
+ */
129022
+ async setScriptEnabled(enabled) {
129023
+ this.ensureOpen();
129024
+ return this.rpc.setScriptEnabled({
129025
+ sessionId: this.id,
129026
+ enabled
129027
+ });
129028
+ }
128504
129029
  /** Sets the maximum RLM recursion depth (default 1). Depth 1 means the root
128505
129030
  * kernel may spawn rlm() children but they cannot spawn grandchildren. */
128506
129031
  async setRlmMaxDepth(maxDepth) {
@@ -129674,6 +130199,13 @@ const BUILTIN_SLASH_COMMANDS = [
129674
130199
  priority: 215,
129675
130200
  availability: "always"
129676
130201
  },
130202
+ {
130203
+ name: "script",
130204
+ aliases: ["script"],
130205
+ description: "registry.script_desc",
130206
+ priority: 214,
130207
+ availability: "always"
130208
+ },
129677
130209
  {
129678
130210
  name: "rlm-max-depth",
129679
130211
  aliases: [],
@@ -135099,6 +135631,27 @@ async function handleRlmCommand(host, args) {
135099
135631
  host.showError(`Failed to set rlm mode: ${msg}`);
135100
135632
  }
135101
135633
  }
135634
+ /** /script [on|off] — toggle the script execution mode (sandboxed batch tool). */
135635
+ async function handleScriptCommand(host, args) {
135636
+ const session = host.session;
135637
+ if (session === void 0) {
135638
+ host.showError(getNoActiveSessionMessage());
135639
+ return;
135640
+ }
135641
+ const subcmd = args.trim().toLowerCase();
135642
+ if (subcmd !== "" && subcmd !== "on" && subcmd !== "off") {
135643
+ host.showError("Usage: /script [on|off]");
135644
+ return;
135645
+ }
135646
+ const requested = subcmd === "" ? void 0 : subcmd === "on";
135647
+ try {
135648
+ const enabled = await session.setScriptEnabled(requested);
135649
+ host.showNotice(t(enabled ? "config.script_on" : "config.script_off"));
135650
+ } catch (error) {
135651
+ const msg = formatErrorMessage(error);
135652
+ host.showError(`Failed to set script mode: ${msg}`);
135653
+ }
135654
+ }
135102
135655
  /** /rlm-max-depth [N] — query or set the maximum RLM recursion depth.
135103
135656
  * N=0 or no limit means unlimited (the default). */
135104
135657
  async function handleRlmMaxDepthCommand(host, args) {
@@ -136367,7 +136920,7 @@ async function guidedGoalSetup(host) {
136367
136920
  host.showNotice(t("goal.storm_breaker"), t("goal.conflict_loop"));
136368
136921
  return;
136369
136922
  }
136370
- const { TextInputDialogComponent } = await import("./text-input-dialog-DQaJf2zB.mjs");
136923
+ const { TextInputDialogComponent } = await import("./text-input-dialog-DcjQLJFz.mjs");
136371
136924
  const initialDesc = await promptText(host, TextInputDialogComponent, {
136372
136925
  title: t("goal.setup_title_initial"),
136373
136926
  subtitle: t("goal.setup_desc_hint"),
@@ -136388,7 +136941,7 @@ async function guidedGoalSetup(host) {
136388
136941
  await showGoalConfigWizard(host, session, confirmed.trim() || objective, false);
136389
136942
  }
136390
136943
  async function showGoalConfigWizard(host, session, objective, replace) {
136391
- const { TextInputDialogComponent } = await import("./text-input-dialog-DQaJf2zB.mjs");
136944
+ const { TextInputDialogComponent } = await import("./text-input-dialog-DcjQLJFz.mjs");
136392
136945
  const turnInput = await promptNumber(host, TextInputDialogComponent, {
136393
136946
  title: t("goal.wizard_title", { objective }),
136394
136947
  subtitle: t("goal.budget_turns_hint"),
@@ -137579,11 +138132,12 @@ const BRANCH_PIPE = " │ ";
137579
138132
  const TREE_PIPE_ROW = " │";
137580
138133
  const THINKING_BODY_PREFIX = " ";
137581
138134
  /**
137582
- * Extra indent for reasoning rows. Tool bodies carry their own three-cell
137583
- * line-number gutter (` 1 /**`), so reasoning text needs the same offset to
137584
- * land in one column with them instead of three cells to their left.
138135
+ * Extra indent shared by every body row of a block. File and diff bodies
138136
+ * reserve three cells for their line-number gutter (` 1 /**`), so tool
138137
+ * bodies, reasoning rows and hints all start at that same content column
138138
+ * instead of sitting three cells to its left.
137585
138139
  */
137586
- const THINKING_TEXT_INDENT = " ";
138140
+ const BODY_TEXT_INDENT = " ";
137587
138141
  /** Cells of a background task notice bullet (`⠋ ` / `✓ ` / `✗ `). */
137588
138142
  const NOTICE_BULLET_WIDTH = 2;
137589
138143
  const THROTTLE_MS = 200;
@@ -138053,7 +138607,7 @@ var ActivityGroupComponent = class extends Container {
138053
138607
  }
138054
138608
  if (hidden > 0) {
138055
138609
  const summary = t("activitygroup.segments_hidden", { count: String(hidden) });
138056
- this.bodyContainer.addChild(new Text(`${BRANCH_PIPE}${chalk.dim(summary)}`, 0, 0));
138610
+ this.bodyContainer.addChild(new Text(`${BRANCH_PIPE}${BODY_TEXT_INDENT}${chalk.dim(summary)}`, 0, 0));
138057
138611
  }
138058
138612
  }
138059
138613
  /** Renders one step of the timeline: its branch row plus its own body rows. */
@@ -138062,7 +138616,7 @@ var ActivityGroupComponent = class extends Container {
138062
138616
  const row = this.toolRow(segment.tc, width, getActivityLines().expandedTool, !this.running);
138063
138617
  const components = [new Text(`${isLast ? BRANCH_LAST : BRANCH_FIRST}${this.statusGlyph(row)} ${row.header}`, 0, 0)];
138064
138618
  const continuation = isLast ? THINKING_BODY_PREFIX : BRANCH_PIPE;
138065
- for (const line of row.body) components.push(new Text(`${continuation}${line}`, 0, 0));
138619
+ for (const line of row.body) components.push(new Text(`${continuation}${BODY_TEXT_INDENT}${line}`, 0, 0));
138066
138620
  return {
138067
138621
  components,
138068
138622
  cost: 1 + row.body.length
@@ -138111,7 +138665,7 @@ var ActivityGroupComponent = class extends Container {
138111
138665
  const components = [new Text(`${isLast ? BRANCH_LAST : BRANCH_FIRST}${this.thinkingGlyph(segment)}${chalk.hex(colors.roleThinking)(t("activitygroup.thinking_label"))}`, 0, 0)];
138112
138666
  let cost = 1;
138113
138667
  const budget = getActivityLines().expandedThinking;
138114
- const textPrefix = `${continuation}${THINKING_TEXT_INDENT}`;
138668
+ const textPrefix = `${continuation}${BODY_TEXT_INDENT}`;
138115
138669
  const inner = Math.max(1, width - visibleWidth(textPrefix));
138116
138670
  const height = (line) => new WrappedLine(textPrefix, textPrefix, line).render(inner).length;
138117
138671
  let selected = segment.live ? lines.slice(-budget) : lines.slice(0, budget);
@@ -148080,6 +148634,9 @@ async function handleBuiltInSlashCommand(host, name, args) {
148080
148634
  case "rlm":
148081
148635
  await handleRlmCommand(host, args);
148082
148636
  return;
148637
+ case "script":
148638
+ await handleScriptCommand(host, args);
148639
+ return;
148083
148640
  case "rlm-max-depth":
148084
148641
  await handleRlmMaxDepthCommand(host, args);
148085
148642
  return;
@@ -3,5 +3,5 @@ import { fileURLToPath as __cjsShimFileURLToPath } from 'node:url';
3
3
  import { dirname as __cjsShimDirname } from 'node:path';
4
4
  const __filename = __cjsShimFileURLToPath(import.meta.url);
5
5
  const __dirname = __cjsShimDirname(__filename);
6
- import { rn as handleConnectCommand } from "./dispatch-DCzx4R71.mjs";
6
+ import { rn as handleConnectCommand } from "./dispatch-DFFuylzQ.mjs";
7
7
  export { handleConnectCommand };
package/dist/main.mjs CHANGED
@@ -6,7 +6,7 @@ const __dirname = __cjsShimDirname(__filename);
6
6
  import "./suppress-sqlite-warning-C2VB0doZ.mjs";
7
7
  //#region src/main.ts
8
8
  try {
9
- (await import("./app-CujwBxt8.mjs")).main();
9
+ (await import("./app-y8MJHp3H.mjs")).main();
10
10
  } catch (error) {
11
11
  process.stderr.write(`${error instanceof Error ? error.stack ?? error.message : String(error)}\n`);
12
12
  process.exit(1);
@@ -1171,6 +1171,8 @@ const dictionaries = {
1171
1171
  "config.ask_already_on": "ASK 模式已开启",
1172
1172
  "config.ask_already_off": "ASK 模式已关闭",
1173
1173
  "config.rlm_on": "RLM 模式已开启 — 持久化 Python 内核可用",
1174
+ "config.script_on": "脚本模式已开启 — RunScript 可在沙箱中批量调用工具",
1175
+ "config.script_off": "脚本模式已关闭 — RunScript 已从工具中移除",
1174
1176
  "config.rlm_off": "RLM 模式已关闭",
1175
1177
  "config.rlm_usage": "用法: /rlm (切换) | /rlm on | /rlm off",
1176
1178
  "config.auto_already_on": "自动模式已开启",
@@ -1221,6 +1223,7 @@ const dictionaries = {
1221
1223
  "registry.bot_desc": "切换无人值守 Bot 模式(bot)",
1222
1224
  "registry.ask_desc": "切换至只读问答模式(ask)",
1223
1225
  "registry.rlm_desc": "RLM 模式:面向长任务的持久 Python 工作环境,支持无限递归子代理(/rlm 切换开关)",
1226
+ "registry.script_desc": "脚本模式:在沙箱中运行 JavaScript 批量调用工具(/script 切换)",
1224
1227
  "registry.rlm_max_depth_desc": "设置 RLM 递归深度上限(/rlm-max-depth N)",
1225
1228
  "registry.wolfpack_desc": "切换群狼协作模式,自动批准+批量并发",
1226
1229
  "registry.sessions_desc": "浏览并恢复会话",
@@ -2478,6 +2481,8 @@ const dictionaries = {
2478
2481
  "config.ask_already_on": "ASK mode is already on",
2479
2482
  "config.ask_already_off": "ASK mode is already off",
2480
2483
  "config.rlm_on": "RLM mode enabled — persistent python kernel available",
2484
+ "config.script_on": "Script mode enabled — RunScript can batch tool calls from a sandbox",
2485
+ "config.script_off": "Script mode disabled — RunScript removed from the tools",
2481
2486
  "config.rlm_off": "RLM mode disabled",
2482
2487
  "config.rlm_usage": "Usage: /rlm (toggle) | /rlm on | /rlm off",
2483
2488
  "config.auto_already_on": "Auto mode is already on",
@@ -2528,6 +2533,7 @@ const dictionaries = {
2528
2533
  "registry.bot_desc": "Toggle unattended bot mode (bot)",
2529
2534
  "registry.ask_desc": "Toggle read-only Q&A mode (ask)",
2530
2535
  "registry.rlm_desc": "RLM mode: a persistent Python workspace for long-running tasks with unlimited recursive subagents (/rlm toggles)",
2536
+ "registry.script_desc": "Script mode: run JavaScript in a sandbox that batches tool calls (/script toggles)",
2531
2537
  "registry.rlm_max_depth_desc": "Set RLM recursion depth limit (/rlm-max-depth N)",
2532
2538
  "registry.wolfpack_desc": "Toggle wolfpack mode, auto-approve + batch concurrency",
2533
2539
  "registry.sessions_desc": "Browse and restore sessions",
@@ -3,5 +3,5 @@ import { fileURLToPath as __cjsShimFileURLToPath } from 'node:url';
3
3
  import { dirname as __cjsShimDirname } from 'node:path';
4
4
  const __filename = __cjsShimFileURLToPath(import.meta.url);
5
5
  const __dirname = __cjsShimDirname(__filename);
6
- import { t as TextInputDialogComponent } from "./text-input-dialog-BoxTq4a3.mjs";
6
+ import { t as TextInputDialogComponent } from "./text-input-dialog-CD0sakGq.mjs";
7
7
  export { TextInputDialogComponent };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scream-code",
3
- "version": "0.17.5",
3
+ "version": "0.17.6",
4
4
  "description": "A terminal-native AI agent for builders",
5
5
  "license": "MIT",
6
6
  "author": "ScreamCli",
@@ -55,6 +55,7 @@
55
55
  "smoke": "node dist/main.mjs --version"
56
56
  },
57
57
  "dependencies": {
58
+ "@earendil-works/pi-codemode": "0.99.1",
58
59
  "@fontsource-variable/inter": "^5.3.0",
59
60
  "@fontsource-variable/jetbrains-mono": "^5.3.0",
60
61
  "@liutod-scream/pi-tui": "^0.87.1",