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,9 +4,9 @@ import { dirname as __cjsShimDirname } from 'node:path';
4
4
  const __filename = __cjsShimFileURLToPath(import.meta.url);
5
5
  const __dirname = __cjsShimDirname(__filename);
6
6
  import "./suppress-sqlite-warning-C2VB0doZ.mjs";
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-B8IVXCrX.mjs";
9
- import { a as setLocale, i as getLocale, o as t } from "./text-input-dialog-CD0sakGq.mjs";
7
+ import { A as sharedMemoStore } from "./src-C7C9e5Ds.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 flushDiagnosticLogs, d as isPlanExpandable, dn as isTodoItemShape, dr as isScreamError, en as SubagentSlots, er as normalizeWorkDir, f as MoonLoader, fn as parseStreamingArgs, fr as isOrphanedToolCallError, 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 log, mn as stringValue, mr as SCREAM_ERROR_INFO, mt as handlePlanCommand, n as clearEvalPanelState, nr as readSessionIndex, on as STATUS_BULLET, or as parseRlmMaxDepthArg, ot as changeThinkingLevel, p as formatMemoryMemoForInjection, pn as serializeToolResultOutput, pr as ErrorCodes, 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 resolveScreamHome, st as getModelCycleLevel, t as dispatchInput, tr as appendSessionIndexEntry, u as isExpandable, un as formatErrorMessage, ur as resolveGlobalLogPath, 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-DfHVLpfA.mjs";
9
+ import { a as setLocale, i as getLocale, o as t } from "./text-input-dialog-CK6jTftM.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.8"),
36
+ version: optionalBuildString("0.18.0"),
37
37
  channel: optionalBuildString(""),
38
38
  commit: optionalBuildString(""),
39
39
  buildTarget: optionalBuildString("darwin-arm64")
@@ -2171,6 +2171,211 @@ function renderChildRequestNotice(fields, name) {
2171
2171
  };
2172
2172
  }
2173
2173
  //#endregion
2174
+ //#region src/tui/utils/subagent-instances.ts
2175
+ /**
2176
+ * agent-core synthesizes `rlm_<timestamp>_<rand>` parent tool-call ids for
2177
+ * rlm() spawns (packages/agent-core/src/agent/tool/index.ts:374), so no
2178
+ * tool-call row ever exists for them — the id shape is the only RLM marker
2179
+ * available to the TUI. A renamed prefix degrades to the generic "unknown"
2180
+ * attribution, never to a wrong one.
2181
+ */
2182
+ const RLM_TOOL_CALL_PREFIX = "rlm_";
2183
+ /** Guard against a corrupt or cyclic parent chain hanging the render path. */
2184
+ const MAX_CHAIN_DEPTH = 16;
2185
+ function descriptionFromToolCall(call) {
2186
+ const fromArgs = call?.args["description"];
2187
+ if (typeof call?.description === "string" && call.description.length > 0) return call.description;
2188
+ return typeof fromArgs === "string" && fromArgs.length > 0 ? fromArgs : void 0;
2189
+ }
2190
+ /** Snapshot a spawn event (+ the spawning tool call, still in flight at that
2191
+ * moment) into a registry record. `spawnedAt` is passed in so a resume keeps
2192
+ * the instance's original start time. */
2193
+ function createSubagentInstanceInfo(event, parentToolCall, spawnedAt = Date.now()) {
2194
+ return {
2195
+ agentId: event.subagentId,
2196
+ type: event.subagentName,
2197
+ description: event.description,
2198
+ parentAgentId: event.parentAgentId,
2199
+ parentToolCallId: event.parentToolCallId,
2200
+ parentToolName: parentToolCall?.name,
2201
+ parentToolDescription: descriptionFromToolCall(parentToolCall),
2202
+ spawnedAt
2203
+ };
2204
+ }
2205
+ /** Close a registry record on subagent.completed / subagent.failed. */
2206
+ function withSubagentInstanceEnded(info, outcome, endedAt = Date.now()) {
2207
+ return {
2208
+ ...info,
2209
+ endedAt,
2210
+ outcome
2211
+ };
2212
+ }
2213
+ /** Walk `parentAgentId` upwards, collecting parent types (nearest first). */
2214
+ function buildAncestorChain(instance, instances) {
2215
+ const names = [];
2216
+ const seen = new Set([instance.agentId]);
2217
+ let parentId = instance.parentAgentId;
2218
+ while (parentId !== void 0 && parentId !== "main" && parentId.length > 0) {
2219
+ if (seen.has(parentId) || names.length >= MAX_CHAIN_DEPTH) return {
2220
+ names,
2221
+ truncated: true
2222
+ };
2223
+ seen.add(parentId);
2224
+ const parent = instances.get(parentId);
2225
+ if (parent === void 0) return {
2226
+ names,
2227
+ truncated: true
2228
+ };
2229
+ names.push(parent.type);
2230
+ parentId = parent.parentAgentId;
2231
+ }
2232
+ return {
2233
+ names,
2234
+ truncated: false
2235
+ };
2236
+ }
2237
+ /** Best-effort "who triggered this instance" resolution. */
2238
+ function resolveSource(instance, instances) {
2239
+ const parentId = instance.parentAgentId;
2240
+ const parentInstance = parentId !== void 0 && parentId !== "main" ? instances.get(parentId) : void 0;
2241
+ const isRlm = instance.parentToolCallId.startsWith(RLM_TOOL_CALL_PREFIX);
2242
+ if (parentInstance !== void 0) return {
2243
+ kind: isRlm ? "rlm" : "agent",
2244
+ name: parentInstance.type,
2245
+ description: parentInstance.description
2246
+ };
2247
+ if (isRlm) return {
2248
+ kind: "rlm",
2249
+ name: void 0,
2250
+ description: instance.description
2251
+ };
2252
+ if (instance.parentToolName !== void 0) return {
2253
+ kind: "tool",
2254
+ name: instance.parentToolName,
2255
+ description: instance.parentToolDescription
2256
+ };
2257
+ if (parentId === void 0 || parentId === "main") return {
2258
+ kind: "main",
2259
+ name: void 0,
2260
+ description: void 0
2261
+ };
2262
+ return {
2263
+ kind: "unknown",
2264
+ name: void 0,
2265
+ description: void 0
2266
+ };
2267
+ }
2268
+ /**
2269
+ * Map the slot snapshot + instance registry into Agents-view rows.
2270
+ *
2271
+ * One row per slot that is live or has any instance history in this session
2272
+ * (never-used slots stay out), plus one row per *registry-only* type: past
2273
+ * `MAX_SUBAGENT_SLOTS` the slot machine returns a detached slot that never
2274
+ * reaches `slots`, so the 17th+ type would otherwise spawn and finish
2275
+ * invisibly while the registry still records every instance. A synthesized
2276
+ * row is never live, takes its status from the type's latest instance
2277
+ * (`completed` / `failed`, else `idle` when nothing terminal was recorded),
2278
+ * counts every recorded instance of the type and has no `instanceId`; its
2279
+ * source and ancestors use the same derivation as slot-backed rows. Live rows
2280
+ * sort first (most recent activity on top), then ended ones by their end time.
2281
+ *
2282
+ * `recent` is the archive ring (newest last): rows derived when an instance
2283
+ * closed. A type the live snapshot no longer covers keeps its archived row, so
2284
+ * splitting the registry into live + ring does not change what the view shows
2285
+ * for anything inside the ring window. Called without `recent` (as before the
2286
+ * ring existed) the output depends on `slots` + `instances` alone.
2287
+ */
2288
+ function buildAgentRows(slots, instances, recent = []) {
2289
+ const byType = /* @__PURE__ */ new Map();
2290
+ for (const info of instances.values()) {
2291
+ const aggregate = byType.get(info.type);
2292
+ if (aggregate === void 0) {
2293
+ byType.set(info.type, {
2294
+ latest: info,
2295
+ count: 1
2296
+ });
2297
+ continue;
2298
+ }
2299
+ aggregate.count += 1;
2300
+ if (info.spawnedAt >= aggregate.latest.spawnedAt) aggregate.latest = info;
2301
+ }
2302
+ const rows = [];
2303
+ const slottedTypes = /* @__PURE__ */ new Set();
2304
+ for (const slot of slots) {
2305
+ slottedTypes.add(slot.type);
2306
+ const latest = byType.get(slot.type)?.latest;
2307
+ const live = slot.count > 0 || slot.status !== "idle";
2308
+ if (!live && latest === void 0) continue;
2309
+ const current = (slot.agentId === void 0 ? void 0 : instances.get(slot.agentId)) ?? latest;
2310
+ const status = live ? slot.status : latest?.outcome ?? "idle";
2311
+ const chain = current === void 0 ? {
2312
+ names: [],
2313
+ truncated: false
2314
+ } : buildAncestorChain(current, instances);
2315
+ const row = {
2316
+ key: slot.type,
2317
+ type: slot.type,
2318
+ status,
2319
+ live,
2320
+ count: slot.count,
2321
+ detail: slot.detail,
2322
+ lastActivityAt: slot.lastActivityAt,
2323
+ description: current?.description,
2324
+ instanceId: live ? slot.agentId : void 0,
2325
+ source: current === void 0 ? {
2326
+ kind: "unknown",
2327
+ name: void 0,
2328
+ description: void 0
2329
+ } : resolveSource(current, instances),
2330
+ ancestors: chain.names,
2331
+ chainTruncated: chain.truncated
2332
+ };
2333
+ const sortAt = live ? slot.lastActivityAt : latest?.endedAt ?? latest?.spawnedAt ?? slot.lastActivityAt;
2334
+ rows.push({
2335
+ row,
2336
+ sortAt
2337
+ });
2338
+ }
2339
+ for (const [type, aggregate] of byType) {
2340
+ if (slottedTypes.has(type)) continue;
2341
+ const { latest } = aggregate;
2342
+ const activityAt = latest.endedAt ?? latest.spawnedAt;
2343
+ const chain = buildAncestorChain(latest, instances);
2344
+ rows.push({
2345
+ row: {
2346
+ key: type,
2347
+ type,
2348
+ status: latest.outcome ?? "idle",
2349
+ live: false,
2350
+ count: aggregate.count,
2351
+ detail: void 0,
2352
+ lastActivityAt: activityAt,
2353
+ description: latest.description,
2354
+ instanceId: void 0,
2355
+ source: resolveSource(latest, instances),
2356
+ ancestors: chain.names,
2357
+ chainTruncated: chain.truncated
2358
+ },
2359
+ sortAt: activityAt
2360
+ });
2361
+ }
2362
+ const coveredTypes = new Set(rows.map((entry) => entry.row.key));
2363
+ const archivedByType = /* @__PURE__ */ new Map();
2364
+ for (const row of recent) archivedByType.set(row.key, row);
2365
+ for (const row of archivedByType.values()) {
2366
+ if (coveredTypes.has(row.key)) continue;
2367
+ rows.push({
2368
+ row,
2369
+ sortAt: row.lastActivityAt
2370
+ });
2371
+ }
2372
+ rows.sort((a, b) => {
2373
+ if (a.row.live !== b.row.live) return a.row.live ? -1 : 1;
2374
+ return b.sortAt - a.sortAt;
2375
+ });
2376
+ return rows.map((entry) => entry.row);
2377
+ }
2378
+ //#endregion
2174
2379
  //#region src/tui/utils/background-task-status.ts
2175
2380
  const MAX_DETAIL_LENGTH = 240;
2176
2381
  function truncate(value) {
@@ -2505,6 +2710,17 @@ function subtractTokenUsage(current, previous) {
2505
2710
  function isZeroUsage(u) {
2506
2711
  return u.inputOther === 0 && u.inputCacheRead === 0 && u.inputCacheCreation === 0 && u.output === 0;
2507
2712
  }
2713
+ /**
2714
+ * Whether a status event may patch main-agent state (RLM mode / recursion cap).
2715
+ * Only the main agent's own events qualify; subagent status updates carry their
2716
+ * own agentId and must never overwrite the main display. Events without an
2717
+ * agentId (older emitters) are accepted for backward compatibility.
2718
+ *
2719
+ * Exported (pure) so the filtering rule is unit-testable without a TUI.
2720
+ */
2721
+ function isMainAgentStatusEvent(event) {
2722
+ return event.agentId === void 0 || event.agentId === "main";
2723
+ }
2508
2724
  var SessionEventHandler = class {
2509
2725
  host;
2510
2726
  constructor(host) {
@@ -2531,6 +2747,22 @@ var SessionEventHandler = class {
2531
2747
  subagentInfo = /* @__PURE__ */ new Map();
2532
2748
  /** Sidebar subagent slot state machine (see utils/subagent-slots.ts). */
2533
2749
  subagentSlots = new SubagentSlots();
2750
+ /**
2751
+ * Per-instance provenance registry (see utils/subagent-instances.ts), read
2752
+ * by the /tasks browser's Agents view. Captured at spawn because the two
2753
+ * clues stop being available later: `subagentInfo.parentToolCallId` is
2754
+ * rewritten to a WolfPack routing id for WolfPack children, and the
2755
+ * spawning tool call leaves the active set as soon as it returns.
2756
+ */
2757
+ subagentInstances = /* @__PURE__ */ new Map();
2758
+ /**
2759
+ * Archive ring behind {@link getRecentSubagentInstances}: the last derived
2760
+ * Agents-view row of every closed instance, oldest first, capped at
2761
+ * {@link MAX_RECENT_SUBAGENT_INSTANCES}. Closing an instance releases its
2762
+ * registry record, so this is what keeps recently-ended agents visible in the
2763
+ * /tasks Agents view instead of letting the registry grow for the session.
2764
+ */
2765
+ recentSubagentInstances = [];
2534
2766
  /** Tool callId of the in-flight UpdateGoal(complete) (judging), if any. */
2535
2767
  goalJudgeCallId;
2536
2768
  /** Rolling first-token latencies of real requests, feeding the sidebar Hub
@@ -2551,6 +2783,8 @@ var SessionEventHandler = class {
2551
2783
  this.backgroundTaskTranscriptedTerminal.clear();
2552
2784
  this.subagentInfo.clear();
2553
2785
  this.subagentSlots.reset();
2786
+ this.subagentInstances.clear();
2787
+ this.recentSubagentInstances.length = 0;
2554
2788
  this.renderedSkillActivationIds.clear();
2555
2789
  this.renderedMcpServerStatusKeys.clear();
2556
2790
  this.stopAllMcpServerStatusSpinners();
@@ -2733,6 +2967,50 @@ var SessionEventHandler = class {
2733
2967
  getSubagentSlots() {
2734
2968
  return this.subagentSlots.getSlots();
2735
2969
  }
2970
+ /** Read-only snapshot of the per-instance provenance registry (Agents view
2971
+ * of the /tasks browser). Keyed by subagent agentId. */
2972
+ getSubagentInstances() {
2973
+ return this.subagentInstances;
2974
+ }
2975
+ /** Read-only snapshot of the archive ring: the last derived Agents-view row
2976
+ * of each closed instance, oldest first, bounded to
2977
+ * {@link MAX_RECENT_SUBAGENT_INSTANCES}. */
2978
+ getRecentSubagentInstances() {
2979
+ return this.recentSubagentInstances;
2980
+ }
2981
+ /** Remember (or refresh, on resume) one spawned instance. The spawning tool
2982
+ * call is resolved here, while it is still in flight. */
2983
+ rememberSubagentInstance(event, fallbackParentAgentId) {
2984
+ const previous = this.subagentInstances.get(event.subagentId);
2985
+ const parentToolCall = this.host.streamingUI.getActiveToolCall(event.parentToolCallId);
2986
+ this.subagentInstances.set(event.subagentId, createSubagentInstanceInfo({
2987
+ ...event,
2988
+ parentAgentId: event.parentAgentId ?? fallbackParentAgentId
2989
+ }, parentToolCall, previous?.spawnedAt));
2990
+ }
2991
+ /**
2992
+ * Close an instance record on subagent.completed / subagent.failed: archive
2993
+ * its last display row in the ring, then release the registry record — the
2994
+ * registry is reset per session, not kept per spawned agent.
2995
+ *
2996
+ * The archived row is derived by the very builder the view uses (over the
2997
+ * registry snapshot that still contains this instance and its terminal
2998
+ * outcome), so it is field-identical to what the view rendered while the
2999
+ * record was live; the deletion below is what makes that derivation the last
3000
+ * chance to produce it.
3001
+ */
3002
+ closeSubagentInstance(agentId, outcome) {
3003
+ const info = this.subagentInstances.get(agentId);
3004
+ if (info === void 0) return;
3005
+ const ended = withSubagentInstanceEnded(info, outcome);
3006
+ const snapshot = new Map([...this.subagentInstances, [agentId, ended]]);
3007
+ const row = buildAgentRows(this.subagentSlots.getSlots(), snapshot).find((candidate) => candidate.key === ended.type);
3008
+ if (row !== void 0) {
3009
+ this.recentSubagentInstances.push(row);
3010
+ if (this.recentSubagentInstances.length > 50) this.recentSubagentInstances.shift();
3011
+ }
3012
+ this.subagentInstances.delete(agentId);
3013
+ }
2736
3014
  /** Keep the last few first-token latencies, per provider. Steps without a
2737
3015
  * measured latency (no stream, cached turn) are ignored rather than counted
2738
3016
  * as a zero. */
@@ -2773,6 +3051,7 @@ var SessionEventHandler = class {
2773
3051
  switch (event.type) {
2774
3052
  case "subagent.spawned":
2775
3053
  this.subagentSlots.onSpawned(event.subagentId, event.subagentName, event.description);
3054
+ this.rememberSubagentInstance(event, agentId);
2776
3055
  break;
2777
3056
  case "subagent.started":
2778
3057
  this.subagentSlots.onStarted(event.subagentId);
@@ -2780,6 +3059,7 @@ var SessionEventHandler = class {
2780
3059
  case "subagent.completed":
2781
3060
  case "subagent.failed":
2782
3061
  this.subagentSlots.onTerminated(event.subagentId);
3062
+ this.closeSubagentInstance(event.subagentId, event.type === "subagent.failed" ? "failed" : "completed");
2783
3063
  break;
2784
3064
  case "tool.call.started":
2785
3065
  if (event.name === "ContactParent") {
@@ -3170,7 +3450,11 @@ var SessionEventHandler = class {
3170
3450
  if (event.maxContextTokens !== void 0) patch.maxContextTokens = event.maxContextTokens;
3171
3451
  if (event.planMode !== void 0) patch.planMode = event.planMode ? event.planStrategy === "fusion" ? "fusionplan" : "plan" : "off";
3172
3452
  if (event.wolfpackMode !== void 0) patch.wolfpackMode = event.wolfpackMode;
3173
- if (event.rlmEnabled !== void 0) patch.rlmEnabled = event.rlmEnabled;
3453
+ if (event.rlmEnabled !== void 0 || event.rlmMaxDepth !== void 0) {
3454
+ const fromMain = isMainAgentStatusEvent(event);
3455
+ if (fromMain && event.rlmEnabled !== void 0) patch.rlmEnabled = event.rlmEnabled;
3456
+ if (fromMain && event.rlmMaxDepth !== void 0) patch.rlmMaxDepth = event.rlmMaxDepth;
3457
+ }
3174
3458
  if (event.permission !== void 0) patch.permissionMode = event.permission;
3175
3459
  if (Object.keys(patch).length > 0) this.host.setAppState(patch);
3176
3460
  }
@@ -3422,6 +3706,7 @@ var SessionEventHandler = class {
3422
3706
  }
3423
3707
  handleSubagentSpawned(event) {
3424
3708
  this.subagentSlots.onSpawned(event.subagentId, event.subagentName, event.description);
3709
+ this.rememberSubagentInstance(event);
3425
3710
  const { streamingUI } = this.host;
3426
3711
  if (streamingUI.getActiveToolCall(event.parentToolCallId)?.name === "WolfPack") {
3427
3712
  const routingId = event.subagentId;
@@ -3495,6 +3780,7 @@ var SessionEventHandler = class {
3495
3780
  }
3496
3781
  handleSubagentCompleted(event) {
3497
3782
  this.subagentSlots.onTerminated(event.subagentId);
3783
+ this.closeSubagentInstance(event.subagentId, "completed");
3498
3784
  this.recordSubagentUsage(event.subagentId, void 0, event.usage);
3499
3785
  const { streamingUI } = this.host;
3500
3786
  const backgroundMeta = this.backgroundAgentMetadata.get(event.subagentId);
@@ -3525,6 +3811,7 @@ var SessionEventHandler = class {
3525
3811
  }
3526
3812
  handleSubagentFailed(event) {
3527
3813
  this.subagentSlots.onTerminated(event.subagentId);
3814
+ this.closeSubagentInstance(event.subagentId, "failed");
3528
3815
  this.recordSubagentUsage(event.subagentId, void 0, event.usage);
3529
3816
  const { streamingUI } = this.host;
3530
3817
  const backgroundMeta = this.backgroundAgentMetadata.get(event.subagentId);
@@ -3919,6 +4206,7 @@ var SessionReplayRenderer = class {
3919
4206
  this.host = host;
3920
4207
  }
3921
4208
  async hydrateFromReplay(session) {
4209
+ this.host.state.ui.resetTextSelection();
3922
4210
  this.host.setAppState({ isReplaying: true });
3923
4211
  try {
3924
4212
  const main = session.getResumeState()?.agents["main"];
@@ -4380,6 +4668,89 @@ function isControlCharacter(ch) {
4380
4668
  const code = ch.codePointAt(0) ?? 0;
4381
4669
  return code >= 0 && code <= 31 || code >= 127 && code <= 159;
4382
4670
  }
4671
+ /** Max characters kept across head + tail of the preview combined. */
4672
+ const UI_OUTPUT_MAX_CHARS = 8e3;
4673
+ /** Same marker string as agent-core's `result-builder.ts` TRUNCATION_MARKER. */
4674
+ const TRUNCATION_MARKER = "[...truncated]";
4675
+ function elisionMarker(elided) {
4676
+ return `[…${String(elided)} lines elided…]`;
4677
+ }
4678
+ /**
4679
+ * Bounds one tool-output string to a head + tail preview:
4680
+ * - at most {@link UI_OUTPUT_MAX_HEAD_LINES} lines from the start and
4681
+ * {@link UI_OUTPUT_MAX_TAIL_LINES} lines from the end (the middle is
4682
+ * replaced with `[…N lines elided…]`);
4683
+ * - the kept head + tail text stays within {@link UI_OUTPUT_MAX_CHARS} chars,
4684
+ * dropping whole lines first and hard-slicing a lone oversized line last
4685
+ * (marked `[...truncated]`).
4686
+ *
4687
+ * Returns the input unchanged when it already fits both budgets.
4688
+ */
4689
+ function boundToolOutput(output) {
4690
+ const lines = output.split("\n");
4691
+ const totalLines = lines.length;
4692
+ const overflowLines = totalLines > 80;
4693
+ const overflowChars = output.length > UI_OUTPUT_MAX_CHARS;
4694
+ if (!overflowLines && !overflowChars) return output;
4695
+ const head = lines.slice(0, 40);
4696
+ const tail = overflowLines ? lines.slice(totalLines - 40) : lines.slice(40);
4697
+ let elided = totalLines - head.length - tail.length;
4698
+ const contentBudget = UI_OUTPUT_MAX_CHARS - 2;
4699
+ let headText = head.join("\n");
4700
+ let tailText = tail.join("\n");
4701
+ while (headText.length + tailText.length > contentBudget) {
4702
+ if (head.length > 1 && (tail.length <= 1 || headText.length >= tailText.length)) {
4703
+ head.pop();
4704
+ headText = head.join("\n");
4705
+ elided += 1;
4706
+ continue;
4707
+ }
4708
+ if (tail.length > 1) {
4709
+ tail.shift();
4710
+ tailText = tail.join("\n");
4711
+ elided += 1;
4712
+ continue;
4713
+ }
4714
+ break;
4715
+ }
4716
+ if (headText.length + tailText.length > contentBudget) {
4717
+ const tailKeep = Math.min(tailText.length, Math.floor(contentBudget / 2));
4718
+ const headKeep = Math.min(headText.length, contentBudget - tailKeep);
4719
+ headText = headText.slice(0, headKeep);
4720
+ tailText = tailText.slice(tailText.length - tailKeep);
4721
+ }
4722
+ const marker = elided > 0 ? elisionMarker(elided) : TRUNCATION_MARKER;
4723
+ const parts = [];
4724
+ if (headText.length > 0) parts.push(headText);
4725
+ parts.push(marker);
4726
+ if (tailText.length > 0) parts.push(tailText);
4727
+ return parts.join("\n");
4728
+ }
4729
+ /**
4730
+ * Returns a transcript entry safe to keep in `state.transcriptEntries`:
4731
+ * tool-group entries get their `result.output` replaced with the bounded
4732
+ * preview (a copy — message, brief, titles, args and every other field stay
4733
+ * as they are). Non-tool entries — and tool entries already within budget —
4734
+ * come back untouched (no copy).
4735
+ */
4736
+ function boundEntryForUi(entry) {
4737
+ const toolCallData = entry.toolCallData;
4738
+ if (toolCallData === void 0) return entry;
4739
+ const result = toolCallData.result;
4740
+ if (result === void 0) return entry;
4741
+ const boundedOutput = boundToolOutput(result.output);
4742
+ if (boundedOutput === result.output) return entry;
4743
+ return {
4744
+ ...entry,
4745
+ toolCallData: {
4746
+ ...toolCallData,
4747
+ result: {
4748
+ ...result,
4749
+ output: boundedOutput
4750
+ }
4751
+ }
4752
+ };
4753
+ }
4383
4754
  //#endregion
4384
4755
  //#region src/tui/controllers/streaming-ui.ts
4385
4756
  var StreamingUIController = class StreamingUIController {
@@ -5139,7 +5510,16 @@ var StreamingUIController = class StreamingUIController {
5139
5510
  this.host.transcriptController.unmarkPending(tc);
5140
5511
  this._pendingToolComponents.delete(toolCallId);
5141
5512
  const entry = this.host.state.transcriptEntries.find((e) => e.toolCallData?.id === toolCallId);
5142
- if (entry?.toolCallData !== void 0) entry.toolCallData.result = result;
5513
+ if (entry?.toolCallData !== void 0) {
5514
+ const bounded = boundEntryForUi({
5515
+ ...entry,
5516
+ toolCallData: {
5517
+ ...entry.toolCallData,
5518
+ result
5519
+ }
5520
+ });
5521
+ entry.toolCallData.result = bounded.toolCallData?.result ?? result;
5522
+ }
5143
5523
  this.host.transcriptController.commit();
5144
5524
  state.ui.requestRender();
5145
5525
  return;
@@ -5538,6 +5918,64 @@ function statusColor(colors, status) {
5538
5918
  function isTerminal(status) {
5539
5919
  return status === "completed" || status === "failed" || status === "killed" || status === "lost";
5540
5920
  }
5921
+ /** Width of the agent-name column in the Agents list (leaves room for the
5922
+ * status word, the instance count and an activity hint on narrow panes). */
5923
+ const AGENT_NAME_COLS = 12;
5924
+ /** Localized word per agent row status: the six live slot states plus the two
5925
+ * terminal outcomes the instance registry can report. */
5926
+ function agentStatusText(status) {
5927
+ switch (status) {
5928
+ case "idle": return t("sidebar.agent_idle");
5929
+ case "working": return t("sidebar.agent_working");
5930
+ case "outputting": return t("sidebar.agent_outputting");
5931
+ case "messaging": return t("sidebar.agent_messaging");
5932
+ case "reworking": return t("sidebar.agent_reworking");
5933
+ case "requesting": return t("sidebar.agent_requesting");
5934
+ case "completed": return t("taskbrowser.agents_completed");
5935
+ case "failed": return t("taskbrowser.agents_failed");
5936
+ }
5937
+ }
5938
+ function agentStatusColor(colors, status) {
5939
+ switch (status) {
5940
+ case "working":
5941
+ case "outputting": return colors.success;
5942
+ case "messaging": return colors.accent;
5943
+ case "reworking":
5944
+ case "requesting": return colors.warning;
5945
+ case "completed": return colors.textMuted;
5946
+ case "failed": return colors.error;
5947
+ case "idle": return colors.textDim;
5948
+ }
5949
+ }
5950
+ /** "Who triggered this agent" line. Each kind names its own evidence, and a
5951
+ * missing clue is voiced as unknown instead of guessed. */
5952
+ function formatAgentSource(source) {
5953
+ switch (source.kind) {
5954
+ case "tool":
5955
+ if (source.name === void 0) return t("taskbrowser.agent_source_unknown");
5956
+ return source.description === void 0 ? source.name : `${source.name} · ${source.description}`;
5957
+ case "agent": {
5958
+ const via = source.name === void 0 ? t("taskbrowser.agent_source_unknown") : t("taskbrowser.agent_source_via", { name: source.name });
5959
+ return source.description === void 0 ? via : `${via} · ${source.description}`;
5960
+ }
5961
+ case "rlm": {
5962
+ const rlm = t("taskbrowser.agent_source_rlm");
5963
+ return source.name === void 0 ? rlm : `${rlm} · ${t("taskbrowser.agent_source_via", { name: source.name })}`;
5964
+ }
5965
+ case "main": return t("taskbrowser.agent_source_main");
5966
+ case "unknown": return t("taskbrowser.agent_source_unknown");
5967
+ }
5968
+ }
5969
+ /** Full spawn chain, root first: `main → researcher → coder`, `…` when the
5970
+ * walk stopped early (missing link / cycle / depth cap). */
5971
+ function formatAgentChain(agent) {
5972
+ const parents = [...agent.ancestors].toReversed();
5973
+ return `${[
5974
+ t("taskbrowser.agent_main"),
5975
+ ...parents,
5976
+ agent.type
5977
+ ].join(" → ")}${agent.chainTruncated ? " → …" : ""}`;
5978
+ }
5541
5979
  function formatRelativeTime$2(ts) {
5542
5980
  if (ts === null || ts === void 0 || !Number.isFinite(ts) || ts <= 0) return "";
5543
5981
  const diffSec = Math.floor(Math.max(0, Date.now() - ts) / 1e3);
@@ -5565,8 +6003,22 @@ function fitExactly$1(line, width) {
5565
6003
  return padToWidth$1(s, width);
5566
6004
  }
5567
6005
  function visibleTasks(tasks, filter) {
5568
- if (filter === "all") return [...tasks];
5569
- return tasks.filter((t) => !isTerminal(t.status));
6006
+ if (filter === "active") return tasks.filter((t) => !isTerminal(t.status));
6007
+ return [...tasks];
6008
+ }
6009
+ /**
6010
+ * Keep `selectedIndex` inside the window `scrollOffset … scrollOffset+rows-1`,
6011
+ * clamped to the list bounds. Shared by the task list and the agent list.
6012
+ */
6013
+ function clampListScroll(selectedIndex, scrollOffset, total, visibleRows) {
6014
+ if (visibleRows <= 0) return 0;
6015
+ let next = scrollOffset;
6016
+ if (selectedIndex < next) next = selectedIndex;
6017
+ else if (selectedIndex >= next + visibleRows) next = selectedIndex - visibleRows + 1;
6018
+ const maxScroll = Math.max(0, total - visibleRows);
6019
+ if (next < 0) next = 0;
6020
+ if (next > maxScroll) next = maxScroll;
6021
+ return next;
5570
6022
  }
5571
6023
  function compareTasks(a, b) {
5572
6024
  const aTerminal = isTerminal(a.status);
@@ -5606,6 +6058,12 @@ var TasksBrowserApp = class extends Container {
5606
6058
  sortedVisible;
5607
6059
  selectedIndex = 0;
5608
6060
  listScroll = 0;
6061
+ /** Agents view selection — component-local: agent rows are read-only, so
6062
+ * nothing needs to travel back to the controller. `selectedAgentKey` is the
6063
+ * anchor: rows re-sort as statuses change, the index must not decide. */
6064
+ agentIndex = 0;
6065
+ agentScroll = 0;
6066
+ selectedAgentKey = void 0;
5609
6067
  pendingStopTaskId = void 0;
5610
6068
  pendingStopTimer = void 0;
5611
6069
  constructor(props, terminal) {
@@ -5619,6 +6077,7 @@ var TasksBrowserApp = class extends Container {
5619
6077
  this.props = next;
5620
6078
  this.sortedVisible = visibleTasks(next.tasks, next.filter).toSorted(compareTasks);
5621
6079
  this.syncSelectionFromProps();
6080
+ this.syncAgentSelectionFromProps();
5622
6081
  if (this.pendingStopTaskId !== void 0) {
5623
6082
  const task = next.tasks.find((t) => t.taskId === this.pendingStopTaskId);
5624
6083
  if (task === void 0 || isTerminal(task.status)) this.clearPendingStop();
@@ -5640,6 +6099,34 @@ var TasksBrowserApp = class extends Container {
5640
6099
  }
5641
6100
  if (this.selectedIndex >= this.sortedVisible.length) this.selectedIndex = this.sortedVisible.length - 1;
5642
6101
  }
6102
+ /** Keep the agents selection pinned to the same slot type across refreshes
6103
+ * (rows re-sort as statuses change); fall back to clamping. */
6104
+ syncAgentSelectionFromProps() {
6105
+ const agents = this.props.agents;
6106
+ if (agents.length === 0) {
6107
+ this.agentIndex = 0;
6108
+ this.agentScroll = 0;
6109
+ this.selectedAgentKey = void 0;
6110
+ return;
6111
+ }
6112
+ if (this.selectedAgentKey !== void 0) {
6113
+ const byKey = agents.findIndex((agent) => agent.key === this.selectedAgentKey);
6114
+ if (byKey !== -1) {
6115
+ this.agentIndex = byKey;
6116
+ return;
6117
+ }
6118
+ }
6119
+ if (this.agentIndex >= agents.length) this.agentIndex = agents.length - 1;
6120
+ this.selectedAgentKey = agents[this.agentIndex]?.key;
6121
+ }
6122
+ /** Move the agents cursor (rows are read-only: no controller round-trip). */
6123
+ selectAgent(index) {
6124
+ const agents = this.props.agents;
6125
+ if (agents.length === 0) return;
6126
+ this.agentIndex = Math.min(agents.length - 1, Math.max(0, index));
6127
+ this.selectedAgentKey = agents[this.agentIndex]?.key;
6128
+ this.invalidate();
6129
+ }
5643
6130
  clearPendingStop() {
5644
6131
  this.pendingStopTaskId = void 0;
5645
6132
  if (this.pendingStopTimer !== void 0) {
@@ -5647,12 +6134,21 @@ var TasksBrowserApp = class extends Container {
5647
6134
  this.pendingStopTimer = void 0;
5648
6135
  }
5649
6136
  }
6137
+ /**
6138
+ * Component-local teardown, called by the controller's close() before the
6139
+ * component is swapped out: the pending-stop confirm timer must not
6140
+ * outlive the browser.
6141
+ */
6142
+ close() {
6143
+ this.clearPendingStop();
6144
+ }
5650
6145
  emitSelect() {
5651
6146
  const task = this.sortedVisible[this.selectedIndex];
5652
6147
  if (task) this.props.onSelect(task.taskId);
5653
6148
  }
5654
6149
  handleInput(data) {
5655
6150
  const k = printableChar(data);
6151
+ const agentsMode = this.props.filter === "agents";
5656
6152
  if (this.pendingStopTaskId !== void 0) {
5657
6153
  if (k === "y" || k === "Y") {
5658
6154
  const taskId = this.pendingStopTaskId;
@@ -5670,6 +6166,10 @@ var TasksBrowserApp = class extends Container {
5670
6166
  return;
5671
6167
  }
5672
6168
  if (matchesKey(data, Key.up) || k === "k") {
6169
+ if (agentsMode) {
6170
+ this.selectAgent(this.agentIndex - 1);
6171
+ return;
6172
+ }
5673
6173
  if (this.sortedVisible.length === 0) return;
5674
6174
  this.selectedIndex = Math.max(0, this.selectedIndex - 1);
5675
6175
  this.emitSelect();
@@ -5677,6 +6177,10 @@ var TasksBrowserApp = class extends Container {
5677
6177
  return;
5678
6178
  }
5679
6179
  if (matchesKey(data, Key.down) || k === "j") {
6180
+ if (agentsMode) {
6181
+ this.selectAgent(this.agentIndex + 1);
6182
+ return;
6183
+ }
5680
6184
  if (this.sortedVisible.length === 0) return;
5681
6185
  this.selectedIndex = Math.min(this.sortedVisible.length - 1, this.selectedIndex + 1);
5682
6186
  this.emitSelect();
@@ -5691,6 +6195,7 @@ var TasksBrowserApp = class extends Container {
5691
6195
  this.props.onRefresh();
5692
6196
  return;
5693
6197
  }
6198
+ if (agentsMode) return;
5694
6199
  if (k === "s" || k === "S") {
5695
6200
  const task = this.sortedVisible[this.selectedIndex];
5696
6201
  if (task === void 0) return;
@@ -5734,15 +6239,26 @@ var TasksBrowserApp = class extends Container {
5734
6239
  renderHeader(width) {
5735
6240
  const colors = this.props.colors;
5736
6241
  const title = chalk.hex(colors.primary).bold(" TASK BROWSER ");
5737
- const filterText = chalk.hex(colors.textMuted)(` filter=${this.props.filter === "all" ? "ALL" : "ACTIVE"} `);
5738
- const counts = countByStatus(this.props.tasks);
5739
- const countSegments = [];
5740
- if (counts.running > 0) countSegments.push(chalk.hex(colors.success)(` ${String(counts.running)} ${t("taskbrowser.running")} `));
5741
- if (counts.awaiting > 0) countSegments.push(chalk.hex(colors.warning)(` ${String(counts.awaiting)} ${t("taskbrowser.awaiting")} `));
5742
- if (counts.completed > 0) countSegments.push(chalk.hex(colors.textDim)(` ${String(counts.completed)} ${t("taskbrowser.completed")} `));
5743
- if (counts.terminalFailed > 0) countSegments.push(chalk.hex(colors.error)(` ${String(counts.terminalFailed)} ${t("taskbrowser.interrupted")} `));
5744
- const totals = chalk.hex(colors.textMuted)(` ${String(this.props.tasks.length)} ${t("taskbrowser.total")} `);
5745
- return fitExactly$1(title + filterText + countSegments.join("") + totals, width);
6242
+ const filterLabel = this.props.filter === "all" ? "ALL" : this.props.filter === "active" ? "ACTIVE" : "AGENTS";
6243
+ const filterText = chalk.hex(colors.textMuted)(` filter=${filterLabel} `);
6244
+ const segments = [];
6245
+ if (this.props.filter === "agents") {
6246
+ const live = this.props.agents.filter((agent) => agent.live).length;
6247
+ const ended = this.props.agents.length - live;
6248
+ if (live > 0) segments.push(chalk.hex(colors.success)(` ${String(live)} ${t("taskbrowser.agents_live")} `));
6249
+ if (ended > 0) segments.push(chalk.hex(colors.textDim)(` ${String(ended)} ${t("taskbrowser.agents_ended")} `));
6250
+ if (this.props.agents.length === 0) segments.push(chalk.hex(colors.textMuted)(` 0 ${t("taskbrowser.agents")} `));
6251
+ } else {
6252
+ const counts = countByStatus(this.props.tasks);
6253
+ if (counts.running > 0) segments.push(chalk.hex(colors.success)(` ${String(counts.running)} ${t("taskbrowser.running")} `));
6254
+ if (counts.awaiting > 0) segments.push(chalk.hex(colors.warning)(` ${String(counts.awaiting)} ${t("taskbrowser.awaiting")} `));
6255
+ if (counts.completed > 0) segments.push(chalk.hex(colors.textDim)(` ${String(counts.completed)} ${t("taskbrowser.completed")} `));
6256
+ if (counts.terminalFailed > 0) segments.push(chalk.hex(colors.error)(` ${String(counts.terminalFailed)} ${t("taskbrowser.interrupted")} `));
6257
+ segments.push(chalk.hex(colors.textMuted)(` ${String(this.props.tasks.length)} ${t("taskbrowser.total")} `));
6258
+ const liveAgents = this.props.agents.filter((agent) => agent.live).length;
6259
+ if (liveAgents > 0) segments.push(chalk.hex(colors.success)(` ${String(liveAgents)} ${t("taskbrowser.agents")} `));
6260
+ }
6261
+ return fitExactly$1(title + filterText + segments.join(""), width);
5746
6262
  }
5747
6263
  renderFooter(width) {
5748
6264
  const colors = this.props.colors;
@@ -5752,14 +6268,19 @@ var TasksBrowserApp = class extends Container {
5752
6268
  const warn = (text) => chalk.hex(colors.warning).bold(text);
5753
6269
  return fitExactly$1(` ${warn(t("taskbrowser.stop"))} ${chalk.hex(colors.text)(this.pendingStopTaskId)}? ${key("Y")} ${dim(t("taskbrowser.confirm"))} ${key("N")} ${dim(t("taskbrowser.cancel"))} `, width);
5754
6270
  }
5755
- const left = [
6271
+ const left = (this.props.filter === "agents" ? [
6272
+ ` ${key("↑↓")} ${dim(t("taskbrowser.select"))}`,
6273
+ `${key("Tab")} ${dim(t("taskbrowser.filter"))}`,
6274
+ `${key("R")} ${dim(t("taskbrowser.refresh"))}`,
6275
+ `${key("Q/Esc")} ${dim(t("taskbrowser.exit"))} `
6276
+ ] : [
5756
6277
  ` ${key("↑↓")} ${dim(t("taskbrowser.select"))}`,
5757
6278
  `${key("Enter/O")} ${dim(t("taskbrowser.output"))}`,
5758
6279
  `${key("S")} ${dim(t("taskbrowser.stop_action"))}`,
5759
6280
  `${key("R")} ${dim(t("taskbrowser.refresh"))}`,
5760
6281
  `${key("Tab")} ${dim(t("taskbrowser.filter"))}`,
5761
6282
  `${key("Q/Esc")} ${dim(t("taskbrowser.exit"))} `
5762
- ].join(" ");
6283
+ ]).join(" ");
5763
6284
  const flash = this.props.flashMessage;
5764
6285
  if (flash !== void 0 && flash.length > 0) {
5765
6286
  const flashStyled = chalk.hex(colors.warning)(` ${flash} `);
@@ -5798,6 +6319,7 @@ var TasksBrowserApp = class extends Container {
5798
6319
  return lines;
5799
6320
  }
5800
6321
  renderListFrame(width, height) {
6322
+ if (this.props.filter === "agents") return this.renderAgentsFrame(width, height);
5801
6323
  const title = `Tasks [${this.props.filter}]`;
5802
6324
  const innerHeight = Math.max(0, height - 2);
5803
6325
  if (this.sortedVisible.length === 0) {
@@ -5818,6 +6340,43 @@ var TasksBrowserApp = class extends Container {
5818
6340
  while (lines.length < innerHeight) lines.push("");
5819
6341
  return this.renderFrame(title, lines, width, height);
5820
6342
  }
6343
+ /** Left pane, `agents` filter: one row per subagent type with history (live
6344
+ * ones first), including types whose slot the cap pushed out and that only
6345
+ * the instance registry knows; read-only — the detail pane carries the full
6346
+ * provenance. */
6347
+ renderAgentsFrame(width, height) {
6348
+ const colors = this.props.colors;
6349
+ const agents = this.props.agents;
6350
+ const innerHeight = Math.max(0, height - 2);
6351
+ const title = `${t("taskbrowser.agents_title")} [${String(agents.length)}]`;
6352
+ if (agents.length === 0) {
6353
+ const lines = [chalk.hex(colors.textMuted)(t("taskbrowser.agents_empty"))];
6354
+ while (lines.length < innerHeight) lines.push("");
6355
+ return this.renderFrame(title, lines, width, height);
6356
+ }
6357
+ this.agentScroll = clampListScroll(this.agentIndex, this.agentScroll, agents.length, innerHeight);
6358
+ const start = this.agentScroll;
6359
+ const window = agents.slice(start, start + innerHeight);
6360
+ const innerWidth = width - 2;
6361
+ const lines = [];
6362
+ for (const [vi, agent] of window.entries()) {
6363
+ const index = start + vi;
6364
+ lines.push(this.renderAgentRow(agent, index === this.agentIndex, innerWidth));
6365
+ }
6366
+ while (lines.length < innerHeight) lines.push("");
6367
+ return this.renderFrame(title, lines, width, height);
6368
+ }
6369
+ renderAgentRow(agent, selected, innerWidth) {
6370
+ const colors = this.props.colors;
6371
+ const pointer = selected ? "> " : " ";
6372
+ const head = `${chalk.hex(selected ? colors.primary : colors.textDim)(pointer)}${chalk.hex(agentStatusColor(colors, agent.status))(agent.status === "idle" ? "○" : "●")} ${chalk.hex(selected ? colors.primary : colors.textStrong)(padToWidth$1(truncateToWidth(agent.type, AGENT_NAME_COLS, ELLIPSIS$3), AGENT_NAME_COLS))} ${chalk.hex(agentStatusColor(colors, agent.status))(agentStatusText(agent.status))}${agent.count > 1 ? chalk.hex(colors.textMuted)(` ×${String(agent.count)}`) : ""}`;
6373
+ const budget = innerWidth - visibleWidth(head) - 1;
6374
+ if (budget < 6) return fitExactly$1(head, innerWidth);
6375
+ const hint = singleLine$2(agent.detail ?? agent.description ?? "");
6376
+ if (hint.length === 0) return fitExactly$1(head, innerWidth);
6377
+ const trimmed = truncateToWidth(hint, budget, ELLIPSIS$3);
6378
+ return fitExactly$1(`${head} ${chalk.hex(colors.textDim)(trimmed)}`, innerWidth);
6379
+ }
5821
6380
  renderListRow(task, selected, innerWidth) {
5822
6381
  const colors = this.props.colors;
5823
6382
  const pointer = selected ? "> " : " ";
@@ -5834,15 +6393,7 @@ var TasksBrowserApp = class extends Container {
5834
6393
  return fitExactly$1(`${prefix} ${chalk.hex(colors.text)(desc)}`, innerWidth);
5835
6394
  }
5836
6395
  adjustScroll(visibleRows) {
5837
- if (visibleRows <= 0) {
5838
- this.listScroll = 0;
5839
- return;
5840
- }
5841
- if (this.selectedIndex < this.listScroll) this.listScroll = this.selectedIndex;
5842
- else if (this.selectedIndex >= this.listScroll + visibleRows) this.listScroll = this.selectedIndex - visibleRows + 1;
5843
- const maxScroll = Math.max(0, this.sortedVisible.length - visibleRows);
5844
- if (this.listScroll < 0) this.listScroll = 0;
5845
- if (this.listScroll > maxScroll) this.listScroll = maxScroll;
6396
+ this.listScroll = clampListScroll(this.selectedIndex, this.listScroll, this.sortedVisible.length, visibleRows);
5846
6397
  }
5847
6398
  renderRightStack(width, height) {
5848
6399
  const detailHeight = Math.max(8, Math.min(Math.floor(height * .4), height - 5));
@@ -5850,6 +6401,7 @@ var TasksBrowserApp = class extends Container {
5850
6401
  return [...this.renderDetailFrame(width, detailHeight), ...this.renderPreviewFrame(width, previewHeight)];
5851
6402
  }
5852
6403
  renderDetailFrame(width, height) {
6404
+ if (this.props.filter === "agents") return this.renderAgentDetailFrame(width, height);
5853
6405
  const colors = this.props.colors;
5854
6406
  const innerHeight = Math.max(0, height - 2);
5855
6407
  const task = this.sortedVisible[this.selectedIndex];
@@ -5876,9 +6428,43 @@ var TasksBrowserApp = class extends Container {
5876
6428
  while (lines.length < innerHeight) lines.push("");
5877
6429
  return this.renderFrame(t("taskbrowser.detail"), lines, width, height);
5878
6430
  }
6431
+ /** Right-top pane, `agents` filter: provenance of the selected agent —
6432
+ * type, live status, spawn description, trigger source, parent chain,
6433
+ * latest activity and the current instance id. */
6434
+ renderAgentDetailFrame(width, height) {
6435
+ const colors = this.props.colors;
6436
+ const innerHeight = Math.max(0, height - 2);
6437
+ const agent = this.props.agents[this.agentIndex];
6438
+ if (agent === void 0) {
6439
+ const lines = [chalk.hex(colors.textMuted)(t("taskbrowser.agents_empty"))];
6440
+ while (lines.length < innerHeight) lines.push("");
6441
+ return this.renderFrame(t("taskbrowser.agent_detail"), lines, width, height);
6442
+ }
6443
+ const label = (text) => chalk.hex(colors.textMuted)(text.padEnd(14));
6444
+ const value = (text) => chalk.hex(colors.text)(text);
6445
+ const countSuffix = agent.count > 1 ? chalk.hex(colors.textMuted)(` ×${String(agent.count)}`) : "";
6446
+ const lines = [
6447
+ `${label(t("taskbrowser.agent_type"))}${value(agent.type)}`,
6448
+ `${label(t("taskbrowser.status"))}${chalk.hex(agentStatusColor(colors, agent.status))(agentStatusText(agent.status))}${countSuffix}`,
6449
+ `${label(t("taskbrowser.agent_source"))}${value(singleLine$2(formatAgentSource(agent.source)))}`,
6450
+ `${label(t("taskbrowser.agent_chain"))}${chalk.hex(colors.textMuted)(formatAgentChain(agent))}`
6451
+ ];
6452
+ if (agent.description !== void 0 && agent.description.length > 0) lines.push(`${label(t("taskbrowser.description"))}${value(singleLine$2(agent.description))}`);
6453
+ if (agent.detail !== void 0 && agent.detail.length > 0) lines.push(`${label(t("taskbrowser.agent_activity"))}${value(singleLine$2(agent.detail))}`);
6454
+ const when = formatRelativeTime$2(agent.lastActivityAt);
6455
+ if (when.length > 0) lines.push(`${label(t("taskbrowser.time"))}${chalk.hex(colors.textMuted)(when)}`);
6456
+ if (agent.instanceId !== void 0) lines.push(`${label(t("taskbrowser.agent_instance"))}${chalk.hex(colors.textMuted)(agent.instanceId)}`);
6457
+ while (lines.length < innerHeight) lines.push("");
6458
+ return this.renderFrame(t("taskbrowser.agent_detail"), lines, width, height);
6459
+ }
5879
6460
  renderPreviewFrame(width, height) {
5880
6461
  const colors = this.props.colors;
5881
6462
  const innerHeight = Math.max(0, height - 2);
6463
+ if (this.props.filter === "agents") {
6464
+ const lines = [chalk.hex(colors.textMuted)(t("taskbrowser.agents_no_output"))];
6465
+ while (lines.length < innerHeight) lines.push("");
6466
+ return this.renderFrame("Preview Output", lines, width, height);
6467
+ }
5882
6468
  if (this.sortedVisible[this.selectedIndex] === void 0) {
5883
6469
  const lines = [chalk.hex(colors.textMuted)("No task selected.")];
5884
6470
  while (lines.length < innerHeight) lines.push("");
@@ -5927,6 +6513,7 @@ var TasksBrowserController = class {
5927
6513
  const selectedTaskId = this.pickInitialSelection(tasks, filter);
5928
6514
  const component = new TasksBrowserApp({
5929
6515
  tasks,
6516
+ agents: this.snapshotAgentRows(),
5930
6517
  filter,
5931
6518
  selectedTaskId,
5932
6519
  tailOutput: void 0,
@@ -5964,6 +6551,7 @@ var TasksBrowserController = class {
5964
6551
  if (browser.viewer !== void 0) this.closeOutputViewer();
5965
6552
  if (browser.pollTimer !== void 0) clearInterval(browser.pollTimer);
5966
6553
  if (browser.flashTimer !== void 0) clearTimeout(browser.flashTimer);
6554
+ browser.component.close();
5967
6555
  state.ui.setLayoutRoot(browser.savedLayoutRoot);
5968
6556
  this.host.setTasksBrowser(void 0);
5969
6557
  state.ui.setFocus(state.editor);
@@ -6034,6 +6622,7 @@ var TasksBrowserController = class {
6034
6622
  if (browser === void 0) return;
6035
6623
  browser.component.setProps({
6036
6624
  tasks,
6625
+ agents: this.snapshotAgentRows(),
6037
6626
  filter: browser.filter,
6038
6627
  selectedTaskId: browser.selectedTaskId,
6039
6628
  tailOutput: browser.tailOutput,
@@ -6044,6 +6633,13 @@ var TasksBrowserController = class {
6044
6633
  });
6045
6634
  this.host.state.ui.requestRender();
6046
6635
  }
6636
+ /** Agents view rows: the live slot state machine (authoritative status)
6637
+ * enriched with the per-instance provenance registry, plus the archive ring
6638
+ * for agents whose registry record has already been released. Rebuilt on
6639
+ * every refresh/repaint — all three sources are in-memory snapshots. */
6640
+ snapshotAgentRows() {
6641
+ return buildAgentRows(this.host.subagentSlots, this.host.subagentInstances, this.host.recentSubagentInstances);
6642
+ }
6047
6643
  buildCallbacks() {
6048
6644
  return {
6049
6645
  onSelect: (taskId) => {
@@ -6082,7 +6678,7 @@ var TasksBrowserController = class {
6082
6678
  handleToggleFilter() {
6083
6679
  const browser = this.host.state.tasksBrowser;
6084
6680
  if (browser === void 0) return;
6085
- browser.filter = browser.filter === "all" ? "active" : "all";
6681
+ browser.filter = browser.filter === "all" ? "active" : browser.filter === "active" ? "agents" : "all";
6086
6682
  this.repaint();
6087
6683
  }
6088
6684
  handleRefresh() {
@@ -6125,6 +6721,7 @@ var TasksBrowserController = class {
6125
6721
  }
6126
6722
  const current = state.tasksBrowser;
6127
6723
  if (current === void 0 || current !== browser) return;
6724
+ if (current.viewer !== void 0) return;
6128
6725
  const viewer = new TaskOutputViewer({
6129
6726
  taskId,
6130
6727
  info: this.host.backgroundTasks.get(taskId),
@@ -6134,7 +6731,7 @@ var TasksBrowserController = class {
6134
6731
  this.closeOutputViewer();
6135
6732
  }
6136
6733
  }, state.terminal);
6137
- const savedBrowserLayout = state.layoutRoot;
6734
+ const savedBrowserLayout = browser.component;
6138
6735
  state.ui.setLayoutRoot(viewer);
6139
6736
  state.ui.setFocus(viewer);
6140
6737
  state.ui.requestRender(true);
@@ -6351,6 +6948,28 @@ function basenameLike(raw) {
6351
6948
  return raw.split(/[\\/]/).filter((part) => part.length > 0).at(-1) ?? raw;
6352
6949
  }
6353
6950
  //#endregion
6951
+ //#region src/tui/utils/collapse-stub.ts
6952
+ /**
6953
+ * The transcript bounds its memory in two places, and both stand in for the
6954
+ * rows they fold away with the same summary line: the shadow entry array
6955
+ * (`transcript-controller`) folds its oldest entries into one stub entry, and
6956
+ * the committed render tree (`committed-transcript`) folds its oldest child
6957
+ * components into one stub row. One definition of the wording keeps the two
6958
+ * folds reading alike — and lets the array fold read its own count back when
6959
+ * it folds again.
6960
+ */
6961
+ /** Matches {@link formatCollapsedEntries} output, capturing the count. */
6962
+ const COLLAPSE_STUB_PATTERN = /^…\((\d+) earlier entries collapsed\)$/;
6963
+ /** Summary line that stands in for `count` folded-away entries. */
6964
+ function formatCollapsedEntries(count) {
6965
+ return `…(${String(count)} earlier entries collapsed)`;
6966
+ }
6967
+ /** How many entries a summary line stands in for; 0 when `text` is not one. */
6968
+ function parseCollapsedEntries(text) {
6969
+ const digits = COLLAPSE_STUB_PATTERN.exec(text)?.[1];
6970
+ return digits === void 0 ? 0 : Number(digits);
6971
+ }
6972
+ //#endregion
6354
6973
  //#region src/tui/components/transcript/committed-transcript.ts
6355
6974
  var CommittedMessageComponent = class {
6356
6975
  entry;
@@ -6420,6 +7039,9 @@ var CommittedTranscriptComponent = class extends CachedContainer {
6420
7039
  header;
6421
7040
  colors;
6422
7041
  committedCount = 0;
7042
+ /** Rows the retention cap has folded into the summary stub. */
7043
+ collapsedCount = 0;
7044
+ collapseStub;
6423
7045
  constructor(colors) {
6424
7046
  super();
6425
7047
  this.colors = colors;
@@ -6429,6 +7051,12 @@ var CommittedTranscriptComponent = class extends CachedContainer {
6429
7051
  getCount() {
6430
7052
  return this.committedCount;
6431
7053
  }
7054
+ /** Rows the tree folded into its summary stub. They still count toward
7055
+ * {@link getCount()}: folding changes what is on screen, never what was
7056
+ * committed. */
7057
+ getCollapsedCount() {
7058
+ return this.collapsedCount;
7059
+ }
6432
7060
  setCount(count) {
6433
7061
  this.committedCount = count;
6434
7062
  if (count === 0) this.header.setText("");
@@ -6437,11 +7065,46 @@ var CommittedTranscriptComponent = class extends CachedContainer {
6437
7065
  }
6438
7066
  appendEntry(entry, colors) {
6439
7067
  this.addChild(new CommittedMessageComponent(entry, colors));
7068
+ this.enforceRetentionCap();
7069
+ }
7070
+ /**
7071
+ * Holds the tree at its retention point: the entry children it renders —
7072
+ * the summary stub included — never exceed MAX_COMMITTED_ENTRIES. Past that
7073
+ * the oldest rows fold into the stub, the same semantics as the entry-array
7074
+ * cap, so the child list (and the render cache every child carries) stops
7075
+ * growing with the session. The stub takes one slot of the budget, so the
7076
+ * fold lands on stub + (MAX - 1) rows: the cap is the actually retained set.
7077
+ */
7078
+ enforceRetentionCap() {
7079
+ if (this.children.length - 1 <= 500) return;
7080
+ let stub = this.collapseStub;
7081
+ if (stub === void 0) {
7082
+ stub = new Text("", 0, 0);
7083
+ this.collapseStub = stub;
7084
+ this.children.splice(1, 0, stub);
7085
+ }
7086
+ const fold = this.children.length - 2 - 499;
7087
+ this.children.splice(2, fold);
7088
+ this.collapsedCount += fold;
7089
+ stub.setText(` ${chalk.hex(this.colors.textDim)(formatCollapsedEntries(this.collapsedCount))}`);
6440
7090
  this.invalidate();
6441
7091
  }
6442
7092
  };
6443
- //#endregion
6444
- //#region src/tui/controllers/transcript-controller.ts
7093
+ /** Id of the aggregate stub that stands in for the folded oldest entries. */
7094
+ const COLLAPSE_STUB_ID = "transcript-collapse-stub";
7095
+ function makeCollapseStub(count) {
7096
+ return {
7097
+ id: COLLAPSE_STUB_ID,
7098
+ kind: "status",
7099
+ renderMode: "plain",
7100
+ content: formatCollapsedEntries(count)
7101
+ };
7102
+ }
7103
+ /** Entries an existing stub already stands in for (0 when it is not one). */
7104
+ function stubCount(entry) {
7105
+ if (entry === void 0 || entry.id !== COLLAPSE_STUB_ID) return 0;
7106
+ return parseCollapsedEntries(entry.content);
7107
+ }
6445
7108
  var TranscriptController = class TranscriptController {
6446
7109
  host;
6447
7110
  welcomeComponent;
@@ -6451,6 +7114,17 @@ var TranscriptController = class TranscriptController {
6451
7114
  * ticker of the card that is still claiming the task is running. */
6452
7115
  liveNoticesByTrackingId = /* @__PURE__ */ new Map();
6453
7116
  pendingComponents = /* @__PURE__ */ new Set();
7117
+ /** Monotonic count of composition changes to `state.transcriptEntries`:
7118
+ * +1 per ingest and +1 per cap fold. Aggregates that memoize over the entry
7119
+ * list key on this — the fold holds the array length at the cap while
7120
+ * replacing the oldest rows, so a length-only key can go stale. */
7121
+ ingestCount = 0;
7122
+ /** Contributions of the rows the entry cap has already folded out of the
7123
+ * array. Kept separate from the rows still held, so aggregates that must
7124
+ * stay monotonic over a long session (the sidebar's session stats) can add
7125
+ * them back — see {@link getFoldedEntryCounts}. */
7126
+ foldedTurns = 0;
7127
+ foldedToolCalls = 0;
6454
7128
  /** Max live transcript children before the oldest are folded into the
6455
7129
  * committed single-line summary. Overridable via SCREAM_TRANSCRIPT_LIVE_LIMIT
6456
7130
  * (mirrors a commit-fold approach for bounding ultra-long sessions). */
@@ -6539,6 +7213,7 @@ var TranscriptController = class TranscriptController {
6539
7213
  this.dropLiveComponent(component);
6540
7214
  }
6541
7215
  this.committedComponent.setCount(this.committedComponent.getCount() + toCommit.length);
7216
+ state.ui.resetTextSelection();
6542
7217
  if (process.env["SCREAM_CODE_DEBUG"] === "1") this.host.showStatus(`[debug] committed=${this.committedComponent.getCount()} live=${this.getLiveCount()}`);
6543
7218
  container.invalidate();
6544
7219
  state.ui.requestRender();
@@ -6583,12 +7258,55 @@ var TranscriptController = class TranscriptController {
6583
7258
  default: return null;
6584
7259
  }
6585
7260
  }
7261
+ /**
7262
+ * The transcript's single bounded ingress: every entry reaches
7263
+ * `state.transcriptEntries` through here — `appendEntry` for rows that mount
7264
+ * their own component, and `ScreamTUI.pushTranscriptEntry` for the live
7265
+ * streaming rows that already own one. One place applies the UI preview
7266
+ * bound (`boundEntryForUi`) and the entry-count cap, so no producer can grow
7267
+ * the array without bound, whichever path it comes in on.
7268
+ *
7269
+ * Returns the entry as stored: a bounded copy when the bound rewrote it,
7270
+ * otherwise the very entry handed in — identity matters to callers that keep
7271
+ * mutating a live draft (assistant text, thinking) after pushing it.
7272
+ */
7273
+ ingestEntry(entry) {
7274
+ const boundedEntry = boundEntryForUi(entry);
7275
+ this.ingestCount += 1;
7276
+ this.host.state.transcriptEntries.push(boundedEntry);
7277
+ this.enforceEntryCap();
7278
+ return boundedEntry;
7279
+ }
7280
+ /**
7281
+ * Monotonic count of transcript composition changes: one per ingest, plus
7282
+ * one per cap fold. Consumers that memoize aggregates over
7283
+ * `state.transcriptEntries` (the sidebar session stats) key on this instead
7284
+ * of `length` alone, because a fold lands the array back on the same length
7285
+ * while replacing its oldest rows.
7286
+ */
7287
+ getIngestCount() {
7288
+ return this.ingestCount;
7289
+ }
7290
+ /**
7291
+ * What the entry cap has already folded out of `state.transcriptEntries`:
7292
+ * user turns and real tool calls (compression markers ride kind 'tool_call'
7293
+ * but are not invocations — same rule as the retained-row scan). Aggregates
7294
+ * that must be monotonic over a long session — the sidebar's session stats —
7295
+ * add these to the rows the array still holds, because the array stops
7296
+ * holding the oldest ones at the cap.
7297
+ */
7298
+ getFoldedEntryCounts() {
7299
+ return {
7300
+ turns: this.foldedTurns,
7301
+ toolCalls: this.foldedToolCalls
7302
+ };
7303
+ }
6586
7304
  appendEntry(entry) {
6587
- this.host.state.transcriptEntries.push(entry);
6588
- const component = this.createComponent(entry);
7305
+ const boundedEntry = this.ingestEntry(entry);
7306
+ const component = this.createComponent(boundedEntry);
6589
7307
  if (component) {
6590
- this.liveComponentToEntry.set(component, entry);
6591
- const status = entry.backgroundAgentStatus;
7308
+ this.liveComponentToEntry.set(component, boundedEntry);
7309
+ const status = boundedEntry.backgroundAgentStatus;
6592
7310
  if (status !== void 0 && component instanceof BackgroundAgentStatusComponent) {
6593
7311
  const trackingId = status.trackingId;
6594
7312
  if (trackingId !== void 0) if (status.phase === "started") this.liveNoticesByTrackingId.set(trackingId, component);
@@ -6602,6 +7320,24 @@ var TranscriptController = class TranscriptController {
6602
7320
  }
6603
7321
  return component ?? null;
6604
7322
  }
7323
+ /**
7324
+ * Keeps `state.transcriptEntries` within {@link MAX_TRANSCRIPT_ENTRIES} by
7325
+ * folding the oldest rows into one aggregate stub. A stub already at the head
7326
+ * keeps that slot and accumulates the count, so the array stays bounded no
7327
+ * matter how long the session runs.
7328
+ */
7329
+ enforceEntryCap() {
7330
+ const entries = this.host.state.transcriptEntries;
7331
+ if (entries.length <= 500) return;
7332
+ const head = entries[0];
7333
+ const isStub = head !== void 0 && head.id === COLLAPSE_STUB_ID;
7334
+ const dropped = entries.splice(0, entries.length - 500 + 1);
7335
+ for (const entry of dropped) if (entry.kind === "user") this.foldedTurns += 1;
7336
+ else if (entry.kind === "tool_call" && entry.compactionData === void 0) this.foldedToolCalls += 1;
7337
+ const folded = dropped.length - (isStub ? 1 : 0);
7338
+ entries.unshift(makeCollapseStub(stubCount(head) + folded));
7339
+ this.ingestCount += 1;
7340
+ }
6605
7341
  /** Append a suffix to the given turn's final assistant message last line.
6606
7342
  * Used to place the turn elapsed marker flush against the last character
6607
7343
  * of the assistant's reply. Turn-scoped so a tool-only turn never stamps
@@ -6652,10 +7388,13 @@ var TranscriptController = class TranscriptController {
6652
7388
  }
6653
7389
  clearAndRedraw() {
6654
7390
  const { state, streamingUI, imageStore } = this.host;
7391
+ state.ui.resetTextSelection();
6655
7392
  streamingUI.discardPending();
6656
7393
  streamingUI.endActivityGroup();
6657
7394
  streamingUI.flushPendingApprovals();
6658
7395
  state.transcriptEntries = [];
7396
+ this.foldedTurns = 0;
7397
+ this.foldedToolCalls = 0;
6659
7398
  streamingUI.disposeActiveCompactionBlock();
6660
7399
  streamingUI.resetLiveText();
6661
7400
  streamingUI.resetToolUi();
@@ -6714,12 +7453,17 @@ var TranscriptController = class TranscriptController {
6714
7453
  const { state } = this.host;
6715
7454
  const next = !state.toolOutputExpanded;
6716
7455
  const children = state.transcriptContainer.children;
7456
+ let resized = false;
6717
7457
  for (let i = children.length - 1; i >= 0; i -= 1) {
6718
7458
  const child = children[i];
6719
7459
  if (child === void 0) continue;
6720
7460
  if (child instanceof UserMessageComponent || child instanceof SkillActivationComponent || child instanceof CronMessageComponent || child instanceof CommittedTranscriptComponent) break;
6721
- if (isExpandable(child)) child.setExpanded(next);
7461
+ if (isExpandable(child)) {
7462
+ child.setExpanded(next);
7463
+ resized = true;
7464
+ }
6722
7465
  }
7466
+ if (resized) state.ui.resetTextSelection();
6723
7467
  state.toolOutputExpanded = next;
6724
7468
  state.ui.requestRender();
6725
7469
  }
@@ -6729,6 +7473,7 @@ var TranscriptController = class TranscriptController {
6729
7473
  let toggled = false;
6730
7474
  for (const child of state.transcriptContainer.children) if (isPlanExpandable(child) && child.setPlanExpanded(next)) toggled = true;
6731
7475
  if (!toggled) return false;
7476
+ state.ui.resetTextSelection();
6732
7477
  state.planExpanded = next;
6733
7478
  state.ui.requestRender();
6734
7479
  return true;
@@ -7969,7 +8714,8 @@ var LifecycleController = class LifecycleController {
7969
8714
  hubProbe = createHubProbe();
7970
8715
  sidebarGitCacheWorkDir = "";
7971
8716
  /** Memo for the session-stats aggregation: recomputed only when the
7972
- * transcript length, compaction count or usage object changes. */
8717
+ * transcript's ingest counter/length, compaction count or usage object
8718
+ * changes. */
7973
8719
  sidebarStatsMemo;
7974
8720
  static MEMORY_IDLE_MS = 900 * 1e3;
7975
8721
  static MEMORY_COUNTDOWN_MS = 15 * 1e3;
@@ -8236,16 +8982,20 @@ var LifecycleController = class LifecycleController {
8236
8982
  }
8237
8983
  readSidebarSessionStats(appState) {
8238
8984
  const entries = this.host.state.transcriptEntries;
8985
+ const ingestCount = this.host.transcriptController.getIngestCount();
8239
8986
  const usage = appState.sessionUsage;
8240
8987
  const subagentUsage = appState.subagentUsage;
8241
8988
  const memo = this.sidebarStatsMemo;
8242
- if (memo !== void 0 && memo.length === entries.length && memo.compactions === appState.autoCompactionCount && memo.usage === usage && memo.subagentUsage === subagentUsage && memo.apiCalls === appState.sessionApiCalls) return memo.stats;
8989
+ if (memo !== void 0 && memo.ingestCount === ingestCount && memo.length === entries.length && memo.compactions === appState.autoCompactionCount && memo.usage === usage && memo.subagentUsage === subagentUsage && memo.apiCalls === appState.sessionApiCalls) return memo.stats;
8243
8990
  let turns = 0;
8244
8991
  let toolCalls = 0;
8245
8992
  for (const entry of entries) {
8246
8993
  if (entry.kind === "user") turns += 1;
8247
8994
  if (entry.kind === "tool_call" && entry.compactionData === void 0) toolCalls += 1;
8248
8995
  }
8996
+ const foldedAway = this.host.transcriptController.getFoldedEntryCounts();
8997
+ turns += foldedAway.turns;
8998
+ toolCalls += foldedAway.toolCalls;
8249
8999
  let subagentOutput = 0;
8250
9000
  for (const sub of Object.values(subagentUsage)) subagentOutput += sub.output;
8251
9001
  const stats = {
@@ -8259,6 +9009,7 @@ var LifecycleController = class LifecycleController {
8259
9009
  tokensOutput: usage.output + subagentOutput
8260
9010
  };
8261
9011
  this.sidebarStatsMemo = {
9012
+ ingestCount,
8262
9013
  length: entries.length,
8263
9014
  compactions: appState.autoCompactionCount,
8264
9015
  usage,
@@ -10194,6 +10945,7 @@ var CustomEditor = class extends Editor {
10194
10945
  thinkingLevel: this.thinkingLevel,
10195
10946
  toolPriority: this.toolPriority,
10196
10947
  paint: this.borderColor ?? ((s) => s),
10948
+ modeHex: getPermissionModeColor(this.permissionMode, this.colors),
10197
10949
  borderHex: this.borderHex
10198
10950
  });
10199
10951
  return lines;
@@ -10344,6 +11096,15 @@ function injectPromptSymbol(line) {
10344
11096
  }
10345
11097
  const THINK_LABEL_MIN_WIDTH = 14;
10346
11098
  const MODE_BADGE_MIN_WIDTH = 10;
11099
+ function getPermissionModeColor(mode, colors) {
11100
+ return {
11101
+ manual: colors.permissionManual,
11102
+ yolo: colors.permissionYolo,
11103
+ auto: colors.permissionAuto,
11104
+ ask: colors.permissionAsk,
11105
+ bot: colors.permissionBot
11106
+ }[mode];
11107
+ }
10347
11108
  function isLightBg(hex) {
10348
11109
  const r = parseInt(hex.slice(1, 3), 16);
10349
11110
  const g = parseInt(hex.slice(3, 5), 16);
@@ -10374,7 +11135,7 @@ function injectBorderBadges(lines, width, opts) {
10374
11135
  let right = paint("─");
10375
11136
  if (width >= MODE_BADGE_MIN_WIDTH) {
10376
11137
  const badgeText = ` ${opts.mode} `;
10377
- left = paint("──") + (opts.borderHex ? chalk.hex(opts.borderHex).bold(badgeText) : paint(badgeText));
11138
+ left = paint("──") + chalk.hex(opts.modeHex).bold(badgeText);
10378
11139
  }
10379
11140
  if (opts.thinking && width >= THINK_LABEL_MIN_WIDTH) right = makeBadge(opts.thinkingLevel !== "off" ? ` Think ${opts.thinkingLevel} ` : " Think ", opts.borderHex) + right;
10380
11141
  if (opts.toolPriority === "skill" || opts.toolPriority === "mcp") right = makeBadge(` First ${opts.toolPriority} `, opts.borderHex) + paint("─") + right;
@@ -11274,6 +12035,7 @@ var SessionManager$1 = class {
11274
12035
  planMode: status.planMode ? status.planStrategy === "fusion" ? "fusionplan" : "plan" : "off",
11275
12036
  wolfpackMode: status.wolfpackMode,
11276
12037
  rlmEnabled: status.rlmEnabled,
12038
+ rlmMaxDepth: status.rlmMaxDepth ?? null,
11277
12039
  contextTokens: status.contextTokens,
11278
12040
  maxContextTokens: status.maxContextTokens,
11279
12041
  contextUsage: status.contextUsage,
@@ -11951,6 +12713,10 @@ const DEFAULT_KEYBOARD_SHORTCUTS = [
11951
12713
  keys: "Shift-Tab",
11952
12714
  description: t("help.toggle_plan")
11953
12715
  },
12716
+ {
12717
+ keys: "Tab",
12718
+ description: t("help.cycle_thinking")
12719
+ },
11954
12720
  {
11955
12721
  keys: "Ctrl-G",
11956
12722
  description: t("help.external_editor")
@@ -11959,10 +12725,26 @@ const DEFAULT_KEYBOARD_SHORTCUTS = [
11959
12725
  keys: "Ctrl-O",
11960
12726
  description: t("help.toggle_output")
11961
12727
  },
12728
+ {
12729
+ keys: "Ctrl-E",
12730
+ description: t("help.toggle_plan_view")
12731
+ },
11962
12732
  {
11963
12733
  keys: "Ctrl-S",
11964
12734
  description: t("help.interrupt")
11965
12735
  },
12736
+ {
12737
+ keys: "Ctrl-X",
12738
+ description: t("help.toggle_sidebar")
12739
+ },
12740
+ {
12741
+ keys: "Ctrl-B",
12742
+ description: t("help.toggle_hint")
12743
+ },
12744
+ {
12745
+ keys: "Ctrl-W",
12746
+ description: t("help.cancel_memory")
12747
+ },
11966
12748
  {
11967
12749
  keys: "Shift-Enter / Ctrl-J",
11968
12750
  description: t("help.newline")
@@ -13490,7 +14272,7 @@ var DialogManager = class {
13490
14272
  }));
13491
14273
  }
13492
14274
  showMemoryPicker(preloadedMemos, preloadedTotal) {
13493
- const store = new MemoryMemoStore(getDataDir());
14275
+ const store = sharedMemoStore(getDataDir());
13494
14276
  const hasData = preloadedMemos !== void 0;
13495
14277
  const memos = preloadedMemos ?? [];
13496
14278
  const total = preloadedTotal ?? 0;
@@ -13655,6 +14437,7 @@ function createInitialAppState(input) {
13655
14437
  ccConnectActive: false,
13656
14438
  wolfpackMode: input.cliOptions.wolfpack === true,
13657
14439
  rlmEnabled: false,
14440
+ rlmMaxDepth: null,
13658
14441
  reconnectAttempt: 0,
13659
14442
  recentSessions: [],
13660
14443
  subagentUsage: {}
@@ -13986,7 +14769,7 @@ var ScreamTUI = class {
13986
14769
  return first;
13987
14770
  }
13988
14771
  pushTranscriptEntry(entry) {
13989
- this.state.transcriptEntries.push(entry);
14772
+ this.transcriptController.ingestEntry(entry);
13990
14773
  }
13991
14774
  setExternalEditorRunning(running) {
13992
14775
  this.state.externalEditorRunning = running;
@@ -14003,6 +14786,21 @@ var ScreamTUI = class {
14003
14786
  get backgroundTasks() {
14004
14787
  return this.sessionEventHandler.backgroundTasks;
14005
14788
  }
14789
+ /** Live per-type subagent slots (sidebar state machine) — source of the
14790
+ * /tasks browser's Agents view. */
14791
+ get subagentSlots() {
14792
+ return this.sessionEventHandler.getSubagentSlots();
14793
+ }
14794
+ /** Per-instance subagent provenance (spawn parent / outcome / description),
14795
+ * keyed by agentId — source of the /tasks browser's Agents view. */
14796
+ get subagentInstances() {
14797
+ return this.sessionEventHandler.getSubagentInstances();
14798
+ }
14799
+ /** Archive ring for the Agents view: closed instances' last derived rows
14800
+ * (oldest first), the bounded home of agents the live map has released. */
14801
+ get recentSubagentInstances() {
14802
+ return this.sessionEventHandler.getRecentSubagentInstances();
14803
+ }
14006
14804
  getCurrentSessionId() {
14007
14805
  return this.state.appState.sessionId;
14008
14806
  }
@@ -14172,7 +14970,7 @@ var ScreamTUI = class {
14172
14970
  const card = new OnboardingCardComponent({
14173
14971
  colors: this.state.theme.colors,
14174
14972
  onConfigure: () => {
14175
- import("./dispatch-Ba8Dz_v_.mjs").then(({ handleConnectCommand }) => handleConnectCommand(this, "")).finally(() => {
14973
+ import("./dispatch-CUfxgLwq.mjs").then(({ handleConnectCommand }) => handleConnectCommand(this, "")).finally(() => {
14176
14974
  this.restoreEditor();
14177
14975
  onDone?.();
14178
14976
  });
@@ -16179,6 +16977,15 @@ var FileGate = class {
16179
16977
  * consumer of the same SDK (alongside run-shell TUI and run-stream-json),
16180
16978
  * not a separate engine. Zero changes to agent-core or node-sdk.
16181
16979
  */
16980
+ /**
16981
+ * In-memory FIFO caps per web session. The on-disk journal stays complete;
16982
+ * these bounds only stop a long-lived session from growing without limit in
16983
+ * RAM. Dropped rows are tallied so the elision stays observable (see
16984
+ * `getMemoryBounds`) — the same `[…N … elided…]` accounting the transcript
16985
+ * uses, without re-deriving it from a cursor gap.
16986
+ */
16987
+ const MAX_JOURNAL_ENTRIES = 200;
16988
+ const MAX_SESSION_MESSAGES = 200;
16182
16989
  /** Thinking bodies longer than this are delivered as a tail excerpt + on-demand full text. */
16183
16990
  const TRUNCATE_THINKING_LEN = 8 * 1024;
16184
16991
  function truncateThinking(messages) {
@@ -16529,6 +17336,50 @@ const MAIN_TRANSCRIPT_EVENTS = new Set([
16529
17336
  "tool.result"
16530
17337
  ]);
16531
17338
  /**
17339
+ * Identity of a persisted user-message row; used by the fork copy to drop the
17340
+ * memory copy of a row the disk already carries.
17341
+ */
17342
+ function persistedUserMessageKey(entry) {
17343
+ return JSON.stringify([
17344
+ entry.beforeSeq,
17345
+ entry.clientMessageId ?? null,
17346
+ entry.text
17347
+ ]);
17348
+ }
17349
+ /**
17350
+ * Union of the two views of one session's durable journal, for the fork copy.
17351
+ *
17352
+ * `diskEntries` (read back from the source's journal file) is the complete
17353
+ * history and forms the base: the in-memory journal is FIFO-capped
17354
+ * (MAX_JOURNAL_ENTRIES), so a long conversation's memory only holds its tail
17355
+ * while the file keeps every row. `memoryEntries` fills the rows a
17356
+ * fire-and-forget write has not flushed yet; rows the disk already carries are
17357
+ * dropped by seq (journal rows) or identity (user messages), so no line is
17358
+ * doubled.
17359
+ */
17360
+ function mergeForkJournalEntries(diskEntries, memoryEntries) {
17361
+ const journalSeqs = /* @__PURE__ */ new Set();
17362
+ const userMessageKeys = /* @__PURE__ */ new Set();
17363
+ const journalEntries = [];
17364
+ const userMessages = [];
17365
+ const push = (entry) => {
17366
+ if (entry.type === "user_message") {
17367
+ const key = persistedUserMessageKey(entry);
17368
+ if (userMessageKeys.has(key)) return;
17369
+ userMessageKeys.add(key);
17370
+ userMessages.push(entry);
17371
+ return;
17372
+ }
17373
+ if (journalSeqs.has(entry.seq)) return;
17374
+ journalSeqs.add(entry.seq);
17375
+ journalEntries.push(entry);
17376
+ };
17377
+ for (const entry of diskEntries) push(entry);
17378
+ for (const entry of memoryEntries) push(entry);
17379
+ journalEntries.sort((a, b) => a.seq - b.seq);
17380
+ return [...journalEntries, ...userMessages];
17381
+ }
17382
+ /**
16532
17383
  * Whether a journal entry already carries a message (if so it needs no seeding
16533
17384
  * from core history).
16534
17385
  */
@@ -16910,6 +17761,10 @@ var WebSession = class {
16910
17761
  connections = /* @__PURE__ */ new Map();
16911
17762
  journal = [];
16912
17763
  userMessages = [];
17764
+ /** Aggregate count of journal entries dropped by the FIFO cap. */
17765
+ droppedJournalCount = 0;
17766
+ /** Aggregate count of message rows dropped by the FIFO cap. */
17767
+ droppedMessageCount = 0;
16913
17768
  nextSeq = 1;
16914
17769
  epoch = 1;
16915
17770
  cachedStatus = null;
@@ -17001,7 +17856,11 @@ var WebSession = class {
17001
17856
  getTitle() {
17002
17857
  return this.title;
17003
17858
  }
17004
- /** Return in-memory durable journal entries for fork copy (avoids file persistence race). */
17859
+ /**
17860
+ * Return the in-memory durable journal entries (capped tail). The fork copy
17861
+ * takes these only for rows the source's journal file does not carry yet;
17862
+ * the file itself is the complete history.
17863
+ */
17005
17864
  getDurableJournalEntries() {
17006
17865
  const entries = [];
17007
17866
  for (const e of this.journal) if (!e.volatile) entries.push({
@@ -17016,6 +17875,15 @@ var WebSession = class {
17016
17875
  });
17017
17876
  return entries;
17018
17877
  }
17878
+ /**
17879
+ * Drain in-flight persistence writes (journal rows, user messages, metadata).
17880
+ * A fork reads the source's journal file afterwards and must not race a write
17881
+ * still queued behind the serialization gate — the reader would otherwise
17882
+ * mistake a flushed row for unsaved state.
17883
+ */
17884
+ async flushPendingWrites() {
17885
+ await Promise.allSettled(this.pendingWrites);
17886
+ }
17019
17887
  async persistEntry(entry, throwOnError = false) {
17020
17888
  if (!this.homeDir) return;
17021
17889
  const line = JSON.stringify({
@@ -17034,6 +17902,7 @@ var WebSession = class {
17034
17902
  },
17035
17903
  beforeSeq
17036
17904
  });
17905
+ this.trimMessages();
17037
17906
  if (!this.homeDir) return;
17038
17907
  const line = JSON.stringify({
17039
17908
  type: "user_message",
@@ -17064,9 +17933,11 @@ var WebSession = class {
17064
17933
  },
17065
17934
  beforeSeq: um.beforeSeq
17066
17935
  });
17936
+ this.trimMessages();
17067
17937
  } else {
17068
17938
  const je = entry;
17069
17939
  this.journal.push(je);
17940
+ this.trimJournal();
17070
17941
  this.nextSeq = Math.max(this.nextSeq, je.seq + 1);
17071
17942
  if (this.session === null && je.payload.agentId === "main" && je.payload.type === "goal.updated") this.cachedGoal = cloneSnapshot(je.payload.snapshot);
17072
17943
  else if (this.session === null && je.payload.agentId === "main" && je.payload.type === "todo.updated") this.cachedTodos = cloneSnapshot(je.payload.todos);
@@ -17133,6 +18004,7 @@ var WebSession = class {
17133
18004
  ...message,
17134
18005
  seq
17135
18006
  });
18007
+ this.trimMessages();
17136
18008
  seq += 1;
17137
18009
  }
17138
18010
  if (seq - 1 >= this.nextSeq) this.nextSeq = seq;
@@ -17156,6 +18028,44 @@ var WebSession = class {
17156
18028
  permission: this.permission
17157
18029
  };
17158
18030
  }
18031
+ /** FIFO-cap `journal` to `MAX_JOURNAL_ENTRIES`, tallying elided rows. */
18032
+ trimJournal() {
18033
+ while (this.journal.length > MAX_JOURNAL_ENTRIES) {
18034
+ this.journal.shift();
18035
+ this.droppedJournalCount += 1;
18036
+ }
18037
+ }
18038
+ /**
18039
+ * FIFO-cap message stores to `MAX_SESSION_MESSAGES`, tallying elided rows.
18040
+ *
18041
+ * Drop order follows the rendered order (`buildMessages` puts the seeded
18042
+ * core-history rows at the head, live user rows after them), so the eviction
18043
+ * is genuinely oldest-first and the visible tail of the conversation is what
18044
+ * survives.
18045
+ */
18046
+ trimMessages() {
18047
+ while (this.userMessages.length + this.seededMessages.length > MAX_SESSION_MESSAGES) {
18048
+ if (this.seededMessages.length > 0) this.seededMessages.shift();
18049
+ else if (this.userMessages.length > 0) this.userMessages.shift();
18050
+ else break;
18051
+ this.droppedMessageCount += 1;
18052
+ }
18053
+ }
18054
+ /**
18055
+ * Observable surface of the in-memory FIFO caps: current occupancy, the
18056
+ * limits, and the aggregate elided-row tallies (`[…N … elided…]` accounting,
18057
+ * shared with the transcript).
18058
+ */
18059
+ getMemoryBounds() {
18060
+ return {
18061
+ journal: this.journal.length,
18062
+ journalLimit: MAX_JOURNAL_ENTRIES,
18063
+ journalElided: this.droppedJournalCount,
18064
+ messages: this.userMessages.length + this.seededMessages.length,
18065
+ messageLimit: MAX_SESSION_MESSAGES,
18066
+ messagesElided: this.droppedMessageCount
18067
+ };
18068
+ }
17159
18069
  appendEvent(event) {
17160
18070
  const payload = event.type === "goal.updated" || event.type === "todo.updated" ? cloneSnapshot(event) : event;
17161
18071
  const entry = {
@@ -17165,6 +18075,7 @@ var WebSession = class {
17165
18075
  payload
17166
18076
  };
17167
18077
  this.journal.push(entry);
18078
+ this.trimJournal();
17168
18079
  if (!entry.volatile) this.trackPending(this.persistEntry(entry));
17169
18080
  return entry;
17170
18081
  }
@@ -17181,6 +18092,7 @@ var WebSession = class {
17181
18092
  payload
17182
18093
  };
17183
18094
  this.journal.push(entry);
18095
+ this.trimJournal();
17184
18096
  const persisted = this.persistEntry(entry, throwOnPersistenceError);
17185
18097
  this.trackPending(persisted);
17186
18098
  return {
@@ -18488,8 +19400,9 @@ var SessionManager = class {
18488
19400
  }
18489
19401
  /**
18490
19402
  * Fork an active session: harness.forkSession copies the agent state into a
18491
- * new session, and we additionally copy the source's durable journal so the
18492
- * forked web session displays the same prior conversation.
19403
+ * new session, and we additionally copy the source's durable journal from its
19404
+ * journal file — the complete history, not the FIFO-capped in-memory tail —
19405
+ * so the forked web session displays the same prior conversation.
18493
19406
  */
18494
19407
  async forkSession(sourceId) {
18495
19408
  const source = this.sessions.get(sourceId);
@@ -18509,13 +19422,18 @@ var SessionManager = class {
18509
19422
  title,
18510
19423
  onFork: (id) => this.forkSession(id)
18511
19424
  });
18512
- const entries = source.getDurableJournalEntries();
18513
- if (entries.length > 0) {
19425
+ await source.flushPendingWrites();
19426
+ const { entries: diskEntries, corrupt } = await loadJournal(this.homeDir, sourceId);
19427
+ const entries = mergeForkJournalEntries(diskEntries, source.getDurableJournalEntries());
19428
+ webSession.loadFromPersisted(entries);
19429
+ const journalHasContent = webSession.hasAnyAssistantContent();
19430
+ const journalDamaged = corrupt > 0 && !journalHasContent;
19431
+ if (journalDamaged) webSession.markDamaged(`journal has ${corrupt} corrupt lines and no parsed message carries a body`);
19432
+ else if (entries.length > 0) {
18514
19433
  const lines = entries.map((e) => JSON.stringify(e)).join("\n") + "\n";
18515
19434
  await writeFile(getJournalPath(this.homeDir, newId), lines);
18516
19435
  }
18517
- webSession.loadFromPersisted(entries);
18518
- if (!entries.some(isMessageBearingEntry)) try {
19436
+ if (!entries.some(isMessageBearingEntry) || journalDamaged) try {
18519
19437
  const context = await session.getContext();
18520
19438
  webSession.seedHistory(contextHistoryToChatMessages(context.history));
18521
19439
  } catch (error) {
@@ -18993,6 +19911,19 @@ function startWebIdleExit(opts) {
18993
19911
  }
18994
19912
  };
18995
19913
  }
19914
+ /**
19915
+ * Validates the optional `maxDepth` body field of the /rlm route with the same
19916
+ * grammar as /rlm-max-depth (a non-negative integer; 0 = unlimited) via the
19917
+ * shared agent-core helper. Absent / null leaves the cap untouched; any other
19918
+ * invalid value is a 400 so a client cannot silently send a cap that never
19919
+ * lands.
19920
+ */
19921
+ function optionalRlmMaxDepth(raw) {
19922
+ if (raw === void 0 || raw === null) return void 0;
19923
+ const parsed = parseRlmMaxDepthArg(typeof raw === "string" ? raw : String(raw));
19924
+ if (!parsed.ok) throw new HttpError(400, `Invalid rlm max depth: ${parsed.reason}`);
19925
+ return parsed.value;
19926
+ }
18996
19927
  async function handleSessionControlRoutes(req, res, url, method, manager) {
18997
19928
  const statusMatch = new RegExp(`^${API_PREFIX}/sessions/([^/]+)/status$`).exec(url);
18998
19929
  if (statusMatch && method === "GET") {
@@ -19080,7 +20011,7 @@ async function handleSessionControlRoutes(req, res, url, method, manager) {
19080
20011
  try {
19081
20012
  const body = await readJsonBody(req);
19082
20013
  const enabled = optionalBoolean(body, "enabled", true);
19083
- const maxDepth = typeof body["maxDepth"] === "number" ? Number(body["maxDepth"]) : void 0;
20014
+ const maxDepth = optionalRlmMaxDepth(body["maxDepth"]);
19084
20015
  await manager.setRlm(decodeURIComponent(rlmMatch[1]), enabled, maxDepth);
19085
20016
  sendJson(res, 200, { ok: true });
19086
20017
  } catch (error) {