scream-code 0.17.7 → 0.17.9

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-DVZFRvzo.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-BGYq0dnV.mjs";
9
+ import { a as setLocale, i as getLocale, o as t } from "./text-input-dialog-BlSqBYxO.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.7"),
36
+ version: optionalBuildString("0.17.9"),
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);
@@ -4380,6 +4667,89 @@ function isControlCharacter(ch) {
4380
4667
  const code = ch.codePointAt(0) ?? 0;
4381
4668
  return code >= 0 && code <= 31 || code >= 127 && code <= 159;
4382
4669
  }
4670
+ /** Max characters kept across head + tail of the preview combined. */
4671
+ const UI_OUTPUT_MAX_CHARS = 8e3;
4672
+ /** Same marker string as agent-core's `result-builder.ts` TRUNCATION_MARKER. */
4673
+ const TRUNCATION_MARKER = "[...truncated]";
4674
+ function elisionMarker(elided) {
4675
+ return `[…${String(elided)} lines elided…]`;
4676
+ }
4677
+ /**
4678
+ * Bounds one tool-output string to a head + tail preview:
4679
+ * - at most {@link UI_OUTPUT_MAX_HEAD_LINES} lines from the start and
4680
+ * {@link UI_OUTPUT_MAX_TAIL_LINES} lines from the end (the middle is
4681
+ * replaced with `[…N lines elided…]`);
4682
+ * - the kept head + tail text stays within {@link UI_OUTPUT_MAX_CHARS} chars,
4683
+ * dropping whole lines first and hard-slicing a lone oversized line last
4684
+ * (marked `[...truncated]`).
4685
+ *
4686
+ * Returns the input unchanged when it already fits both budgets.
4687
+ */
4688
+ function boundToolOutput(output) {
4689
+ const lines = output.split("\n");
4690
+ const totalLines = lines.length;
4691
+ const overflowLines = totalLines > 80;
4692
+ const overflowChars = output.length > UI_OUTPUT_MAX_CHARS;
4693
+ if (!overflowLines && !overflowChars) return output;
4694
+ const head = lines.slice(0, 40);
4695
+ const tail = overflowLines ? lines.slice(totalLines - 40) : lines.slice(40);
4696
+ let elided = totalLines - head.length - tail.length;
4697
+ const contentBudget = UI_OUTPUT_MAX_CHARS - 2;
4698
+ let headText = head.join("\n");
4699
+ let tailText = tail.join("\n");
4700
+ while (headText.length + tailText.length > contentBudget) {
4701
+ if (head.length > 1 && (tail.length <= 1 || headText.length >= tailText.length)) {
4702
+ head.pop();
4703
+ headText = head.join("\n");
4704
+ elided += 1;
4705
+ continue;
4706
+ }
4707
+ if (tail.length > 1) {
4708
+ tail.shift();
4709
+ tailText = tail.join("\n");
4710
+ elided += 1;
4711
+ continue;
4712
+ }
4713
+ break;
4714
+ }
4715
+ if (headText.length + tailText.length > contentBudget) {
4716
+ const tailKeep = Math.min(tailText.length, Math.floor(contentBudget / 2));
4717
+ const headKeep = Math.min(headText.length, contentBudget - tailKeep);
4718
+ headText = headText.slice(0, headKeep);
4719
+ tailText = tailText.slice(tailText.length - tailKeep);
4720
+ }
4721
+ const marker = elided > 0 ? elisionMarker(elided) : TRUNCATION_MARKER;
4722
+ const parts = [];
4723
+ if (headText.length > 0) parts.push(headText);
4724
+ parts.push(marker);
4725
+ if (tailText.length > 0) parts.push(tailText);
4726
+ return parts.join("\n");
4727
+ }
4728
+ /**
4729
+ * Returns a transcript entry safe to keep in `state.transcriptEntries`:
4730
+ * tool-group entries get their `result.output` replaced with the bounded
4731
+ * preview (a copy — message, brief, titles, args and every other field stay
4732
+ * as they are). Non-tool entries — and tool entries already within budget —
4733
+ * come back untouched (no copy).
4734
+ */
4735
+ function boundEntryForUi(entry) {
4736
+ const toolCallData = entry.toolCallData;
4737
+ if (toolCallData === void 0) return entry;
4738
+ const result = toolCallData.result;
4739
+ if (result === void 0) return entry;
4740
+ const boundedOutput = boundToolOutput(result.output);
4741
+ if (boundedOutput === result.output) return entry;
4742
+ return {
4743
+ ...entry,
4744
+ toolCallData: {
4745
+ ...toolCallData,
4746
+ result: {
4747
+ ...result,
4748
+ output: boundedOutput
4749
+ }
4750
+ }
4751
+ };
4752
+ }
4383
4753
  //#endregion
4384
4754
  //#region src/tui/controllers/streaming-ui.ts
4385
4755
  var StreamingUIController = class StreamingUIController {
@@ -4874,9 +5244,12 @@ var StreamingUIController = class StreamingUIController {
4874
5244
  }
4875
5245
  /**
4876
5246
  * Advance the smooth-render cursor by this frame's character budget.
4877
- * Budget clamps to the measured arrival rate (fast models keep up, slow
4878
- * models never fall behind) with a floor so the stream never freezes and a
4879
- * ceiling so one oversized burst is spread across frames.
5247
+ * The base budget clamps to the measured arrival rate (slow models never
5248
+ * fall behind) with a floor so the stream never freezes and a ceiling so one
5249
+ * oversized burst is spread across frames. Once the backlog passes
5250
+ * SMOOTH_CATCHUP_THRESHOLD, a catch-up budget (backlog / DIVISOR, capped at
5251
+ * MAX_PER_FRAME) raises it so a fast stream converges to a bounded lag
5252
+ * instead of accumulating until finalize.
4880
5253
  */
4881
5254
  advanceAssistantShown() {
4882
5255
  const draft = this._assistantDraft;
@@ -4884,7 +5257,9 @@ var StreamingUIController = class StreamingUIController {
4884
5257
  const measured = getSharedSpeedTracker().getSpeed();
4885
5258
  const tokPerSec = measured > 0 ? measured : 50;
4886
5259
  const requested = charsForTokenBudget(draft.slice(this._shownAssistantLength), tokPerSec * (50 / 1e3));
4887
- const budget = Math.min(25, Math.max(1, requested));
5260
+ let budget = Math.min(25, Math.max(1, requested));
5261
+ const backlog = draft.length - this._shownAssistantLength;
5262
+ if (backlog > 250) budget = Math.max(budget, Math.min(Math.ceil(backlog / 8), 300));
4888
5263
  this._shownAssistantLength = Math.min(draft.length, this._shownAssistantLength + budget);
4889
5264
  return this._shownAssistantLength;
4890
5265
  }
@@ -5134,7 +5509,16 @@ var StreamingUIController = class StreamingUIController {
5134
5509
  this.host.transcriptController.unmarkPending(tc);
5135
5510
  this._pendingToolComponents.delete(toolCallId);
5136
5511
  const entry = this.host.state.transcriptEntries.find((e) => e.toolCallData?.id === toolCallId);
5137
- if (entry?.toolCallData !== void 0) entry.toolCallData.result = result;
5512
+ if (entry?.toolCallData !== void 0) {
5513
+ const bounded = boundEntryForUi({
5514
+ ...entry,
5515
+ toolCallData: {
5516
+ ...entry.toolCallData,
5517
+ result
5518
+ }
5519
+ });
5520
+ entry.toolCallData.result = bounded.toolCallData?.result ?? result;
5521
+ }
5138
5522
  this.host.transcriptController.commit();
5139
5523
  state.ui.requestRender();
5140
5524
  return;
@@ -5533,6 +5917,64 @@ function statusColor(colors, status) {
5533
5917
  function isTerminal(status) {
5534
5918
  return status === "completed" || status === "failed" || status === "killed" || status === "lost";
5535
5919
  }
5920
+ /** Width of the agent-name column in the Agents list (leaves room for the
5921
+ * status word, the instance count and an activity hint on narrow panes). */
5922
+ const AGENT_NAME_COLS = 12;
5923
+ /** Localized word per agent row status: the six live slot states plus the two
5924
+ * terminal outcomes the instance registry can report. */
5925
+ function agentStatusText(status) {
5926
+ switch (status) {
5927
+ case "idle": return t("sidebar.agent_idle");
5928
+ case "working": return t("sidebar.agent_working");
5929
+ case "outputting": return t("sidebar.agent_outputting");
5930
+ case "messaging": return t("sidebar.agent_messaging");
5931
+ case "reworking": return t("sidebar.agent_reworking");
5932
+ case "requesting": return t("sidebar.agent_requesting");
5933
+ case "completed": return t("taskbrowser.agents_completed");
5934
+ case "failed": return t("taskbrowser.agents_failed");
5935
+ }
5936
+ }
5937
+ function agentStatusColor(colors, status) {
5938
+ switch (status) {
5939
+ case "working":
5940
+ case "outputting": return colors.success;
5941
+ case "messaging": return colors.accent;
5942
+ case "reworking":
5943
+ case "requesting": return colors.warning;
5944
+ case "completed": return colors.textMuted;
5945
+ case "failed": return colors.error;
5946
+ case "idle": return colors.textDim;
5947
+ }
5948
+ }
5949
+ /** "Who triggered this agent" line. Each kind names its own evidence, and a
5950
+ * missing clue is voiced as unknown instead of guessed. */
5951
+ function formatAgentSource(source) {
5952
+ switch (source.kind) {
5953
+ case "tool":
5954
+ if (source.name === void 0) return t("taskbrowser.agent_source_unknown");
5955
+ return source.description === void 0 ? source.name : `${source.name} · ${source.description}`;
5956
+ case "agent": {
5957
+ const via = source.name === void 0 ? t("taskbrowser.agent_source_unknown") : t("taskbrowser.agent_source_via", { name: source.name });
5958
+ return source.description === void 0 ? via : `${via} · ${source.description}`;
5959
+ }
5960
+ case "rlm": {
5961
+ const rlm = t("taskbrowser.agent_source_rlm");
5962
+ return source.name === void 0 ? rlm : `${rlm} · ${t("taskbrowser.agent_source_via", { name: source.name })}`;
5963
+ }
5964
+ case "main": return t("taskbrowser.agent_source_main");
5965
+ case "unknown": return t("taskbrowser.agent_source_unknown");
5966
+ }
5967
+ }
5968
+ /** Full spawn chain, root first: `main → researcher → coder`, `…` when the
5969
+ * walk stopped early (missing link / cycle / depth cap). */
5970
+ function formatAgentChain(agent) {
5971
+ const parents = [...agent.ancestors].toReversed();
5972
+ return `${[
5973
+ t("taskbrowser.agent_main"),
5974
+ ...parents,
5975
+ agent.type
5976
+ ].join(" → ")}${agent.chainTruncated ? " → …" : ""}`;
5977
+ }
5536
5978
  function formatRelativeTime$2(ts) {
5537
5979
  if (ts === null || ts === void 0 || !Number.isFinite(ts) || ts <= 0) return "";
5538
5980
  const diffSec = Math.floor(Math.max(0, Date.now() - ts) / 1e3);
@@ -5560,8 +6002,22 @@ function fitExactly$1(line, width) {
5560
6002
  return padToWidth$1(s, width);
5561
6003
  }
5562
6004
  function visibleTasks(tasks, filter) {
5563
- if (filter === "all") return [...tasks];
5564
- return tasks.filter((t) => !isTerminal(t.status));
6005
+ if (filter === "active") return tasks.filter((t) => !isTerminal(t.status));
6006
+ return [...tasks];
6007
+ }
6008
+ /**
6009
+ * Keep `selectedIndex` inside the window `scrollOffset … scrollOffset+rows-1`,
6010
+ * clamped to the list bounds. Shared by the task list and the agent list.
6011
+ */
6012
+ function clampListScroll(selectedIndex, scrollOffset, total, visibleRows) {
6013
+ if (visibleRows <= 0) return 0;
6014
+ let next = scrollOffset;
6015
+ if (selectedIndex < next) next = selectedIndex;
6016
+ else if (selectedIndex >= next + visibleRows) next = selectedIndex - visibleRows + 1;
6017
+ const maxScroll = Math.max(0, total - visibleRows);
6018
+ if (next < 0) next = 0;
6019
+ if (next > maxScroll) next = maxScroll;
6020
+ return next;
5565
6021
  }
5566
6022
  function compareTasks(a, b) {
5567
6023
  const aTerminal = isTerminal(a.status);
@@ -5601,6 +6057,12 @@ var TasksBrowserApp = class extends Container {
5601
6057
  sortedVisible;
5602
6058
  selectedIndex = 0;
5603
6059
  listScroll = 0;
6060
+ /** Agents view selection — component-local: agent rows are read-only, so
6061
+ * nothing needs to travel back to the controller. `selectedAgentKey` is the
6062
+ * anchor: rows re-sort as statuses change, the index must not decide. */
6063
+ agentIndex = 0;
6064
+ agentScroll = 0;
6065
+ selectedAgentKey = void 0;
5604
6066
  pendingStopTaskId = void 0;
5605
6067
  pendingStopTimer = void 0;
5606
6068
  constructor(props, terminal) {
@@ -5614,6 +6076,7 @@ var TasksBrowserApp = class extends Container {
5614
6076
  this.props = next;
5615
6077
  this.sortedVisible = visibleTasks(next.tasks, next.filter).toSorted(compareTasks);
5616
6078
  this.syncSelectionFromProps();
6079
+ this.syncAgentSelectionFromProps();
5617
6080
  if (this.pendingStopTaskId !== void 0) {
5618
6081
  const task = next.tasks.find((t) => t.taskId === this.pendingStopTaskId);
5619
6082
  if (task === void 0 || isTerminal(task.status)) this.clearPendingStop();
@@ -5635,6 +6098,34 @@ var TasksBrowserApp = class extends Container {
5635
6098
  }
5636
6099
  if (this.selectedIndex >= this.sortedVisible.length) this.selectedIndex = this.sortedVisible.length - 1;
5637
6100
  }
6101
+ /** Keep the agents selection pinned to the same slot type across refreshes
6102
+ * (rows re-sort as statuses change); fall back to clamping. */
6103
+ syncAgentSelectionFromProps() {
6104
+ const agents = this.props.agents;
6105
+ if (agents.length === 0) {
6106
+ this.agentIndex = 0;
6107
+ this.agentScroll = 0;
6108
+ this.selectedAgentKey = void 0;
6109
+ return;
6110
+ }
6111
+ if (this.selectedAgentKey !== void 0) {
6112
+ const byKey = agents.findIndex((agent) => agent.key === this.selectedAgentKey);
6113
+ if (byKey !== -1) {
6114
+ this.agentIndex = byKey;
6115
+ return;
6116
+ }
6117
+ }
6118
+ if (this.agentIndex >= agents.length) this.agentIndex = agents.length - 1;
6119
+ this.selectedAgentKey = agents[this.agentIndex]?.key;
6120
+ }
6121
+ /** Move the agents cursor (rows are read-only: no controller round-trip). */
6122
+ selectAgent(index) {
6123
+ const agents = this.props.agents;
6124
+ if (agents.length === 0) return;
6125
+ this.agentIndex = Math.min(agents.length - 1, Math.max(0, index));
6126
+ this.selectedAgentKey = agents[this.agentIndex]?.key;
6127
+ this.invalidate();
6128
+ }
5638
6129
  clearPendingStop() {
5639
6130
  this.pendingStopTaskId = void 0;
5640
6131
  if (this.pendingStopTimer !== void 0) {
@@ -5642,12 +6133,21 @@ var TasksBrowserApp = class extends Container {
5642
6133
  this.pendingStopTimer = void 0;
5643
6134
  }
5644
6135
  }
6136
+ /**
6137
+ * Component-local teardown, called by the controller's close() before the
6138
+ * component is swapped out: the pending-stop confirm timer must not
6139
+ * outlive the browser.
6140
+ */
6141
+ close() {
6142
+ this.clearPendingStop();
6143
+ }
5645
6144
  emitSelect() {
5646
6145
  const task = this.sortedVisible[this.selectedIndex];
5647
6146
  if (task) this.props.onSelect(task.taskId);
5648
6147
  }
5649
6148
  handleInput(data) {
5650
6149
  const k = printableChar(data);
6150
+ const agentsMode = this.props.filter === "agents";
5651
6151
  if (this.pendingStopTaskId !== void 0) {
5652
6152
  if (k === "y" || k === "Y") {
5653
6153
  const taskId = this.pendingStopTaskId;
@@ -5665,6 +6165,10 @@ var TasksBrowserApp = class extends Container {
5665
6165
  return;
5666
6166
  }
5667
6167
  if (matchesKey(data, Key.up) || k === "k") {
6168
+ if (agentsMode) {
6169
+ this.selectAgent(this.agentIndex - 1);
6170
+ return;
6171
+ }
5668
6172
  if (this.sortedVisible.length === 0) return;
5669
6173
  this.selectedIndex = Math.max(0, this.selectedIndex - 1);
5670
6174
  this.emitSelect();
@@ -5672,6 +6176,10 @@ var TasksBrowserApp = class extends Container {
5672
6176
  return;
5673
6177
  }
5674
6178
  if (matchesKey(data, Key.down) || k === "j") {
6179
+ if (agentsMode) {
6180
+ this.selectAgent(this.agentIndex + 1);
6181
+ return;
6182
+ }
5675
6183
  if (this.sortedVisible.length === 0) return;
5676
6184
  this.selectedIndex = Math.min(this.sortedVisible.length - 1, this.selectedIndex + 1);
5677
6185
  this.emitSelect();
@@ -5686,6 +6194,7 @@ var TasksBrowserApp = class extends Container {
5686
6194
  this.props.onRefresh();
5687
6195
  return;
5688
6196
  }
6197
+ if (agentsMode) return;
5689
6198
  if (k === "s" || k === "S") {
5690
6199
  const task = this.sortedVisible[this.selectedIndex];
5691
6200
  if (task === void 0) return;
@@ -5729,15 +6238,26 @@ var TasksBrowserApp = class extends Container {
5729
6238
  renderHeader(width) {
5730
6239
  const colors = this.props.colors;
5731
6240
  const title = chalk.hex(colors.primary).bold(" TASK BROWSER ");
5732
- const filterText = chalk.hex(colors.textMuted)(` filter=${this.props.filter === "all" ? "ALL" : "ACTIVE"} `);
5733
- const counts = countByStatus(this.props.tasks);
5734
- const countSegments = [];
5735
- if (counts.running > 0) countSegments.push(chalk.hex(colors.success)(` ${String(counts.running)} ${t("taskbrowser.running")} `));
5736
- if (counts.awaiting > 0) countSegments.push(chalk.hex(colors.warning)(` ${String(counts.awaiting)} ${t("taskbrowser.awaiting")} `));
5737
- if (counts.completed > 0) countSegments.push(chalk.hex(colors.textDim)(` ${String(counts.completed)} ${t("taskbrowser.completed")} `));
5738
- if (counts.terminalFailed > 0) countSegments.push(chalk.hex(colors.error)(` ${String(counts.terminalFailed)} ${t("taskbrowser.interrupted")} `));
5739
- const totals = chalk.hex(colors.textMuted)(` ${String(this.props.tasks.length)} ${t("taskbrowser.total")} `);
5740
- return fitExactly$1(title + filterText + countSegments.join("") + totals, width);
6241
+ const filterLabel = this.props.filter === "all" ? "ALL" : this.props.filter === "active" ? "ACTIVE" : "AGENTS";
6242
+ const filterText = chalk.hex(colors.textMuted)(` filter=${filterLabel} `);
6243
+ const segments = [];
6244
+ if (this.props.filter === "agents") {
6245
+ const live = this.props.agents.filter((agent) => agent.live).length;
6246
+ const ended = this.props.agents.length - live;
6247
+ if (live > 0) segments.push(chalk.hex(colors.success)(` ${String(live)} ${t("taskbrowser.agents_live")} `));
6248
+ if (ended > 0) segments.push(chalk.hex(colors.textDim)(` ${String(ended)} ${t("taskbrowser.agents_ended")} `));
6249
+ if (this.props.agents.length === 0) segments.push(chalk.hex(colors.textMuted)(` 0 ${t("taskbrowser.agents")} `));
6250
+ } else {
6251
+ const counts = countByStatus(this.props.tasks);
6252
+ if (counts.running > 0) segments.push(chalk.hex(colors.success)(` ${String(counts.running)} ${t("taskbrowser.running")} `));
6253
+ if (counts.awaiting > 0) segments.push(chalk.hex(colors.warning)(` ${String(counts.awaiting)} ${t("taskbrowser.awaiting")} `));
6254
+ if (counts.completed > 0) segments.push(chalk.hex(colors.textDim)(` ${String(counts.completed)} ${t("taskbrowser.completed")} `));
6255
+ if (counts.terminalFailed > 0) segments.push(chalk.hex(colors.error)(` ${String(counts.terminalFailed)} ${t("taskbrowser.interrupted")} `));
6256
+ segments.push(chalk.hex(colors.textMuted)(` ${String(this.props.tasks.length)} ${t("taskbrowser.total")} `));
6257
+ const liveAgents = this.props.agents.filter((agent) => agent.live).length;
6258
+ if (liveAgents > 0) segments.push(chalk.hex(colors.success)(` ${String(liveAgents)} ${t("taskbrowser.agents")} `));
6259
+ }
6260
+ return fitExactly$1(title + filterText + segments.join(""), width);
5741
6261
  }
5742
6262
  renderFooter(width) {
5743
6263
  const colors = this.props.colors;
@@ -5747,14 +6267,19 @@ var TasksBrowserApp = class extends Container {
5747
6267
  const warn = (text) => chalk.hex(colors.warning).bold(text);
5748
6268
  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);
5749
6269
  }
5750
- const left = [
6270
+ const left = (this.props.filter === "agents" ? [
6271
+ ` ${key("↑↓")} ${dim(t("taskbrowser.select"))}`,
6272
+ `${key("Tab")} ${dim(t("taskbrowser.filter"))}`,
6273
+ `${key("R")} ${dim(t("taskbrowser.refresh"))}`,
6274
+ `${key("Q/Esc")} ${dim(t("taskbrowser.exit"))} `
6275
+ ] : [
5751
6276
  ` ${key("↑↓")} ${dim(t("taskbrowser.select"))}`,
5752
6277
  `${key("Enter/O")} ${dim(t("taskbrowser.output"))}`,
5753
6278
  `${key("S")} ${dim(t("taskbrowser.stop_action"))}`,
5754
6279
  `${key("R")} ${dim(t("taskbrowser.refresh"))}`,
5755
6280
  `${key("Tab")} ${dim(t("taskbrowser.filter"))}`,
5756
6281
  `${key("Q/Esc")} ${dim(t("taskbrowser.exit"))} `
5757
- ].join(" ");
6282
+ ]).join(" ");
5758
6283
  const flash = this.props.flashMessage;
5759
6284
  if (flash !== void 0 && flash.length > 0) {
5760
6285
  const flashStyled = chalk.hex(colors.warning)(` ${flash} `);
@@ -5793,6 +6318,7 @@ var TasksBrowserApp = class extends Container {
5793
6318
  return lines;
5794
6319
  }
5795
6320
  renderListFrame(width, height) {
6321
+ if (this.props.filter === "agents") return this.renderAgentsFrame(width, height);
5796
6322
  const title = `Tasks [${this.props.filter}]`;
5797
6323
  const innerHeight = Math.max(0, height - 2);
5798
6324
  if (this.sortedVisible.length === 0) {
@@ -5813,6 +6339,43 @@ var TasksBrowserApp = class extends Container {
5813
6339
  while (lines.length < innerHeight) lines.push("");
5814
6340
  return this.renderFrame(title, lines, width, height);
5815
6341
  }
6342
+ /** Left pane, `agents` filter: one row per subagent type with history (live
6343
+ * ones first), including types whose slot the cap pushed out and that only
6344
+ * the instance registry knows; read-only — the detail pane carries the full
6345
+ * provenance. */
6346
+ renderAgentsFrame(width, height) {
6347
+ const colors = this.props.colors;
6348
+ const agents = this.props.agents;
6349
+ const innerHeight = Math.max(0, height - 2);
6350
+ const title = `${t("taskbrowser.agents_title")} [${String(agents.length)}]`;
6351
+ if (agents.length === 0) {
6352
+ const lines = [chalk.hex(colors.textMuted)(t("taskbrowser.agents_empty"))];
6353
+ while (lines.length < innerHeight) lines.push("");
6354
+ return this.renderFrame(title, lines, width, height);
6355
+ }
6356
+ this.agentScroll = clampListScroll(this.agentIndex, this.agentScroll, agents.length, innerHeight);
6357
+ const start = this.agentScroll;
6358
+ const window = agents.slice(start, start + innerHeight);
6359
+ const innerWidth = width - 2;
6360
+ const lines = [];
6361
+ for (const [vi, agent] of window.entries()) {
6362
+ const index = start + vi;
6363
+ lines.push(this.renderAgentRow(agent, index === this.agentIndex, innerWidth));
6364
+ }
6365
+ while (lines.length < innerHeight) lines.push("");
6366
+ return this.renderFrame(title, lines, width, height);
6367
+ }
6368
+ renderAgentRow(agent, selected, innerWidth) {
6369
+ const colors = this.props.colors;
6370
+ const pointer = selected ? "> " : " ";
6371
+ 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)}`) : ""}`;
6372
+ const budget = innerWidth - visibleWidth(head) - 1;
6373
+ if (budget < 6) return fitExactly$1(head, innerWidth);
6374
+ const hint = singleLine$2(agent.detail ?? agent.description ?? "");
6375
+ if (hint.length === 0) return fitExactly$1(head, innerWidth);
6376
+ const trimmed = truncateToWidth(hint, budget, ELLIPSIS$3);
6377
+ return fitExactly$1(`${head} ${chalk.hex(colors.textDim)(trimmed)}`, innerWidth);
6378
+ }
5816
6379
  renderListRow(task, selected, innerWidth) {
5817
6380
  const colors = this.props.colors;
5818
6381
  const pointer = selected ? "> " : " ";
@@ -5829,15 +6392,7 @@ var TasksBrowserApp = class extends Container {
5829
6392
  return fitExactly$1(`${prefix} ${chalk.hex(colors.text)(desc)}`, innerWidth);
5830
6393
  }
5831
6394
  adjustScroll(visibleRows) {
5832
- if (visibleRows <= 0) {
5833
- this.listScroll = 0;
5834
- return;
5835
- }
5836
- if (this.selectedIndex < this.listScroll) this.listScroll = this.selectedIndex;
5837
- else if (this.selectedIndex >= this.listScroll + visibleRows) this.listScroll = this.selectedIndex - visibleRows + 1;
5838
- const maxScroll = Math.max(0, this.sortedVisible.length - visibleRows);
5839
- if (this.listScroll < 0) this.listScroll = 0;
5840
- if (this.listScroll > maxScroll) this.listScroll = maxScroll;
6395
+ this.listScroll = clampListScroll(this.selectedIndex, this.listScroll, this.sortedVisible.length, visibleRows);
5841
6396
  }
5842
6397
  renderRightStack(width, height) {
5843
6398
  const detailHeight = Math.max(8, Math.min(Math.floor(height * .4), height - 5));
@@ -5845,6 +6400,7 @@ var TasksBrowserApp = class extends Container {
5845
6400
  return [...this.renderDetailFrame(width, detailHeight), ...this.renderPreviewFrame(width, previewHeight)];
5846
6401
  }
5847
6402
  renderDetailFrame(width, height) {
6403
+ if (this.props.filter === "agents") return this.renderAgentDetailFrame(width, height);
5848
6404
  const colors = this.props.colors;
5849
6405
  const innerHeight = Math.max(0, height - 2);
5850
6406
  const task = this.sortedVisible[this.selectedIndex];
@@ -5871,9 +6427,43 @@ var TasksBrowserApp = class extends Container {
5871
6427
  while (lines.length < innerHeight) lines.push("");
5872
6428
  return this.renderFrame(t("taskbrowser.detail"), lines, width, height);
5873
6429
  }
6430
+ /** Right-top pane, `agents` filter: provenance of the selected agent —
6431
+ * type, live status, spawn description, trigger source, parent chain,
6432
+ * latest activity and the current instance id. */
6433
+ renderAgentDetailFrame(width, height) {
6434
+ const colors = this.props.colors;
6435
+ const innerHeight = Math.max(0, height - 2);
6436
+ const agent = this.props.agents[this.agentIndex];
6437
+ if (agent === void 0) {
6438
+ const lines = [chalk.hex(colors.textMuted)(t("taskbrowser.agents_empty"))];
6439
+ while (lines.length < innerHeight) lines.push("");
6440
+ return this.renderFrame(t("taskbrowser.agent_detail"), lines, width, height);
6441
+ }
6442
+ const label = (text) => chalk.hex(colors.textMuted)(text.padEnd(14));
6443
+ const value = (text) => chalk.hex(colors.text)(text);
6444
+ const countSuffix = agent.count > 1 ? chalk.hex(colors.textMuted)(` ×${String(agent.count)}`) : "";
6445
+ const lines = [
6446
+ `${label(t("taskbrowser.agent_type"))}${value(agent.type)}`,
6447
+ `${label(t("taskbrowser.status"))}${chalk.hex(agentStatusColor(colors, agent.status))(agentStatusText(agent.status))}${countSuffix}`,
6448
+ `${label(t("taskbrowser.agent_source"))}${value(singleLine$2(formatAgentSource(agent.source)))}`,
6449
+ `${label(t("taskbrowser.agent_chain"))}${chalk.hex(colors.textMuted)(formatAgentChain(agent))}`
6450
+ ];
6451
+ if (agent.description !== void 0 && agent.description.length > 0) lines.push(`${label(t("taskbrowser.description"))}${value(singleLine$2(agent.description))}`);
6452
+ if (agent.detail !== void 0 && agent.detail.length > 0) lines.push(`${label(t("taskbrowser.agent_activity"))}${value(singleLine$2(agent.detail))}`);
6453
+ const when = formatRelativeTime$2(agent.lastActivityAt);
6454
+ if (when.length > 0) lines.push(`${label(t("taskbrowser.time"))}${chalk.hex(colors.textMuted)(when)}`);
6455
+ if (agent.instanceId !== void 0) lines.push(`${label(t("taskbrowser.agent_instance"))}${chalk.hex(colors.textMuted)(agent.instanceId)}`);
6456
+ while (lines.length < innerHeight) lines.push("");
6457
+ return this.renderFrame(t("taskbrowser.agent_detail"), lines, width, height);
6458
+ }
5874
6459
  renderPreviewFrame(width, height) {
5875
6460
  const colors = this.props.colors;
5876
6461
  const innerHeight = Math.max(0, height - 2);
6462
+ if (this.props.filter === "agents") {
6463
+ const lines = [chalk.hex(colors.textMuted)(t("taskbrowser.agents_no_output"))];
6464
+ while (lines.length < innerHeight) lines.push("");
6465
+ return this.renderFrame("Preview Output", lines, width, height);
6466
+ }
5877
6467
  if (this.sortedVisible[this.selectedIndex] === void 0) {
5878
6468
  const lines = [chalk.hex(colors.textMuted)("No task selected.")];
5879
6469
  while (lines.length < innerHeight) lines.push("");
@@ -5922,6 +6512,7 @@ var TasksBrowserController = class {
5922
6512
  const selectedTaskId = this.pickInitialSelection(tasks, filter);
5923
6513
  const component = new TasksBrowserApp({
5924
6514
  tasks,
6515
+ agents: this.snapshotAgentRows(),
5925
6516
  filter,
5926
6517
  selectedTaskId,
5927
6518
  tailOutput: void 0,
@@ -5959,6 +6550,7 @@ var TasksBrowserController = class {
5959
6550
  if (browser.viewer !== void 0) this.closeOutputViewer();
5960
6551
  if (browser.pollTimer !== void 0) clearInterval(browser.pollTimer);
5961
6552
  if (browser.flashTimer !== void 0) clearTimeout(browser.flashTimer);
6553
+ browser.component.close();
5962
6554
  state.ui.setLayoutRoot(browser.savedLayoutRoot);
5963
6555
  this.host.setTasksBrowser(void 0);
5964
6556
  state.ui.setFocus(state.editor);
@@ -6029,6 +6621,7 @@ var TasksBrowserController = class {
6029
6621
  if (browser === void 0) return;
6030
6622
  browser.component.setProps({
6031
6623
  tasks,
6624
+ agents: this.snapshotAgentRows(),
6032
6625
  filter: browser.filter,
6033
6626
  selectedTaskId: browser.selectedTaskId,
6034
6627
  tailOutput: browser.tailOutput,
@@ -6039,6 +6632,13 @@ var TasksBrowserController = class {
6039
6632
  });
6040
6633
  this.host.state.ui.requestRender();
6041
6634
  }
6635
+ /** Agents view rows: the live slot state machine (authoritative status)
6636
+ * enriched with the per-instance provenance registry, plus the archive ring
6637
+ * for agents whose registry record has already been released. Rebuilt on
6638
+ * every refresh/repaint — all three sources are in-memory snapshots. */
6639
+ snapshotAgentRows() {
6640
+ return buildAgentRows(this.host.subagentSlots, this.host.subagentInstances, this.host.recentSubagentInstances);
6641
+ }
6042
6642
  buildCallbacks() {
6043
6643
  return {
6044
6644
  onSelect: (taskId) => {
@@ -6077,7 +6677,7 @@ var TasksBrowserController = class {
6077
6677
  handleToggleFilter() {
6078
6678
  const browser = this.host.state.tasksBrowser;
6079
6679
  if (browser === void 0) return;
6080
- browser.filter = browser.filter === "all" ? "active" : "all";
6680
+ browser.filter = browser.filter === "all" ? "active" : browser.filter === "active" ? "agents" : "all";
6081
6681
  this.repaint();
6082
6682
  }
6083
6683
  handleRefresh() {
@@ -6346,6 +6946,28 @@ function basenameLike(raw) {
6346
6946
  return raw.split(/[\\/]/).filter((part) => part.length > 0).at(-1) ?? raw;
6347
6947
  }
6348
6948
  //#endregion
6949
+ //#region src/tui/utils/collapse-stub.ts
6950
+ /**
6951
+ * The transcript bounds its memory in two places, and both stand in for the
6952
+ * rows they fold away with the same summary line: the shadow entry array
6953
+ * (`transcript-controller`) folds its oldest entries into one stub entry, and
6954
+ * the committed render tree (`committed-transcript`) folds its oldest child
6955
+ * components into one stub row. One definition of the wording keeps the two
6956
+ * folds reading alike — and lets the array fold read its own count back when
6957
+ * it folds again.
6958
+ */
6959
+ /** Matches {@link formatCollapsedEntries} output, capturing the count. */
6960
+ const COLLAPSE_STUB_PATTERN = /^…\((\d+) earlier entries collapsed\)$/;
6961
+ /** Summary line that stands in for `count` folded-away entries. */
6962
+ function formatCollapsedEntries(count) {
6963
+ return `…(${String(count)} earlier entries collapsed)`;
6964
+ }
6965
+ /** How many entries a summary line stands in for; 0 when `text` is not one. */
6966
+ function parseCollapsedEntries(text) {
6967
+ const digits = COLLAPSE_STUB_PATTERN.exec(text)?.[1];
6968
+ return digits === void 0 ? 0 : Number(digits);
6969
+ }
6970
+ //#endregion
6349
6971
  //#region src/tui/components/transcript/committed-transcript.ts
6350
6972
  var CommittedMessageComponent = class {
6351
6973
  entry;
@@ -6415,6 +7037,9 @@ var CommittedTranscriptComponent = class extends CachedContainer {
6415
7037
  header;
6416
7038
  colors;
6417
7039
  committedCount = 0;
7040
+ /** Rows the retention cap has folded into the summary stub. */
7041
+ collapsedCount = 0;
7042
+ collapseStub;
6418
7043
  constructor(colors) {
6419
7044
  super();
6420
7045
  this.colors = colors;
@@ -6424,6 +7049,12 @@ var CommittedTranscriptComponent = class extends CachedContainer {
6424
7049
  getCount() {
6425
7050
  return this.committedCount;
6426
7051
  }
7052
+ /** Rows the tree folded into its summary stub. They still count toward
7053
+ * {@link getCount()}: folding changes what is on screen, never what was
7054
+ * committed. */
7055
+ getCollapsedCount() {
7056
+ return this.collapsedCount;
7057
+ }
6427
7058
  setCount(count) {
6428
7059
  this.committedCount = count;
6429
7060
  if (count === 0) this.header.setText("");
@@ -6432,11 +7063,46 @@ var CommittedTranscriptComponent = class extends CachedContainer {
6432
7063
  }
6433
7064
  appendEntry(entry, colors) {
6434
7065
  this.addChild(new CommittedMessageComponent(entry, colors));
7066
+ this.enforceRetentionCap();
7067
+ }
7068
+ /**
7069
+ * Holds the tree at its retention point: the entry children it renders —
7070
+ * the summary stub included — never exceed MAX_COMMITTED_ENTRIES. Past that
7071
+ * the oldest rows fold into the stub, the same semantics as the entry-array
7072
+ * cap, so the child list (and the render cache every child carries) stops
7073
+ * growing with the session. The stub takes one slot of the budget, so the
7074
+ * fold lands on stub + (MAX - 1) rows: the cap is the actually retained set.
7075
+ */
7076
+ enforceRetentionCap() {
7077
+ if (this.children.length - 1 <= 500) return;
7078
+ let stub = this.collapseStub;
7079
+ if (stub === void 0) {
7080
+ stub = new Text("", 0, 0);
7081
+ this.collapseStub = stub;
7082
+ this.children.splice(1, 0, stub);
7083
+ }
7084
+ const fold = this.children.length - 2 - 499;
7085
+ this.children.splice(2, fold);
7086
+ this.collapsedCount += fold;
7087
+ stub.setText(` ${chalk.hex(this.colors.textDim)(formatCollapsedEntries(this.collapsedCount))}`);
6435
7088
  this.invalidate();
6436
7089
  }
6437
7090
  };
6438
- //#endregion
6439
- //#region src/tui/controllers/transcript-controller.ts
7091
+ /** Id of the aggregate stub that stands in for the folded oldest entries. */
7092
+ const COLLAPSE_STUB_ID = "transcript-collapse-stub";
7093
+ function makeCollapseStub(count) {
7094
+ return {
7095
+ id: COLLAPSE_STUB_ID,
7096
+ kind: "status",
7097
+ renderMode: "plain",
7098
+ content: formatCollapsedEntries(count)
7099
+ };
7100
+ }
7101
+ /** Entries an existing stub already stands in for (0 when it is not one). */
7102
+ function stubCount(entry) {
7103
+ if (entry === void 0 || entry.id !== COLLAPSE_STUB_ID) return 0;
7104
+ return parseCollapsedEntries(entry.content);
7105
+ }
6440
7106
  var TranscriptController = class TranscriptController {
6441
7107
  host;
6442
7108
  welcomeComponent;
@@ -6446,6 +7112,17 @@ var TranscriptController = class TranscriptController {
6446
7112
  * ticker of the card that is still claiming the task is running. */
6447
7113
  liveNoticesByTrackingId = /* @__PURE__ */ new Map();
6448
7114
  pendingComponents = /* @__PURE__ */ new Set();
7115
+ /** Monotonic count of composition changes to `state.transcriptEntries`:
7116
+ * +1 per ingest and +1 per cap fold. Aggregates that memoize over the entry
7117
+ * list key on this — the fold holds the array length at the cap while
7118
+ * replacing the oldest rows, so a length-only key can go stale. */
7119
+ ingestCount = 0;
7120
+ /** Contributions of the rows the entry cap has already folded out of the
7121
+ * array. Kept separate from the rows still held, so aggregates that must
7122
+ * stay monotonic over a long session (the sidebar's session stats) can add
7123
+ * them back — see {@link getFoldedEntryCounts}. */
7124
+ foldedTurns = 0;
7125
+ foldedToolCalls = 0;
6449
7126
  /** Max live transcript children before the oldest are folded into the
6450
7127
  * committed single-line summary. Overridable via SCREAM_TRANSCRIPT_LIVE_LIMIT
6451
7128
  * (mirrors a commit-fold approach for bounding ultra-long sessions). */
@@ -6578,12 +7255,55 @@ var TranscriptController = class TranscriptController {
6578
7255
  default: return null;
6579
7256
  }
6580
7257
  }
7258
+ /**
7259
+ * The transcript's single bounded ingress: every entry reaches
7260
+ * `state.transcriptEntries` through here — `appendEntry` for rows that mount
7261
+ * their own component, and `ScreamTUI.pushTranscriptEntry` for the live
7262
+ * streaming rows that already own one. One place applies the UI preview
7263
+ * bound (`boundEntryForUi`) and the entry-count cap, so no producer can grow
7264
+ * the array without bound, whichever path it comes in on.
7265
+ *
7266
+ * Returns the entry as stored: a bounded copy when the bound rewrote it,
7267
+ * otherwise the very entry handed in — identity matters to callers that keep
7268
+ * mutating a live draft (assistant text, thinking) after pushing it.
7269
+ */
7270
+ ingestEntry(entry) {
7271
+ const boundedEntry = boundEntryForUi(entry);
7272
+ this.ingestCount += 1;
7273
+ this.host.state.transcriptEntries.push(boundedEntry);
7274
+ this.enforceEntryCap();
7275
+ return boundedEntry;
7276
+ }
7277
+ /**
7278
+ * Monotonic count of transcript composition changes: one per ingest, plus
7279
+ * one per cap fold. Consumers that memoize aggregates over
7280
+ * `state.transcriptEntries` (the sidebar session stats) key on this instead
7281
+ * of `length` alone, because a fold lands the array back on the same length
7282
+ * while replacing its oldest rows.
7283
+ */
7284
+ getIngestCount() {
7285
+ return this.ingestCount;
7286
+ }
7287
+ /**
7288
+ * What the entry cap has already folded out of `state.transcriptEntries`:
7289
+ * user turns and real tool calls (compression markers ride kind 'tool_call'
7290
+ * but are not invocations — same rule as the retained-row scan). Aggregates
7291
+ * that must be monotonic over a long session — the sidebar's session stats —
7292
+ * add these to the rows the array still holds, because the array stops
7293
+ * holding the oldest ones at the cap.
7294
+ */
7295
+ getFoldedEntryCounts() {
7296
+ return {
7297
+ turns: this.foldedTurns,
7298
+ toolCalls: this.foldedToolCalls
7299
+ };
7300
+ }
6581
7301
  appendEntry(entry) {
6582
- this.host.state.transcriptEntries.push(entry);
6583
- const component = this.createComponent(entry);
7302
+ const boundedEntry = this.ingestEntry(entry);
7303
+ const component = this.createComponent(boundedEntry);
6584
7304
  if (component) {
6585
- this.liveComponentToEntry.set(component, entry);
6586
- const status = entry.backgroundAgentStatus;
7305
+ this.liveComponentToEntry.set(component, boundedEntry);
7306
+ const status = boundedEntry.backgroundAgentStatus;
6587
7307
  if (status !== void 0 && component instanceof BackgroundAgentStatusComponent) {
6588
7308
  const trackingId = status.trackingId;
6589
7309
  if (trackingId !== void 0) if (status.phase === "started") this.liveNoticesByTrackingId.set(trackingId, component);
@@ -6597,6 +7317,24 @@ var TranscriptController = class TranscriptController {
6597
7317
  }
6598
7318
  return component ?? null;
6599
7319
  }
7320
+ /**
7321
+ * Keeps `state.transcriptEntries` within {@link MAX_TRANSCRIPT_ENTRIES} by
7322
+ * folding the oldest rows into one aggregate stub. A stub already at the head
7323
+ * keeps that slot and accumulates the count, so the array stays bounded no
7324
+ * matter how long the session runs.
7325
+ */
7326
+ enforceEntryCap() {
7327
+ const entries = this.host.state.transcriptEntries;
7328
+ if (entries.length <= 500) return;
7329
+ const head = entries[0];
7330
+ const isStub = head !== void 0 && head.id === COLLAPSE_STUB_ID;
7331
+ const dropped = entries.splice(0, entries.length - 500 + 1);
7332
+ for (const entry of dropped) if (entry.kind === "user") this.foldedTurns += 1;
7333
+ else if (entry.kind === "tool_call" && entry.compactionData === void 0) this.foldedToolCalls += 1;
7334
+ const folded = dropped.length - (isStub ? 1 : 0);
7335
+ entries.unshift(makeCollapseStub(stubCount(head) + folded));
7336
+ this.ingestCount += 1;
7337
+ }
6600
7338
  /** Append a suffix to the given turn's final assistant message last line.
6601
7339
  * Used to place the turn elapsed marker flush against the last character
6602
7340
  * of the assistant's reply. Turn-scoped so a tool-only turn never stamps
@@ -6651,6 +7389,8 @@ var TranscriptController = class TranscriptController {
6651
7389
  streamingUI.endActivityGroup();
6652
7390
  streamingUI.flushPendingApprovals();
6653
7391
  state.transcriptEntries = [];
7392
+ this.foldedTurns = 0;
7393
+ this.foldedToolCalls = 0;
6654
7394
  streamingUI.disposeActiveCompactionBlock();
6655
7395
  streamingUI.resetLiveText();
6656
7396
  streamingUI.resetToolUi();
@@ -7964,7 +8704,8 @@ var LifecycleController = class LifecycleController {
7964
8704
  hubProbe = createHubProbe();
7965
8705
  sidebarGitCacheWorkDir = "";
7966
8706
  /** Memo for the session-stats aggregation: recomputed only when the
7967
- * transcript length, compaction count or usage object changes. */
8707
+ * transcript's ingest counter/length, compaction count or usage object
8708
+ * changes. */
7968
8709
  sidebarStatsMemo;
7969
8710
  static MEMORY_IDLE_MS = 900 * 1e3;
7970
8711
  static MEMORY_COUNTDOWN_MS = 15 * 1e3;
@@ -8231,16 +8972,20 @@ var LifecycleController = class LifecycleController {
8231
8972
  }
8232
8973
  readSidebarSessionStats(appState) {
8233
8974
  const entries = this.host.state.transcriptEntries;
8975
+ const ingestCount = this.host.transcriptController.getIngestCount();
8234
8976
  const usage = appState.sessionUsage;
8235
8977
  const subagentUsage = appState.subagentUsage;
8236
8978
  const memo = this.sidebarStatsMemo;
8237
- 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;
8979
+ 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;
8238
8980
  let turns = 0;
8239
8981
  let toolCalls = 0;
8240
8982
  for (const entry of entries) {
8241
8983
  if (entry.kind === "user") turns += 1;
8242
8984
  if (entry.kind === "tool_call" && entry.compactionData === void 0) toolCalls += 1;
8243
8985
  }
8986
+ const foldedAway = this.host.transcriptController.getFoldedEntryCounts();
8987
+ turns += foldedAway.turns;
8988
+ toolCalls += foldedAway.toolCalls;
8244
8989
  let subagentOutput = 0;
8245
8990
  for (const sub of Object.values(subagentUsage)) subagentOutput += sub.output;
8246
8991
  const stats = {
@@ -8254,6 +8999,7 @@ var LifecycleController = class LifecycleController {
8254
8999
  tokensOutput: usage.output + subagentOutput
8255
9000
  };
8256
9001
  this.sidebarStatsMemo = {
9002
+ ingestCount,
8257
9003
  length: entries.length,
8258
9004
  compactions: appState.autoCompactionCount,
8259
9005
  usage,
@@ -11159,7 +11905,7 @@ var SessionManager$1 = class {
11159
11905
  this.host = host;
11160
11906
  }
11161
11907
  async init(options) {
11162
- const { startup, workDir } = options;
11908
+ const { startup, workDir, sessionsPrefetch } = options;
11163
11909
  let session;
11164
11910
  let shouldReplayHistory = false;
11165
11911
  const isResumeStartup = startup.sessionFlag !== void 0 || startup.continueLast;
@@ -11175,12 +11921,9 @@ var SessionManager$1 = class {
11175
11921
  throw new Error("picker");
11176
11922
  }
11177
11923
  if (startup.sessionFlag !== void 0) {
11178
- const target = (await this.host.harness.listSessions({
11179
- sessionId: startup.sessionFlag,
11180
- workDir
11181
- }))[0];
11924
+ const target = await this.findSessionById(startup.sessionFlag, workDir);
11182
11925
  if (target === void 0) throw new Error(t("session.not_found", { sessionId: startup.sessionFlag }));
11183
- if (target.workDir !== workDir) throw new Error(t("session.wrong_dir", {
11926
+ if (normalizeWorkDir(target.workDir) !== normalizeWorkDir(workDir)) throw new Error(t("session.wrong_dir", {
11184
11927
  sessionId: startup.sessionFlag,
11185
11928
  workDir: target.workDir
11186
11929
  }));
@@ -11203,7 +11946,7 @@ var SessionManager$1 = class {
11203
11946
  await this.syncRuntimeState(session);
11204
11947
  if (startup.wolfpack && !isResumeStartup) await session.setWolfpackMode(true);
11205
11948
  this.host.state.startupState = "ready";
11206
- await this.pruneEmptySessions(workDir, session.id);
11949
+ await this.pruneEmptySessions(workDir, session.id, sessionsPrefetch);
11207
11950
  this.host.sessionEventHandler.startSubscription();
11208
11951
  return {
11209
11952
  session,
@@ -11216,21 +11959,46 @@ var SessionManager$1 = class {
11216
11959
  * one-off empty session shells. The session about to be used is skipped, as
11217
11960
  * are archived sessions, any session that produced a prompt or a title, and
11218
11961
  * any session touched within the grace window (protects a fresh empty
11219
- * session being used from another terminal). Best-effort: cleanup failures
11220
- * never block startup.
11962
+ * session being used from another terminal). The prefetched listing, when
11963
+ * present, only enumerates candidates: the snapshot can lag the splash
11964
+ * duration, so every candidate is re-checked against a live id lookup and is
11965
+ * deleted only if it still looks prunable right now (a session that woke up
11966
+ * meanwhile — prompt, rename, new activity — stays). Best-effort: cleanup
11967
+ * failures never block startup.
11221
11968
  */
11222
- async pruneEmptySessions(workDir, currentSessionId) {
11969
+ async pruneEmptySessions(workDir, currentSessionId, prefetch) {
11223
11970
  try {
11224
11971
  const now = Date.now();
11225
- const summaries = await this.host.harness.listSessions({ workDir });
11226
- for (const summary of summaries) {
11972
+ const normalizedWorkDir = normalizeWorkDir(workDir);
11973
+ const candidates = prefetch === void 0 ? await this.host.harness.listSessions({ workDir }) : await prefetch;
11974
+ for (const summary of candidates) {
11227
11975
  if (summary.id === currentSessionId) continue;
11228
- if (isPrunableEmptySession(summary, now)) await this.host.harness.deleteSession(summary.id).catch(() => {});
11976
+ if (!isPrunableEmptySession(summary, now)) continue;
11977
+ const latest = (await this.host.harness.listSessions({
11978
+ sessionId: summary.id,
11979
+ workDir
11980
+ }))[0];
11981
+ if (latest === void 0) continue;
11982
+ if (normalizeWorkDir(latest.workDir) !== normalizedWorkDir) continue;
11983
+ if (!isPrunableEmptySession(latest, Date.now())) continue;
11984
+ await this.host.harness.deleteSession(summary.id).catch(() => {});
11229
11985
  }
11230
11986
  } catch (error) {
11231
11987
  this.host.showStatus(`Session cleanup skipped: ${String(error)}`);
11232
11988
  }
11233
11989
  }
11990
+ /**
11991
+ * Resolve the `--session <id>` target from a live listing (resume decisions
11992
+ * never read the prefetched snapshot). The id-filtered form is the only call
11993
+ * that can tell "no such session" apart from "session lives in another
11994
+ * workDir".
11995
+ */
11996
+ async findSessionById(sessionId, workDir) {
11997
+ return (await this.host.harness.listSessions({
11998
+ sessionId,
11999
+ workDir
12000
+ }))[0];
12001
+ }
11234
12002
  async setSession(session) {
11235
12003
  await this.unloadCurrentSession("switching session")?.close({ extractMemories: false });
11236
12004
  this.host.session = session;
@@ -11247,6 +12015,7 @@ var SessionManager$1 = class {
11247
12015
  planMode: status.planMode ? status.planStrategy === "fusion" ? "fusionplan" : "plan" : "off",
11248
12016
  wolfpackMode: status.wolfpackMode,
11249
12017
  rlmEnabled: status.rlmEnabled,
12018
+ rlmMaxDepth: status.rlmMaxDepth ?? null,
11250
12019
  contextTokens: status.contextTokens,
11251
12020
  maxContextTokens: status.maxContextTokens,
11252
12021
  contextUsage: status.contextUsage,
@@ -13463,7 +14232,7 @@ var DialogManager = class {
13463
14232
  }));
13464
14233
  }
13465
14234
  showMemoryPicker(preloadedMemos, preloadedTotal) {
13466
- const store = new MemoryMemoStore(getDataDir());
14235
+ const store = sharedMemoStore(getDataDir());
13467
14236
  const hasData = preloadedMemos !== void 0;
13468
14237
  const memos = preloadedMemos ?? [];
13469
14238
  const total = preloadedTotal ?? 0;
@@ -13628,6 +14397,7 @@ function createInitialAppState(input) {
13628
14397
  ccConnectActive: false,
13629
14398
  wolfpackMode: input.cliOptions.wolfpack === true,
13630
14399
  rlmEnabled: false,
14400
+ rlmMaxDepth: null,
13631
14401
  reconnectAttempt: 0,
13632
14402
  recentSessions: [],
13633
14403
  subagentUsage: {}
@@ -13653,6 +14423,7 @@ var ScreamTUI = class {
13653
14423
  /** Interval handle for the periodic provider-balance refresh. */
13654
14424
  balancePollTimer;
13655
14425
  updatePrefetched;
14426
+ sessionsPrefetch;
13656
14427
  sessionManager;
13657
14428
  dialogManager;
13658
14429
  streamingUI;
@@ -13692,6 +14463,7 @@ var ScreamTUI = class {
13692
14463
  this.options = tuiOptions;
13693
14464
  this.startupNotice = startupInput.startupNotice;
13694
14465
  this.updatePrefetched = startupInput.updatePrefetched === true;
14466
+ this.sessionsPrefetch = startupInput.sessionsPrefetch;
13695
14467
  this.state = createTUIState(tuiOptions);
13696
14468
  this.harness.setSubagentModelBindings(() => this.state.appState.subagentModels);
13697
14469
  this.reverseRpcDisposers.push(...registerReverseRPCHandlers(this.approvalController, this.questionController, {
@@ -13827,7 +14599,8 @@ var ScreamTUI = class {
13827
14599
  try {
13828
14600
  const { shouldReplay } = await this.sessionManager.init({
13829
14601
  startup: this.options.startup,
13830
- workDir: this.state.appState.workDir
14602
+ workDir: this.state.appState.workDir,
14603
+ sessionsPrefetch: this.sessionsPrefetch
13831
14604
  });
13832
14605
  return shouldReplay;
13833
14606
  } catch (error) {
@@ -13956,7 +14729,7 @@ var ScreamTUI = class {
13956
14729
  return first;
13957
14730
  }
13958
14731
  pushTranscriptEntry(entry) {
13959
- this.state.transcriptEntries.push(entry);
14732
+ this.transcriptController.ingestEntry(entry);
13960
14733
  }
13961
14734
  setExternalEditorRunning(running) {
13962
14735
  this.state.externalEditorRunning = running;
@@ -13973,6 +14746,21 @@ var ScreamTUI = class {
13973
14746
  get backgroundTasks() {
13974
14747
  return this.sessionEventHandler.backgroundTasks;
13975
14748
  }
14749
+ /** Live per-type subagent slots (sidebar state machine) — source of the
14750
+ * /tasks browser's Agents view. */
14751
+ get subagentSlots() {
14752
+ return this.sessionEventHandler.getSubagentSlots();
14753
+ }
14754
+ /** Per-instance subagent provenance (spawn parent / outcome / description),
14755
+ * keyed by agentId — source of the /tasks browser's Agents view. */
14756
+ get subagentInstances() {
14757
+ return this.sessionEventHandler.getSubagentInstances();
14758
+ }
14759
+ /** Archive ring for the Agents view: closed instances' last derived rows
14760
+ * (oldest first), the bounded home of agents the live map has released. */
14761
+ get recentSubagentInstances() {
14762
+ return this.sessionEventHandler.getRecentSubagentInstances();
14763
+ }
13976
14764
  getCurrentSessionId() {
13977
14765
  return this.state.appState.sessionId;
13978
14766
  }
@@ -14142,7 +14930,7 @@ var ScreamTUI = class {
14142
14930
  const card = new OnboardingCardComponent({
14143
14931
  colors: this.state.theme.colors,
14144
14932
  onConfigure: () => {
14145
- import("./dispatch-Ccdic2EJ.mjs").then(({ handleConnectCommand }) => handleConnectCommand(this, "")).finally(() => {
14933
+ import("./dispatch-DoaBG--u.mjs").then(({ handleConnectCommand }) => handleConnectCommand(this, "")).finally(() => {
14146
14934
  this.restoreEditor();
14147
14935
  onDone?.();
14148
14936
  });
@@ -14514,7 +15302,7 @@ function supportsAnsi() {
14514
15302
  ansiSupported = false;
14515
15303
  return false;
14516
15304
  }
14517
- async function runLoadingAnimation(theme = "dark") {
15305
+ async function runLoadingAnimation(theme, tuiConfig) {
14518
15306
  if (!supportsAnsi()) {
14519
15307
  const { cols } = getTerminalSize();
14520
15308
  if (cols >= FULL_LOGO_MIN_COLS) for (const line of LOGO) stdout.write(`${fg(...LOGO_RGB)}${line}${RESET}\n`);
@@ -14522,10 +15310,7 @@ async function runLoadingAnimation(theme = "dark") {
14522
15310
  stdout.write(`${BOLD}${fg(...THEME_PRIMARY[theme])}${t("loading.waking")}${RESET}\n`);
14523
15311
  return;
14524
15312
  }
14525
- let autoStart = false;
14526
- try {
14527
- autoStart = (await loadTuiConfig()).autoStart;
14528
- } catch {}
15313
+ let autoStart = tuiConfig.autoStart;
14529
15314
  const autoStartAtBoot = autoStart;
14530
15315
  let toggled = false;
14531
15316
  return new Promise((resolve) => {
@@ -14679,10 +15464,12 @@ async function runShell(opts, version) {
14679
15464
  });
14680
15465
  await harness.ensureConfigFile();
14681
15466
  await harness.preflight();
15467
+ const sessionsPrefetch = harness.listSessions({ workDir });
15468
+ sessionsPrefetch.catch(() => {});
14682
15469
  refreshUpdateCache().catch((error) => {
14683
15470
  log.warn("update cache refresh failed", { error });
14684
15471
  });
14685
- await runLoadingAnimation(resolvedTheme);
15472
+ await runLoadingAnimation(resolvedTheme, tuiConfig);
14686
15473
  const tui = new ScreamTUI(harness, {
14687
15474
  cliOptions: opts,
14688
15475
  tuiConfig,
@@ -14690,7 +15477,8 @@ async function runShell(opts, version) {
14690
15477
  workDir,
14691
15478
  startupNotice: configWarning,
14692
15479
  resolvedTheme,
14693
- updatePrefetched: true
15480
+ updatePrefetched: true,
15481
+ sessionsPrefetch
14694
15482
  });
14695
15483
  tui.onExit = async (exitCode = 0) => {
14696
15484
  const sessionId = tui.getCurrentSessionId();
@@ -16149,6 +16937,15 @@ var FileGate = class {
16149
16937
  * consumer of the same SDK (alongside run-shell TUI and run-stream-json),
16150
16938
  * not a separate engine. Zero changes to agent-core or node-sdk.
16151
16939
  */
16940
+ /**
16941
+ * In-memory FIFO caps per web session. The on-disk journal stays complete;
16942
+ * these bounds only stop a long-lived session from growing without limit in
16943
+ * RAM. Dropped rows are tallied so the elision stays observable (see
16944
+ * `getMemoryBounds`) — the same `[…N … elided…]` accounting the transcript
16945
+ * uses, without re-deriving it from a cursor gap.
16946
+ */
16947
+ const MAX_JOURNAL_ENTRIES = 200;
16948
+ const MAX_SESSION_MESSAGES = 200;
16152
16949
  /** Thinking bodies longer than this are delivered as a tail excerpt + on-demand full text. */
16153
16950
  const TRUNCATE_THINKING_LEN = 8 * 1024;
16154
16951
  function truncateThinking(messages) {
@@ -16499,6 +17296,50 @@ const MAIN_TRANSCRIPT_EVENTS = new Set([
16499
17296
  "tool.result"
16500
17297
  ]);
16501
17298
  /**
17299
+ * Identity of a persisted user-message row; used by the fork copy to drop the
17300
+ * memory copy of a row the disk already carries.
17301
+ */
17302
+ function persistedUserMessageKey(entry) {
17303
+ return JSON.stringify([
17304
+ entry.beforeSeq,
17305
+ entry.clientMessageId ?? null,
17306
+ entry.text
17307
+ ]);
17308
+ }
17309
+ /**
17310
+ * Union of the two views of one session's durable journal, for the fork copy.
17311
+ *
17312
+ * `diskEntries` (read back from the source's journal file) is the complete
17313
+ * history and forms the base: the in-memory journal is FIFO-capped
17314
+ * (MAX_JOURNAL_ENTRIES), so a long conversation's memory only holds its tail
17315
+ * while the file keeps every row. `memoryEntries` fills the rows a
17316
+ * fire-and-forget write has not flushed yet; rows the disk already carries are
17317
+ * dropped by seq (journal rows) or identity (user messages), so no line is
17318
+ * doubled.
17319
+ */
17320
+ function mergeForkJournalEntries(diskEntries, memoryEntries) {
17321
+ const journalSeqs = /* @__PURE__ */ new Set();
17322
+ const userMessageKeys = /* @__PURE__ */ new Set();
17323
+ const journalEntries = [];
17324
+ const userMessages = [];
17325
+ const push = (entry) => {
17326
+ if (entry.type === "user_message") {
17327
+ const key = persistedUserMessageKey(entry);
17328
+ if (userMessageKeys.has(key)) return;
17329
+ userMessageKeys.add(key);
17330
+ userMessages.push(entry);
17331
+ return;
17332
+ }
17333
+ if (journalSeqs.has(entry.seq)) return;
17334
+ journalSeqs.add(entry.seq);
17335
+ journalEntries.push(entry);
17336
+ };
17337
+ for (const entry of diskEntries) push(entry);
17338
+ for (const entry of memoryEntries) push(entry);
17339
+ journalEntries.sort((a, b) => a.seq - b.seq);
17340
+ return [...journalEntries, ...userMessages];
17341
+ }
17342
+ /**
16502
17343
  * Whether a journal entry already carries a message (if so it needs no seeding
16503
17344
  * from core history).
16504
17345
  */
@@ -16880,6 +17721,10 @@ var WebSession = class {
16880
17721
  connections = /* @__PURE__ */ new Map();
16881
17722
  journal = [];
16882
17723
  userMessages = [];
17724
+ /** Aggregate count of journal entries dropped by the FIFO cap. */
17725
+ droppedJournalCount = 0;
17726
+ /** Aggregate count of message rows dropped by the FIFO cap. */
17727
+ droppedMessageCount = 0;
16883
17728
  nextSeq = 1;
16884
17729
  epoch = 1;
16885
17730
  cachedStatus = null;
@@ -16971,7 +17816,11 @@ var WebSession = class {
16971
17816
  getTitle() {
16972
17817
  return this.title;
16973
17818
  }
16974
- /** Return in-memory durable journal entries for fork copy (avoids file persistence race). */
17819
+ /**
17820
+ * Return the in-memory durable journal entries (capped tail). The fork copy
17821
+ * takes these only for rows the source's journal file does not carry yet;
17822
+ * the file itself is the complete history.
17823
+ */
16975
17824
  getDurableJournalEntries() {
16976
17825
  const entries = [];
16977
17826
  for (const e of this.journal) if (!e.volatile) entries.push({
@@ -16986,6 +17835,15 @@ var WebSession = class {
16986
17835
  });
16987
17836
  return entries;
16988
17837
  }
17838
+ /**
17839
+ * Drain in-flight persistence writes (journal rows, user messages, metadata).
17840
+ * A fork reads the source's journal file afterwards and must not race a write
17841
+ * still queued behind the serialization gate — the reader would otherwise
17842
+ * mistake a flushed row for unsaved state.
17843
+ */
17844
+ async flushPendingWrites() {
17845
+ await Promise.allSettled(this.pendingWrites);
17846
+ }
16989
17847
  async persistEntry(entry, throwOnError = false) {
16990
17848
  if (!this.homeDir) return;
16991
17849
  const line = JSON.stringify({
@@ -17004,6 +17862,7 @@ var WebSession = class {
17004
17862
  },
17005
17863
  beforeSeq
17006
17864
  });
17865
+ this.trimMessages();
17007
17866
  if (!this.homeDir) return;
17008
17867
  const line = JSON.stringify({
17009
17868
  type: "user_message",
@@ -17034,9 +17893,11 @@ var WebSession = class {
17034
17893
  },
17035
17894
  beforeSeq: um.beforeSeq
17036
17895
  });
17896
+ this.trimMessages();
17037
17897
  } else {
17038
17898
  const je = entry;
17039
17899
  this.journal.push(je);
17900
+ this.trimJournal();
17040
17901
  this.nextSeq = Math.max(this.nextSeq, je.seq + 1);
17041
17902
  if (this.session === null && je.payload.agentId === "main" && je.payload.type === "goal.updated") this.cachedGoal = cloneSnapshot(je.payload.snapshot);
17042
17903
  else if (this.session === null && je.payload.agentId === "main" && je.payload.type === "todo.updated") this.cachedTodos = cloneSnapshot(je.payload.todos);
@@ -17103,6 +17964,7 @@ var WebSession = class {
17103
17964
  ...message,
17104
17965
  seq
17105
17966
  });
17967
+ this.trimMessages();
17106
17968
  seq += 1;
17107
17969
  }
17108
17970
  if (seq - 1 >= this.nextSeq) this.nextSeq = seq;
@@ -17126,6 +17988,44 @@ var WebSession = class {
17126
17988
  permission: this.permission
17127
17989
  };
17128
17990
  }
17991
+ /** FIFO-cap `journal` to `MAX_JOURNAL_ENTRIES`, tallying elided rows. */
17992
+ trimJournal() {
17993
+ while (this.journal.length > MAX_JOURNAL_ENTRIES) {
17994
+ this.journal.shift();
17995
+ this.droppedJournalCount += 1;
17996
+ }
17997
+ }
17998
+ /**
17999
+ * FIFO-cap message stores to `MAX_SESSION_MESSAGES`, tallying elided rows.
18000
+ *
18001
+ * Drop order follows the rendered order (`buildMessages` puts the seeded
18002
+ * core-history rows at the head, live user rows after them), so the eviction
18003
+ * is genuinely oldest-first and the visible tail of the conversation is what
18004
+ * survives.
18005
+ */
18006
+ trimMessages() {
18007
+ while (this.userMessages.length + this.seededMessages.length > MAX_SESSION_MESSAGES) {
18008
+ if (this.seededMessages.length > 0) this.seededMessages.shift();
18009
+ else if (this.userMessages.length > 0) this.userMessages.shift();
18010
+ else break;
18011
+ this.droppedMessageCount += 1;
18012
+ }
18013
+ }
18014
+ /**
18015
+ * Observable surface of the in-memory FIFO caps: current occupancy, the
18016
+ * limits, and the aggregate elided-row tallies (`[…N … elided…]` accounting,
18017
+ * shared with the transcript).
18018
+ */
18019
+ getMemoryBounds() {
18020
+ return {
18021
+ journal: this.journal.length,
18022
+ journalLimit: MAX_JOURNAL_ENTRIES,
18023
+ journalElided: this.droppedJournalCount,
18024
+ messages: this.userMessages.length + this.seededMessages.length,
18025
+ messageLimit: MAX_SESSION_MESSAGES,
18026
+ messagesElided: this.droppedMessageCount
18027
+ };
18028
+ }
17129
18029
  appendEvent(event) {
17130
18030
  const payload = event.type === "goal.updated" || event.type === "todo.updated" ? cloneSnapshot(event) : event;
17131
18031
  const entry = {
@@ -17135,6 +18035,7 @@ var WebSession = class {
17135
18035
  payload
17136
18036
  };
17137
18037
  this.journal.push(entry);
18038
+ this.trimJournal();
17138
18039
  if (!entry.volatile) this.trackPending(this.persistEntry(entry));
17139
18040
  return entry;
17140
18041
  }
@@ -17151,6 +18052,7 @@ var WebSession = class {
17151
18052
  payload
17152
18053
  };
17153
18054
  this.journal.push(entry);
18055
+ this.trimJournal();
17154
18056
  const persisted = this.persistEntry(entry, throwOnPersistenceError);
17155
18057
  this.trackPending(persisted);
17156
18058
  return {
@@ -18458,8 +19360,9 @@ var SessionManager = class {
18458
19360
  }
18459
19361
  /**
18460
19362
  * Fork an active session: harness.forkSession copies the agent state into a
18461
- * new session, and we additionally copy the source's durable journal so the
18462
- * forked web session displays the same prior conversation.
19363
+ * new session, and we additionally copy the source's durable journal from its
19364
+ * journal file — the complete history, not the FIFO-capped in-memory tail —
19365
+ * so the forked web session displays the same prior conversation.
18463
19366
  */
18464
19367
  async forkSession(sourceId) {
18465
19368
  const source = this.sessions.get(sourceId);
@@ -18479,13 +19382,18 @@ var SessionManager = class {
18479
19382
  title,
18480
19383
  onFork: (id) => this.forkSession(id)
18481
19384
  });
18482
- const entries = source.getDurableJournalEntries();
18483
- if (entries.length > 0) {
19385
+ await source.flushPendingWrites();
19386
+ const { entries: diskEntries, corrupt } = await loadJournal(this.homeDir, sourceId);
19387
+ const entries = mergeForkJournalEntries(diskEntries, source.getDurableJournalEntries());
19388
+ webSession.loadFromPersisted(entries);
19389
+ const journalHasContent = webSession.hasAnyAssistantContent();
19390
+ const journalDamaged = corrupt > 0 && !journalHasContent;
19391
+ if (journalDamaged) webSession.markDamaged(`journal has ${corrupt} corrupt lines and no parsed message carries a body`);
19392
+ else if (entries.length > 0) {
18484
19393
  const lines = entries.map((e) => JSON.stringify(e)).join("\n") + "\n";
18485
19394
  await writeFile(getJournalPath(this.homeDir, newId), lines);
18486
19395
  }
18487
- webSession.loadFromPersisted(entries);
18488
- if (!entries.some(isMessageBearingEntry)) try {
19396
+ if (!entries.some(isMessageBearingEntry) || journalDamaged) try {
18489
19397
  const context = await session.getContext();
18490
19398
  webSession.seedHistory(contextHistoryToChatMessages(context.history));
18491
19399
  } catch (error) {
@@ -18963,6 +19871,19 @@ function startWebIdleExit(opts) {
18963
19871
  }
18964
19872
  };
18965
19873
  }
19874
+ /**
19875
+ * Validates the optional `maxDepth` body field of the /rlm route with the same
19876
+ * grammar as /rlm-max-depth (a non-negative integer; 0 = unlimited) via the
19877
+ * shared agent-core helper. Absent / null leaves the cap untouched; any other
19878
+ * invalid value is a 400 so a client cannot silently send a cap that never
19879
+ * lands.
19880
+ */
19881
+ function optionalRlmMaxDepth(raw) {
19882
+ if (raw === void 0 || raw === null) return void 0;
19883
+ const parsed = parseRlmMaxDepthArg(typeof raw === "string" ? raw : String(raw));
19884
+ if (!parsed.ok) throw new HttpError(400, `Invalid rlm max depth: ${parsed.reason}`);
19885
+ return parsed.value;
19886
+ }
18966
19887
  async function handleSessionControlRoutes(req, res, url, method, manager) {
18967
19888
  const statusMatch = new RegExp(`^${API_PREFIX}/sessions/([^/]+)/status$`).exec(url);
18968
19889
  if (statusMatch && method === "GET") {
@@ -19050,7 +19971,7 @@ async function handleSessionControlRoutes(req, res, url, method, manager) {
19050
19971
  try {
19051
19972
  const body = await readJsonBody(req);
19052
19973
  const enabled = optionalBoolean(body, "enabled", true);
19053
- const maxDepth = typeof body["maxDepth"] === "number" ? Number(body["maxDepth"]) : void 0;
19974
+ const maxDepth = optionalRlmMaxDepth(body["maxDepth"]);
19054
19975
  await manager.setRlm(decodeURIComponent(rlmMatch[1]), enabled, maxDepth);
19055
19976
  sendJson(res, 200, { ok: true });
19056
19977
  } catch (error) {