Lazy content load (instant cold-mount + truly lazy N-actions widget)
Opening a session paints visible content within a few hundred milliseconds however long the conversation is, and the collapsible pill that hides a turn's tool calls now fetches its contents only when you open it. Together they shrink what a cold mount has to build by orders of magnitude on tool-heavy threads.
What it is
What it does
When you open a Claude Code session in Omniscio — switching to it in the sidebar, clicking it from search, or returning to it after a window hide — the chat panel paints visible content within a few hundred milliseconds, no matter how many messages or tool calls the session contains. A 4,000-message tool-heavy thread feels the same as a 20-message one.
Inside every agent turn, the per-turn "Actions" widget (the collapsible pill that hides the agent's tool calls and tool results) is now truly lazy: at cold mount the widget's content is not in memory at all. Clicking the pill fires a one-off fetch for just that turn's tool activity, parses it, and renders the expanded view. Closing the pill releases the content. If you never click, that content never gets paid for.
Together these two changes shrink the cold-mount payload by roughly 10×–100× on tool-heavy sessions (the exact ratio is the size of tool-output bytes divided by real-prose bytes per turn), without changing what the expanded activity view looks like once you do click.
Where to find it
Settings → Performance, as the lazy-content-load toggle. Its effect is felt everywhere a session opens — switching to it in the sidebar, clicking it from search, or returning to it after the window was hidden.
How it behaves
What the user sees
- Switching to a long session is visibly instant. There's no multi-second freeze, no progress shimmer, no "loading messages" pill at the top of the panel — the content just appears.
- The "Actions" pill (one per agent turn that called any tools) looks identical to before: same icon, same label
Actions, same chevron. - The first click on a pill is the same fast expand it always was — the round-trip to fetch that turn's content is short enough to feel like a normal toggle. Subsequent re-expand of the same pill fires the fetch again (no renderer cache in v1, by design — re-expand is rare).
- Search across long sessions still works. If a search hit lands on a tool-output line inside a not-yet-expanded turn, Omniscio auto-expands that turn before scrolling, so the highlighted hit is visible when you arrive.
- Nothing changes on export, share-publish, or audit-export. Those paths still get the full message content.
What changed under the hood
Before this change, Omniscio's cold-mount SELECT projected every column of conversation_messages, including the content TEXT column. On a tool-heavy turn that column is hundreds of KB to several MB — tool-call markers, tool-result blocks, embedded JSON. Every cold mount paid the cost of marshalling, JSON-parsing, and content-parsing that whole blob for every visible row, just to render a per-turn collapsed pill the user usually never expanded.
After this change there are two new columns on conversation_messages and a new IPC channel that swaps in a "lite" cold-mount path:
summary_prose TEXT— the marker-stripped, prose-only body for an agent turn. NULL means "not yet derived" (a fallback signal, not an error).has_tool_activity INTEGER NOT NULL DEFAULT 0— 1 iff the originalcontentcontained any tool-call lines, so the renderer knows whether to draw the Actions pill at all.
Cold-mount now calls getSessionHistoryLite() (a separate function from the original getSessionHistory() — a deliberate function-pair, not a boolean flag, so the TypeScript compiler enforces correct consumption per call site). The lite SELECT projects everything except content, and ships rows where content has been replaced by summary_prose and a small metadata.liteContent marker that tells the renderer where to fetch the full content if it needs to.
When the user clicks an Actions pill — or when a search hit lands inside a not-yet-expanded turn — the renderer fires a new IPC channel SESSION_GET_TURN_CONTENT { messageId } that returns just that one row's full content. The renderer parses it with the existing parseContentSegments and renders the expanded view inline.
The full-content path is still alive and unchanged for export, share-publish, audit-export, and FTS. They keep calling getSessionHistory() directly.
Subagent-result rows defer their output too (2026-05-26)
Agent turns are not the only heavy rows. When a session spawns a subagent (the Task tool), the subagent's full result comes back as its own row — a system row tagged metadata.kind = "subagent-result", holding up to 32 KB of the subagent's output (the cap is SUBAGENT_RESULT_PREVIEW_MAX_CHARS). In the chat these render as a collapsed "subagent result" card with a Show output toggle (auto-expanded only when the subagent errored). A session that fanned out a lot of subagent work can carry dozens of these rows, and because the original lite strip only touched agent rows, every one of those 32 KB blobs still shipped on cold mount. On heavy sessions that was 90–98% of the whole cold-mount payload — the concrete cause of the "switching to the next session takes 4+ seconds" lag this whole architecture exists to kill.
So the lite cold-mount now strips subagent-result content the same way it strips agent content: the cold-mount SELECT replaces the body of any subagent-result system row with an empty string, and the row ships with a metadata.subagentContentDeferred = true marker instead of its output. All the small metadata the card needs to render its collapsed header — the subagent's description, how long it ran, whether it errored, and the original byte size (contentSize) — is already in metadata and rides along untouched, so the collapsed card looks exactly the same as before.
When you click Show output (or when an errored subagent card auto-expands at mount), the card fires the very same SESSION_GET_TURN_CONTENT { messageId } IPC the Actions pill uses — it's source-agnostic, fetching one row's full content by ID regardless of whether that row is an agent turn or a system subagent-result — shows a brief "Loading output…" line, then renders the real output inline. The truncated-size hint under the output ("13 of 50,000 chars (truncated)") is computed from the original contentSize in metadata, so it reads correctly even before the body has been fetched. If the fetch fails, the card shows "(no output)" rather than an error. Subagent-result rows that are not deferred (the kill-switch / full-content path) keep rendering their output inline with no IPC, exactly as before.
This strip is gated by the same AMC_DISABLE_LITE_COLD_MOUNT=1 kill switch — with lite off, subagent-result rows ship full content and render inline with no fetch. Export, share, audit, and FTS are unaffected (they never went through the lite path).
How content gets into the two new columns
There are three paths (one now dormant), in order of importance:
Finalize-time derive on new rows. When an agent turn completes (NDJSON stream finalize, or
/compactpost-summary turn write), the main process runs a small helperextractProse(content)and binds both columns into the same INSERT. Fail-open: if the helper throws, the INSERT lands with NULLsummary_proseand 0has_tool_activity, and the renderer falls back to fetching full content for that row. The streaming-checkpoint path (partial content flushed mid-turn) deliberately does NOT derive — its content isn't final, so any derived value would be stale.Producer-side rescue (the trailing-bookkeeping-tool case).
extractProseis trailing-only — it resets its prose buffer on every top-level tool marker (▸). So when an agent writes its real final answer and then a trailing bookkeeping tool call (a▸ TaskUpdate, a final▸ Bashcommit to save its work), the flattenedcontentends on that marker, the walker's buffer resets to empty, and the derive returns empty prose for a turn that genuinely had an answer. Left alone, the lite classifier reads "empty prose + tool activity" as a pure-tool turn and folds the real answer into a collapsed "N actions" activity pill — the answer effectively vanishes. No content-only walker can repair this, because once flattened, "answer before bookkeeping tool" is byte-identical to "intermediate narration before bookkeeping tool." The producer, however, still holds the structure the flattening destroyed, so at finalize it passes the turn's last real answer block —lastRealAssistantText, the last text block that passedisRealMessageProsewhile the turn streamed — intoaddMessageas arescueProseargument.addMessagepromotes it intosummary_proseonly when all three gates pass: the derive came back empty, the row actually carries non-thinking activity (has_non_thinking_activity === 1), and the rescue text itself passesisRealMessageProse. TheextractProseparser stays pure and untouched — the fix lives entirely in the producer — and the rescue is forward-only: it fixes new rows as they are written, with no migration to backfill rows buried before the fix shipped. On a derive throw the gate fails closed (prose stays NULL, activity flag 0), so the row fails open to full-content fetch exactly as before. See the trailing-bookkeeping-tool-erases-prose postmortem in the repo.This is now the only live derive path.
Idle backfill of existing rows — DORMANT (unwired 2026-05-24). A background service (
summary-prose-backfill.ts+ its scheduler) can walk every pre-existing agent row in batches and derive both columns, but nothing in production triggers it anymore. It used to drain on the main window'sblurevent; that trigger was removed because the backfill had no guard against in-flight (metadata.partial = 1) rows — it summarized truncated mid-stream checkpoints and stored'', which the lite path then painted as an empty shell over still-streaming content (the collapse-to-"Generating…" bug: messages flash in on click, then vanish; reported 2026-05-24, recurred on every window blur). The module is kept (not deleted) so a future re-enable with a partial-row guard is cheap, but as of now the only way a pre-existing NULL row shows content is the renderer's NULL → full-content fallback. Migration v231 NULLed the rows the backfill had already corrupted. See the partial-row backfill postmortem in the repo.UPDATE-time re-derive. Any path that mutates an existing row's
content(operator edit, compact-summary write, session-restore) re-derives both columns in the same UPDATE.
A NULL summary_prose is the "not yet derived" signal, and what the lite read path does with it depends on whether the turn is settled or still partial:
- Settled row, NULL prose (a never-finalized-but-ended turn, a pre-v218 row, or one the v227/v231/v235 migrations NULLed): the lite read path (
mapMessageLite) runsextractProseover the rawcontentat read time, ships the trailing prose in place ofcontent, and stamps theliteContentmarker — so the row still gets the payload shrink even though nothing is stored insummary_prose. This read-time derive is what stops old archived sessions from shipping multi-MB raw tool output on cold mount (it was added in the load-old-archived-session fix, 2026-05-25). - Partial row (in-flight or interrupted,
metadata.partial === true): both read paths —mapMessageLiteand the renderer'sbridgeLiteMessage— short-circuit and serve the fullcontentverbatim, skipping the read-time derive entirely. A partial row's stored content is a mid-stream checkpoint; running the trailing-only slice over it would hide the agent's live narration (or return empty mid-tool-block), and the renderer's append-on-delta cache would then lose the original prose permanently. The short-circuit mirrors the producer-side guard that already leavessummary_proseNULL on partial rows. See the lite-cold-mount partial-row-strip postmortem in the repo.
Why the function-pair shape, not a flag
The first version of this plan used getSessionHistory({ lite: boolean }). It got rejected at red-team review because a boolean flag forces every caller to handle a union return type — and TypeScript can only enforce that union if every call site checks the flag, which is lint-fragile and easy to drift. A separate getSessionHistoryLite() function with a separate return type (LiteConversationMessage) makes the compiler the enforcer: if you call getSessionHistoryLite() and try to read .content, you get a type error, full stop.
This matters because there are exactly five cache-warmers that must opt into lite mode together (cold-mount panel fetch, Dashboard adjacent-prefetch, SidebarSessionRow hover, ArchiveView hover, mobile-prefetch streaming), and there are several other call sites (export, share, audit, FTS, full-content cold-mount when a search-hit auto-expands) that must NOT. The function-pair enforces the split at compile time; a CI lint test (tests/unit/lint/lite-warmer-coverage.test.ts) pins which side each call site sits on so future refactors can't silently move one across the boundary.
Mobile is fine too
Unlike the /compact-clamp (which is desktop-only because its post-compact branch can return multi-MB payloads that crash mobile JSON.parse), lite mode is desktop-AND-mobile safe. It always shrinks the payload, never grows it. The long-session-mobile-blank bug that the compact-clamp had to carve out for is, with lite mode, addressed at the source — the cold-mount payload is small to start with, so the JSON parse doesn't OOM the tab.
As of 2026-05-24, mobile cold-mount actively uses lite + 200-row bounded (was 2-row + full-content). The per-row shrink (KB-MB → hundreds of bytes) is what makes a 200-row Tailscale-Funnel cold-mount tolerable; the 2-row cap that lite was originally written around is gone. Mobile users now see the full real conversation on cold-mount, same as desktop. The shape lives in pickColdMountFetchShape (mobile branch) and is pinned by mobile-shape in cache-warmer-shape-contract.md. compactClamp is still off on mobile — the post-compact branch is the one path even lite can't tame.
That said, mobile inherits the same per-row content fetch when expanding a pill. On a slow connection that adds a one-time round-trip per first expand. Acceptable cost — the user is already opted into mobile's "explicit click to expand" model, and the alternative (pre-loading content the user usually doesn't click) is what crashed the tab in the first place.
Bootstrap-LAM seed (mobile cold-mount blank-screen kill)
On mobile, even the trimmed lite cold-mount round-trip takes 1–3 s over Tailscale Funnel (longer when the single main thread is saturated under a many-session storm), so tapping a session with no warmed cache used to land on an empty panel with a spinner until the lite fetch resolved. To kill that blank-screen window, the WEB_BOOTSTRAP payload ships a lastAgentMessageContent field for two status groups: inbox-attention sessions (needs_you / error / stalled) carry the final agent message's prose (summary_prose preferred, content fallback, capped at ~50 KB); live working sessions (running / starting) carry the last settled agent message (summary_prose only, gated on summary_prose IS NOT NULL so the in-flight partial row is skipped — a streaming session never seeds a raw half-stream — capped tighter at ~4 KB since many can ship at once). Both exclude asides. (Running/starting seeding shipped 2026-06-16 to fix a "running session takes ~30 s to load on mobile" report — the session was tiny; the wait was the missing seed plus main-thread contention, not payload size.) The renderer builds a synthetic agent MessageBubble from that field and renders it the instant messages.length === 0 && lastAgentMessageContent — before any lite fetch has even started. When the real lite history resolves, the normal virtual list takes over and the seed disappears naturally. No cache poke, no merge logic. The per-session LAM lookup is served by idx_messages_session_source_ts (session_id, source, timestamp); without that covering index the source='agent' … ORDER BY timestamp subquery fell into a per-session TEMP B-TREE sort that ballooned WEB_BOOTSTRAP to 0.7–1.5 s at ~126 active sessions (fixed 2026-06-24, migration 20260624090438 — see the mobile-bootstrap-lam-temp-btree postmortem). Full bootstrap-payload spec + Zod compat shape: mobile-remote-access.md § Bootstrap LAM.
Live delivery — the seed also rides the attention-transition push (2026-07-31)
The WEB_BOOTSTRAP seed above only covers sessions that were already inbox-attention at bootstrap. A session that transitions into an attention status while the app is open — the #1 inbox scenario (an agent finishes / asks a question and pings you) — carried NO seed, because the live SESSION_STATUS_CHANGED push is a status delta and the renderer's patchSession never sets the field. So tapping that inbox card raced a cold lite fetch and felt slow (the "mobile is loading the first message really slowly" report). Fix: the seed now ALSO rides the mobile-bound SESSION_STATUS_CHANGED push for an attention transition. enrichSessionStatusPushForRemote (web-access-session-seed-enrich.ts) attaches lastAgentMessageContent at the shared main-side remote-projection point BOTH web-access transports pass through — the in-main forwarder AND the off-loop worker relay (the worker has NO DB, so the seed MUST be read main-side before postMessage; enriching only the forwarder would miss the worker transport). It is bounded + freeze-safe: a single indexed-row read (getSessionSeedContent — the bootstrap LAM attention subquery factored per-session), attention-status-only, capped at LAM_LIVE_SEED_CAP_CHARS, wrapped in try/catch (a DB hiccup never breaks the push), behind kill switch AMC_DISABLE_LIVE_SEED_PUSH. The optional lastAgentMessageContent field is declared on sessionStatusChangedPayload (else the renderer's parseSessionStatusChangedPayload safeParse strips it); useSessionSync patches it onto the session via updateSessionLastAgentMessageContent before updateSessionStatus, and the existing bootstrapSeedMessage render path paints it. Desktop's Electron push path never reaches the projection → untouched. Worker-gated (2026-08-11): enrichSessionStatusPushForRemote skips the seed read when isDbWorkerActive(). The read is a warm-mmap optimization; with the off-thread DB worker ON, main's conversation_messages mmap is COLD, so this per-push sync read hard-faulted it back off the disk → multi-second STARVED UI-thread freezes (up to ~74 s on a slow-DB-disk box under attention churn). With the worker on it degrades to the on-tap lite fetch; the worker is OFF by default, so only a worker-enabled box loses the instant seed (and it never freezes). See db-worker-offload-contract.md.
What didn't change
- Search index (
conversation_messages_fts) is curated, not full-content:indexableSearchText()(src/main/db/fts-index-text.ts) indexes operator rows verbatim, agent rows as ONLY their trailing final prose (extractProse— mid-turn narration / tool-call / tool-result lines excluded), and partial + system rows not at all. Migration v244 rebuilt the whole index under this per-source rule. So a search hit can no longer land on a tool-output line; it lands on operator text or an agent's final prose. - The visual / interaction design of
ToolActivityBlockonce expanded is identical — same per-step rendering, same icons, same wrapping. - The "Load older messages" pill's machinery at the top of long sessions still works the same way under the hood (extends the window by 200 rows per click on desktop) — but the pill JSX itself is disabled by default for every user as of 2026-05-21 via the
LOAD_OLDER_MESSAGES_PILL_ENABLEDcode-level flag inSessionPanel.tsx, so it does not actually render. See load-older-pill-disabled postmortem. - The
/compact-clamp on cold mount still applies — lite mode is layered on top of it, not a replacement. A clamped lite cold mount on a settled-compact session returns the since-compact rows in lite shape. - Export to markdown, the share-publish flow, the audit-export, and any other caller that legitimately needs the full content keeps calling
getSessionHistory()directly. No changes there.
The narration-leak rescue (2026-05-21, removed 2026-05-24)
Status: REMOVED. The renderer-side rescue (splitLiteProseByHeuristic) and its sibling mergeFinalMessageMetadata synthesis were both deleted on 2026-05-24 once the schema v227 summary_prose re-derive landed and the rev-13 merged-bubble layout reshaped where tool activity surfaces. The history below is kept for context — the policy now is: if the final bubble looks wrong, fix extractProse and ship a v228+ re-derive, not a render-time patch. See .claude/memory/contracts/summary-prose-contract.md § "No renderer-side rescue".
— Original history —
The first day lite mode shipped, agent messages with tool activity started rendering with the model's pre-amble — "Reading the plan…", "Yes, that's right…", brief recaps — visible inline above the final answer. Before lite mode, that text was hidden inside the collapsed Actions pill alongside the tool calls, and only the final answer rendered inline. The regression made every long answer look like a stream-of-consciousness dump.
The cause was mechanical, not a model-behavior change. extractProse(content) strips tool-call (▸ ) and tool-result (← ) markers when it builds summary_prose, so by the time the renderer's parseContentSegments saw the lite body there were no marker boundaries left — the whole turn became one giant prose segment. computeActivityOverview (the splitter that normally decides "this goes in the pill, that renders inline") needed markers to find the boundary. Without them it put everything in trailingSegments, and the leading narration painted inline.
The original fix was a renderer-only paragraph-walker (splitLiteProseByHeuristic) that scanned for the first paragraph passing isRealMessageProse (Heuristic D) and hid everything above it. It worked for the cases it was designed for, but it had two ongoing failure modes: (1) a paragraph that should have been hidden but didn't pass the heuristic would leak through; (2) the rescue silently dropped intermediate paragraphs the user wanted to read (the 2026-05-22 last-paragraph-fallback truncation regression). The fix superseded the rescue: the schema v227 migration now re-derives summary_prose from raw content for every has_tool_activity = 1 row using the same trailing-only contract as extractProse, so the renderer no longer needs to guess. Under rev-13's merged-bubble layout the activity bubble above the merged bubble carries any leaked narration anyway — so even if a paragraph survives, the duplication is far less bad than truncation.
Kill switch fully removed: AMC_DISABLE_NARRATION_RESCUE was a no-op since the schema v227 re-derive; its preload deprecation warning was removed 2026-07-06, so the flag is gone entirely. AMC_DISABLE_LITE_COLD_MOUNT=1 still routes lite back to full.
Scroll-engine timing relationship (2026-05-23, SHIPPED; mobile reliability follow-up 2026-06-18)
Because lite cold-mount paints the message bubbles before the lazy summary_prose placeholders have grown into their final rendered prose, there's a brief window where the scroll engine can measure the panel's scrollHeight against the pre-expansion layout. On a session whose final agent reply overflows the viewport in its final form but appears to fit during that window, the engine can pick the wrong scroll target — and on re-entry to a long session, the saved-position fingerprint can be computed against a different row count than the one used when the position was saved.
The fix work lives entirely in the scroll layer, not in the lite path: a new inner ref on the merged-turn bubble so the engine can target the agent's prose rather than the activity-toggle header, a late-render re-classification that re-measures once the real heights arrive, and a fingerprint that counts main-thread agent messages instead of raw row count. None of this changes how lite mode itself works; the AMC_DISABLE_LITE_COLD_MOUNT=1 kill switch is still the panic button for the underlying lite-mode behaviour.
Shipped 2026-05-23 (commit c86d8b79a4): all three fixes landed on master. They guarantee the scroll engine ENDS at the correct position once content settles.
Mobile reliability follow-up (2026-06-18). The May late-re-classification trigger was a ResizeObserver wired only when the merged-turn prose ref is attached. On a fresh mobile cold-open that ref is frequently null (the target row isn't mounted yet) and a tall QuestionWidget grows outside the prose body — so no watcher was installed, and the view stayed pinned at the bottom until some unrelated event repositioned it. That is the user-reported "open a session on the phone, it lands at the bottom, then snaps to the top after several seconds." The follow-up adds, on mobile, a bounded setTimeout poll of the scroll container's own scrollHeight (the exact quantity the decision compares) so ANY late growth — from any element, with or without the prose ref — reliably and promptly re-measures and snaps to the message top. It runs through the same pure shouldLateReclassify gate (material-growth floor + the user-gesture veto + the post-mount time window) and is one-shot: whichever trigger (poll or observer) sees the growth first fires the re-measure and tears the rest down. Desktop is unchanged (its cold-mount is masked by the warm cache). Full contract: frontend-scroll-contract.md § Cold-mount lite-content timing, rule #2.
For agents
The extractProse helper
extractProse(content) lives at src/main/services/extract-prose.ts — a thin, deterministic wrapper. The trailing-prose walk itself lives ONCE in the shared canonical core deriveAgentProse (src/shared/agent-prose-core.ts); extractProse delegates to it and widens the result with commentCount + toolCount (W10/F020 — the walk was previously duplicated byte-for-byte with the renderer's deriveProseAndActivity, which is now also a one-line delegate). The core depends on the shared marker walkers (@shared/agent-content-markers) and the QW v3 parser (@shared/question-widget-parser-v3) — not config-free. It splits content by newline, tracks whether each line is inside a triple-backtick fence, and classifies non-fence lines as either tool-marker (▸ for calls, ← for results) or prose. extractProse returns { prose: string, hasToolActivity: boolean, commentCount: number, toolCount: number, hasNonThinkingActivity: boolean } (the last three feed the TurnActivityBubble "X comments, Y actions" label and the MergedTurnBubble-vs-MessageBubble discriminator).
The reason it's a dedicated helper rather than reusing the renderer's parseContentSegments is that parseContentSegments depends on the renderer's PreprocessConfig (link normalization, QW parser version, etc.) — config the main process doesn't have and shouldn't need. The dedicated fence-aware extractor is simpler, faster, and parity-tested against the renderer's output via tests/unit/services/extract-prose-parity.test.ts on 12 real-shaped fixtures.
The kill switch
If a perf regression or a correctness bug shows up in lite mode in the wild, the user can flip one environment variable and restart:
AMC_DISABLE_LITE_COLD_MOUNT=1
With the kill switch set, getSessionHistoryLite() aliases back to getSessionHistory() — same lite shape, but with the full content populated on every row. The renderer is unchanged; the Actions pill renders inline from props (no IPC) because the content is already there. This is the "panic button" rollback path so the user never has to wait for a code change to land.
The flag is read once at boot, lives in src/main/services/lite-mode-flag.ts, and any string other than the literal "1" is treated as "lite enabled" (so an absent variable, an empty string, "0", or "true" all keep lite on).
Where it lives in code
| What | Where |
|---|---|
| Schema columns + backfill-state table | src/main/db/incremental-migrations.ts migration v218 |
extractProse(content) helper |
src/main/services/extract-prose.ts |
| Kill switch reader | src/main/services/lite-mode-flag.ts |
| Idle backfill service (DORMANT — unwired 2026-05-24) | src/main/services/summary-prose-backfill.ts + scheduler at summary-prose-backfill-scheduler.ts; unwire locked by tests/unit/lint/summary-prose-backfill-unwired.test.ts |
| Partial-row repair migration | src/main/db/incremental-migrations.ts migration v231 + tests/unit/db/migration-v231-partial-row-resentinel.test.ts |
getSessionHistoryLite() + mapMessageLite() (agent + subagent-result strip) |
src/main/db/queries-messages/history.ts |
SESSION_GET_TURN_CONTENT IPC (source-agnostic — serves agent turns AND subagent-result rows) |
handler in src/main/ipc/session-handlers.ts, channel in src/shared/ipc-channels/session.ts, schema in src/shared/ipc-schemas.ts |
Subagent-result card lazy-fetch + subagentContentDeferred marker consume |
src/renderer/src/components/ui/MessageBubble/MessageBubble.tsx (isSubagentResult block) |
LiteConversationMessage shape |
src/shared/ipc-types.ts |
| Renderer placeholder for lite turns | liteContent prop on AgentMarkdown, threaded from MessageBubble.tsx:extractLiteContent |
| Five-warmer lint pin | tests/unit/lint/lite-warmer-coverage.test.ts |
Per-session LAM seed read (getSessionSeedContent) |
src/main/db/queries-sessions/lifecycle.ts |
Live attention-transition seed enrichment (enrichSessionStatusPushForRemote, both transports) |
src/main/services/web/web-access-session-seed-enrich.ts — wired in web-access-push-forwarder.ts + web-access-host.ts |
lastAgentMessageContent push field + renderer patch (updateSessionLastAgentMessageContent) |
src/shared/push-event-schemas/session-lifecycle.ts · useSessionSync.ts · session-store.ts |
Related
- scroll-position-memory.md — the other layer that affects how a session paints on return.
- The postmortem in the repo:
.claude/memory/postmortems/lazy-content-load-architecture-postmortem.md— the WHY this architecture, the alternatives that were considered and rejected, and the 10-lens red-team that shaped v2. - The narration-leak follow-up postmortem in the repo:
.claude/memory/postmortems/lite-cold-mount-narration-leak-postmortem.md— the regression mechanism and the renderer-only rescue (removed 2026-05-24).
Last verified 2026-10-06