Promote expbkmain to bkmain: plan review, session trees, lineage, worktree reclaim - #69
Merged
Conversation
Adds a native plan review that lives in the right panel beside Plannotator, behind Settings -> Beta features -> Native plan review (on by default). Migration 1009 stores one plan document per lineage with an append-only, attributed version history, a draft guarded by a revision token, and discussions. Approval sends a short acknowledgement instead of repeating the plan; feedback sends anchored <review_comment> blocks plus a diff of reviewer edits. Editing is Plate (MIT), lazily imported, with suggestion mode on by default. No orchestration command or event was added: everything rides the existing fork RPC seam, so upstream contracts are untouched and mobile needs no change. The Fork markers check is red for a pre-existing reason — the origin/main mirror it diffs against is behind what expbkmain has merged, so 79 files read as unmarked. This branch adds none of them. Written by Claude Opus 5 in Claude Code.
…existing plans (#57) Backfills a plan document for threads whose plan predates the feature, so the Preview entry point appears wherever a reviewable plan exists. Gives every Plate node type a component — the panel registered the types but never the components, so headings rendered at body size and the plan read as a wall of text. Adds heading hierarchy, real ul/ol/li with task checkboxes via @platejs/list-classic, tables, code blocks, quotes and links, plus a floating selection toolbar with bold/italic/underline/strikethrough/highlight/code and Comment. Commented spans highlight; tracked edits render green for insertions and red struck through for deletions. Written by Claude Opus 5 in Claude Code.
…pace (#59) Serialization is pull-based instead of running on every keystroke, and the memoized editor no longer receives an unstable prop, so typing stops re-rendering the whole Plate tree. Loading a version no longer counts as a reviewer edit — the guard compared serialized markdown against the stored version, which never matched because Plate normalizes formatting, so every open wrote a draft and looped. A rejected save now only warns when the server's draft belongs to another user. The title row and mode banner collapse into the single tab row, giving the document the vertical space. Markdown input rules are wired per element plugin the way Plate's playground does it, so typing '## ', '- ', '> ' and friends formats as you type. Written by Claude Opus 5 in Claude Code.
A session that fans work out — typically cross-repo, via the t3_create_session MCP tool — produced rows indistinguishable from work you started yourself. One orchestrating session plus four repos read as five unrelated rows. Threads now record the thread that spawned them. The experimental phase-grouped sidebar files a child under its parent as a collapsed subtree with a child-count pill, and pulls a parent into Implementing whenever anything in its subtree is working — a session that fanned work out is not "Ready" while that work is still running. Other states follow the parent's own lifecycle: a child needing input does not move its parent, because the child is the thing that needs answering and it is one disclosure away. Nesting is the agent's call. t3_create_session takes createAsChild (default true) and parentSessionId; the two cannot contradict each other. The implicit default only fires for a real provider session — an external-user token is scoped to that user's conductor thread, so defaulting to it would file every session the user creates under one synthetic root. Subtree state only surfaces while the subtree is closed: a collapsed parent carries the running glint and an outlined "↳ WORKING" badge, and once open the child that is actually working carries it. Never two sweeps for one unit of work. A row whose parent is not rendering in the same section — settled, snoozed, filtered out, deleted — renders at the top level with a breadcrumb naming it, rather than silently losing its lineage. Filtering runs against the tree, so a cross-repo match is never hidden inside a collapsed parent from another repository. Re-filing is manual and reversible: "Move under session…" and "Detach from parent" on the row context menu. Lineage stays a forest; the decider rejects any parent that would close a cycle, and the picker never offers one. Server changes are the priority/linearIssueUrl field pattern applied mechanically. Migration 1009 adds a nullable parent_thread_id plus a partial index for the cycle guard. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The lifecycle groups were fed rows that partitionPhaseSidebarRows had already filtered, so the tree builder never saw a non-matching parent — exactly the cross-repo case the ancestor-preserving filter exists for. Filtering by repo B dropped the repo A parent before nesting ran, and the matching child rendered detached instead of nested. Partition the unfiltered set instead: classification never reads the filters, and only the lifecycle groups need whole subtrees. The two shelves stay flat history lists and filter row-by-row as before. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
# Conflicts: # apps/server/src/persistence/Migrations.ts
PhaseThreadRow is memo'd, but the tree bundle was a fresh object with fresh closures on every render, so every row in the lifecycle groups re-rendered on every shell event — re-running the outbox atom read, execution presentation and work-badge resolution per row per tick. Pass flat primitives plus one actions object built once and keyed by scoped thread key, so the memo does its job again. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…tory Upstream removes a worktree only when a thread is deleted, so the only way to free the disk an archived session holds is to destroy the record of the work. On this box that is 64 GB across 335 worktrees with the root filesystem at 90%. Add the middle path. Reclaiming a session exports its history first — a digest plus a full transcript sidecar, written outside the worktree — and only then either slims the worktree (deleting node_modules, build output and caches, leaving a usable checkout) or removes it outright. The export is verified on disk before anything is deleted; a failed export aborts that session's reclaim rather than proceeding. Two gates are deliberately un-forceable, because they protect work that is not the operator's to discard: a worktree shared with a non-archived thread, and one a provider session or a live T3 deployment is running out of. Dirty-tree and unpushed-commit gates guard the operator's own work and can be forced. Worktree directories no thread references are reported but never reclaimed — nothing in the projection can vouch for them. Manual from Settings > Archived; the retention-gated automatic sweep is off by default. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
no-manual-effect-runtime-in-tests forbids Effect.runSyncExit in tests; the two requireThreadLineageAcyclic cases now use @effect/vitest's it.effect with Effect.exit, matching the other decider tests. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Both sides added fork-only RPCs to the same two registration lists, so the conflicts in `rpcForkScopes.ts` and `rpcFork.ts` were purely additive — plan review's methods and the session archive's now sit side by side. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A single-line marker added in the same hunk as the code it describes does not satisfy check-fork-markers: coverage requires every added line to be marked, and the lookbehind only searches above the hunk. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
feat(sidebar): nest agent-spawned sessions as trees in the exp sidebar
dce80f7 added sessionArchiveScan/Export/Reclaim to rpcFork but left the web UI catalog assertions at 111, so expbkmain went red on the next push. The generated count really is 114 now. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`catalog.test.ts` and `registration.test.ts` assert the total number of websocket RPCs exposed as virtual MCP tools. The archived-worktree reclaim feature adds three, so the expected total moves 111 -> 114. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
test(server): count the three archive RPCs in the web UI catalog
Agent-spawned sessions were stamped parent_thread_id = NULL, so nothing ever nested. Two links in the chain dropped the field: - dispatchCommand rebuilds a thread.bootstrap.request from the turn's bootstrap.request field by field, and did not copy parentThreadId. This is the only carrier for a prompt-bearing t3_create_session, which is the common case. - Coordinator dispatches thread.create from the resolved request and did not pass parentThreadId, so even a request that survived the rebuild was created as a root. The decider, resolver and projector were already correct and unit-tested, which is exactly why this shipped green: nothing covered the seams between them. Both are now covered, and both new Coordinator tests fail without this change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Creating a session already defaults to nesting it under the caller, with createAsChild: false to opt out. What was missing was editing lineage afterwards, so an agent can organise a workspace it did not lay out itself. t3_link_session files one session under another; t3_unlink_session returns it to the top level. Two verbs rather than one nullable field: an agent reorganising sessions has to be able to reach "detach" without emitting a literal null, and a tool named for what it does is far likelier to be picked correctly. Read access is the bar for the parent — the caller is not changing it, only pointing at it — and an inaccessible session is reported as absent, so neither tool leaks existence. The decider keeps owning the tree invariant, so a cycle fails with its message instead of a second copy of the rule drifting in the handler. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ootstrap fix session lineage on the bootstrap create path; add link/unlink MCP tools
A prompt-bearing t3_create_session does not dispatch thread.create at all. It commits thread, message and intent in a single thread.turn.start whose bootstrap.createThread carries the new thread's fields — and that construction omitted parentThreadId, so every agent-spawned session with a prompt was created at the top level. Live evidence on expbkt3: "Lineage E — EXPLICIT parent, NO prompt" persisted its parent correctly while A, B and C (all with prompts) came back NULL. The no-prompt path was already fixed; this is the branch the common case actually takes. The previous round's Coordinator tests only exercised the non-atomic branch, which is why they passed while the feature stayed broken. Both new tests fail without this change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…rn-start fix(server): carry session lineage through the atomic first turn
…ssion Adds the two filters, and fixes the one that was already there but did nothing. "Assigned to me" tested owner-or-tagged — which is exactly the server's own visibility rule in accessRules.ts. Every thread you can see already satisfies it, so ticking the box selected everything. It is now "Started by me" and tests ownership, which is the distinction that means something: my sessions versus the ones I was pulled into. "People on the session" lists teammates from the org directory and narrows to sessions they are on. Selecting two people asks for their shared sessions rather than the union, since the union is what you already see. Reconcile drops ids that leave the directory, but only when a directory set is supplied — an empty list mid-load must not wipe a live selection. Both live in the existing v1 filter blob and default off for anyone whose persisted filters predate them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The memo refactor split the row's tree bundle into flat primitives but
left the call site spreading `{...(tree ? { tree } : {})}` — a `tree`
prop that no longer exists. Every treeXxx prop arrived undefined, so
hasChildren was false: no chevron, no count pill, no indent, no lineage
context-menu items.
The nesting itself was working the whole time, which is what made this
hard to see: children were correctly folded into their parent and so
vanished from the top level, with no control to unfold them.
TypeScript cannot catch this. JSX spread expressions skip
excess-property checking, so the stale wrapper compiled clean. Noted at
the call site.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
fix(web): actually forward the session-tree props to the sidebar row
The chevron and count sat in their own flex child alongside the unread spacer, so a parent row paid for both slots plus the row's gap-2 between them: its title started ~78px in against ~30px for every neighbour, and the misalignment read as a gap rather than as structure. The disclosure now occupies the unread lane instead of adding one, and the count is bare tabular text — pill chrome cost ~10px of padding to frame a single glyph. An unread parent still shows its dot; only the empty spacer is dropped, since a row with a chevron does not need reserved space to stay aligned. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Reverses the original rule that only live work moved a parent. In practice a child stuck on an approval two levels down sat under a parent filed as "Implementing" and was invisible — nothing surfaced it until someone expanded the right row, which defeats the point of scanning Needs Input first. Precedence is now: anything in the subtree waiting on a human wins, then anything working, then the row's own phase. The parent carries an outlined "↳ APPROVAL" / "↳ INPUT" / "↳ ERROR" badge naming the most blocking descendant, so its placement never reads as a glitch, and the child count turns red rather than sky. Pending approvals never change a session's own phase, so the rollup reads the attention kind as well as the phase. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…panel Three additions to the archived-session reclaim panel: Bulk selection. "All", "None" and "Reclaimable" plus select-by-project and select-by-state pickers, because picking a set out of hundreds of archived sessions is a distinct step from acting on it. Scopes replace the selection rather than accumulating: a destructive set has to be easy to reason about. Forced removal. `reclaim` already accepted `force`; nothing exposed it. The panel now offers "Force remove" for worktrees held only by uncommitted, untracked or unpushed work, with a confirmation that names the count and says plainly that the work is lost. Live and shared worktrees are still never removed — `FORCEABLE_BLOCKED_REASONS` lives in contracts so both tiers agree on which gates an operator may override. Per-mode gate reporting. `scan` previously evaluated only `slim`, so a worktree with uncommitted work looked unblocked while a remove would refuse it. Entries now carry `removeBlockedReason` alongside `blockedReason`, and rows read Reclaimable / Needs force / Held accordingly. Entries also carry `projectName`, so select-by-project works for threads whose project the client no longer lists. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
tighten the tree disclosure and hoist parents on child attention
…66) Every control moved into the right rail so the document column owns the panel's full height. Mermaid fences render as diagrams with a source toggle; the engine is lazily imported, rendering is cancel-guarded, and it runs at securityLevel strict because the document is agent-authored. Migration 1011 records a plan's renderer. HTML plans render read-only in a sandboxed iframe with an opaque origin, so charts and animations run without reaching the surrounding page. Written by Claude Opus 5 in Claude Code.
Managing 20+ parallel sessions through the sidebar does not scale: rows are small, bulk actions hide behind a context menu the phase-grouped sidebar does not even have, and there is no way to see what a session did or how far along it is without opening it. Add a fork-owned full-screen /sessions page reached from the exp sidebar: a virtualized table with facet filters, saved views, shift/range multi-select, and a bulk toolbar (retitle, summarize, priority, snooze, settle, archive, pin, stop, delete) that fans out with bounded concurrency and reports partial failures. Two new table columns are server-generated. thread.work-summary.request runs a dedicated drainable-worker reactor that asks the configured text-generation model for a work summary plus a structured progress assessment (stage, what remains, percent), persisted on projection_threads and exposed on the thread shell so the table can render it without opening each session. Model, prompt instructions, context budget, and an enable switch live in Experiments settings, mirroring the catch-up summary section. Migration lands at 1012: 1008 is already registered to an upstream file in the 1000+ lane and expbkmain uses through 1011, and Effect only runs migrations above the highest applied id. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
# Conflicts: # apps/server/src/orchestration/Layers/ProjectionPipeline.ts # apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.test.ts # apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts # apps/server/src/persistence/Layers/ProjectionThreads.ts # apps/server/src/persistence/Migrations.ts # apps/server/src/persistence/Services/ProjectionThreads.ts # packages/contracts/src/orchestration.ts # packages/contracts/src/settings.ts
feat(sessions): bulk session manager with AI work summaries
Verified on expbkt3: a bulk Summarize over four sessions produced two real summaries and left the two archived rows spinning on "Summarizing…" forever. Archived threads are absent from the detail read model, so the reactor returned quietly after the projector had already written the pending marker, and only a terminal update clears it. Report the archived state instead, and do the same when checkpoint context is missing. Also pass extraData to the virtualized list. It only re-invokes renderItem when data or extraData changes, so selection, expansion, pending state and density — none of which live in the row data — left rows rendering stale checkboxes while the header correctly said "4 selected". Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Promotes the accumulated staging work on
expbkmain(72dd42126) to productionbkmain(b6dda6094) — 34 commits, 146 files, +22,356/−149.What ships
Plan review (#56, #57, #59, #66) — the largest piece. Native versioned plan review in the right panel, plans rendered as documents, mermaid diagrams and HTML plans, plus responsiveness fixes, false conflict warnings, and panel space.
Session trees and sidebar — agent-spawned sessions nest as trees, filtering by ownership and by who else is on a session, parents hoisted when any child needs a human, a disclosure fix that stopped indentation shoving parent titles right, and a memo fix keeping sidebar rows alive across session-tree renders.
Session lineage — lineage now carries through both the bootstrap create path and the atomic first turn, plus new MCP tools letting agents link and unlink session parents.
Archived-session worktree reclaim — reclaim archived sessions' worktrees while keeping their history, a bulk-selection and forced-removal panel, and an opt-in sweeper that no-ops unless an operator switches the automatic sweep on.
Bulk session manager with AI work summaries, and a fix for the summarize spinner on archived sessions.
Migrations
Four new, all cheap — no data backfill and no index build over the event log:
PlanReviewDocumentsplan_documents,plan_document_versions,plan_document_drafts,plan_discussions,plan_discussion_commentsProjectionThreadsParentThreadADD COLUMN parent_thread_idPlanDocumentFormatADD COLUMN formatProjectionThreadsWorkSummaryADD COLUMN work_summaryThese should be effectively instant even on bkt3's 1.9 GB database, unlike migration 1007, whose index build measured 6.4 s cold.
Dependency install required on deploy
pnpm-lock.yamlgrows by +1,631/−12, addingplatejs(rich-text editor, eight@platejs/*packages),mermaid11.16.1 anddate-fns. Expect a heavier deploy than a normal artifact swap and a larger web bundle.Confidence
72dd42126.SqlError/PersistenceDecodeError/database is locked/ backfill-failure signatures.forkParked), the slow-SQL span filter alongside upstream'struncateTraceAttributes, and.mcp.jsondeleted and gitignored. The only touch toserverRuntimeStartup.tsadds the archive sweeper; it does not disturb the parked backfill.Known failing check:
markersExpect
markersto fail, for the same false-positive reason it failed on #55, and unrelated to these 34 commits.scripts/check-fork-markers.tsdiffs againstorigin/main, the byte-pure upstream mirror — but that branch is still ata2ca89aa1, the base from before the upstream nightly merge. So upstream's own new code shows up as unmarked fork edits. Verified: of the files it flags, 73 are byte-identical to the merged nightly23f0a1ae3and contain zero fork code (packages/shared/src/shell.ts,packages/contracts/src/ipc.ts,patches/effect@4.0.0-beta.103.patchamong them).Marking those would falsely claim upstream code as fork-owned — the inversion of what the check exists for — and baselining them would loosen the ratchet. The correct fix is one fast-forward of the mirror:
With the mirror current,
FORK_UPSTREAM_REF=23f0a1ae3 node scripts/check-fork-markers.tsalready exits 0 on this branch.Deploy notes
Merging restarts
t3-bkmain, which kills any agent session hosted on it — dispatch the merge from a non-bkt3 session or a human shell. Downtime is now ~4 s rather than the ~147 s it was before #55.🤖 Generated with Claude Code
Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.