fix(planning): the Work and Code tabs read a plan's work back after a restart
Summary
Dean: "the Work tab can be empty when I restart Luke… Similar with the Code tab." Confirmed. Opening a plan after a restart, or switching to another plan and back, left both tabs empty even though the service had everything on record.
Root cause, per tab
- Work. The host (
packages/host/src/compose-planning.ts) only ever held a plan's Work turns from liveplan.workframes during a call. It never read them back from anywhere.PLANNING_OPEN(every launch, every plan switch) clears them throughwithoutActivity, and nothing re-reads them, although the relay journals every planning turn in the plan's conversation. So: pushed live only, never re-read on launch or plan open. - Code. It was ephemeral by design:
planning-view.tssaid "It stands for the call alone", the host'scallEndedcleared it, and the renderer drew code only whilelive. So the tab went back to its empty line after every call, not just after a restart. Butshow_code's lines are the call's journaled answer, so keeping them needs no new server field. Product call, made with the orchestrator/Dean: persist it. Every acceptedshow_codeanswer on the plan is read back, and the Code tab shows the newest. A stacked follow-up turns the tab into a feed of every snippet. - Board and Transcript don't fail this way. Both are read on every
PLANNING_OPENandPLANNING_REFRESH(readBoard,readTranscript), so a restart reads them back. - The persisted chosen tab is not at fault:
use-side-panel.tsreads it once, and the tab redraws from whatever the view brings.
What changed
- Server:
GET /api/plans/{id}/work(apps/web/server/hosted/plan-work-store.ts), scoped to the owning account like the other plan routes (another account's plan reads as 404).- Returns the plan conversation's turns twenty a page, newest first, each projected by the same
planWorkOfthe live call uses (worker sessions included).?before=<turn id>reads the page before that turn, and the answer namesearlierwhile an older page stands. No cap on how far back (Dean's decision). - The newest page also carries every accepted
show_codeanswer on the plan, oldest first, several in one file included. Each entry hascallId,turnId,shownAt, andcode(path, line range, window lines, repository). - Journals holding a
show_codecall are picked in SQL (parts @> '[{"type":"tool-show_code"}]'), so the read doesn't pull every journal of a long plan.
- Returns the plan conversation's turns twenty a page, newest first, each projected by the same
- Refusal rules on read-back: only
acceptedanswers read back. The checkout's realpath check, missing file, binary, and oversized refusals all journal asrejectedand never come back.shownCodeOfnow applies the.env*secret rule again, live and on read-back, so a journal written under an older rule can't put a secret file on screen. There's a test that fails without it. - Host:
- Reads the newest page with the document on open and refresh.
- Keeps every turn: the 20-turn cap in
showWorkis gone. - A turn a call told stands over the record's copy while that copy still says running. A settled record copy wins.
- Code a call put on screen stands over the record's, since the record may hold code the call never got to show.
- Code now survives
callEnded. - New gateway method
planning.workEarlier, and actplanning.workEarlier, read the page before the oldest turn held.
- Renderer:
- The Work tab shows a "Show earlier work" row above its oldest turn when older turns exist. It reads the next page when it scrolls into view (IntersectionObserver), or on press. If a read loads nothing, it says "Could not read earlier work. Try again" and waits for a press instead of retrying on its own.
- The Code tab draws the plan's code with no call standing.
- Code counts as an arrival (the panel opening by itself) only during a call, so opening a plan that already has code doesn't pop the panel.
- Docs:
PRIVACY.md(product change, called out here): the Work tab's turns and the code shown are now read back from the service when a plan opens, held in memory only. The "cleared when the call ends" line is replaced. Also updated:docs/PLANNING.md,apps/web/server/README.md, and theplanning-view.tscomments.
Reproduction
- Host, through the real composer (
compose-planning.test.ts): a fresh composer stands for a restart. Open a plan whose service holds turns and code: onmainthe view'sworkandcodestay empty. Switching to another plan and back has the same result. - Server, on PGlite (
apps/web/tests/plan-work.test.ts): real eve events replayed through the store relay into a plan's conversation, then read back throughreadPlanWork. Covers turn order, every code shown (one refused), 22 turns paged 20 + 2, the secret file not read back, worker sessions, and another account reading nothing. - Renderer, through public hooks (
use-plans-tab.test.ts,desktop-plans.test.ts): with no call standing, code is drawn and no panel opens. The earlier-work row reads on scroll-in, stops after a read that loaded nothing, and reads again on press.
Before / after
Headless Chrome render of the real DesktopPlans page and side panel (stub controls, built styles.css, dark). "Before" is what a plan opened after a restart drew: nothing on record reached the tabs. "After" is the same plan once the /work read lands. The Work tab draws its turns, with the row above the oldest reading the page before it as it scrolls into view. The Code tab draws the last code shown, with no call standing.
| Before (restart) | After | |
|---|---|---|
| Work | ![]() | ![]() |
| Code | ![]() | ![]() |
Evidence
- Platform-independent checks:
./scripts/check.shexits 0 locally. CI runs it on Linux. - macOS Electron verification (
./scripts/verify.sh): not run. This workspace is a Linux VM. CI could not verify the Mac UI, and the Mac pass is LUKE-159.
Visual evidence
- Fixture screenshots inspected:
not attached(no Mac). The headless Chrome renders above were inspected.
Physical-device evidence
- Screenshot or screen recording:
not attached(no Mac in this workspace)
Tests
Each fails on main (or on this branch with the fix reverted) and passes here:
packages/host/src/compose-planning.test.ts:- a plan opened after a restart or a switch draws its work and the code last shown
- the service's read lands under what the call told
- the Work tab's ask reads older pages until none stand, and a refresh keeps them
- the call's end keeps its code (this replaces the old "clears" test)
- the call keeps every turn past a page (this replaces the old cap test)
apps/web/tests/plan-work.test.ts(PGlite): turns oldest first plus every code shown, owner-scoped; paging; never-planned plan; secret file not read back; worker sessions.apps/web/tests/plans-app.test.ts: the route is owner-scoped, refuses a badbefore, and is GET only.packages/hosted/src/plan-client.test.ts:readWorkreads a page at a time.apps/desktop/src/main/ipc/planning-acts.test.ts:planning.workEarlierreaches the host from a panel only.apps/desktop/src/renderer/planning/use-plans-tab.test.ts: code drawn with no call, no panel pop; earlier work read through the act.apps/desktop/src/renderer/desktop/desktop-plans.test.ts: the earlier-work row reads on scroll and stops after a read that loaded nothing.
Test count: +11 new tests, 2 rewritten for the new behaviour (code kept past a call's end; no turn cap).
Invariants
-
./scripts/check.shgreen../scripts/verify.shwas not run (Linux VM, no Mac): CI cannot verify the Mac UI, see LUKE-159. - JSON Schema goldens unchanged.
- Every behavior change has, in this PR, the test that fails without it; no change-detector test added.
- Gateway envelope goldens unchanged.
- No new
astype assertion outside the allowlisted files (the one test cast carriesSAFETY:). - No
Effect.runPromise/runSync/runForkoutside the runtime edges. - No new
setTimeout/setInterval/new Promise/AbortController/fs.watch. - No Effect, Stream, or Layer dropped as a statement.
- Test count delta listed above.
- No hand-rolled file replaced.
- Docs naming the changed parts edited (
docs/PLANNING.md,apps/web/server/README.md).AGENTS.md/CLAUDE.mduntouched. -
PRIVACY.mdchanged: a product decision, called out above (Work turns and shown code are read back from the service when a plan opens, memory only). - Package deps unchanged.
function-externals.jsongainsnode:dns/promisesfordefault.js, a Node builtin the plans route now reaches through the planning tool registry. - Barrel/door rule holds.
Notes
- Follow-up (stacked, another workspace): the Code tab becomes a feed of every snippet, using the
codelist this read already returns.
🤖 Generated with Claude Code



