Skip to content

Promote expbkmain to bkmain: plan review, session trees, lineage, worktree reclaim - #69

Merged
tusharbhardwaj-bk merged 34 commits into
bkmainfrom
expbkmain
Aug 9, 2026
Merged

Promote expbkmain to bkmain: plan review, session trees, lineage, worktree reclaim#69
tusharbhardwaj-bk merged 34 commits into
bkmainfrom
expbkmain

Conversation

@tusharbhardwaj-bk

@tusharbhardwaj-bk tusharbhardwaj-bk commented Aug 9, 2026

Copy link
Copy Markdown

Promotes the accumulated staging work on expbkmain (72dd42126) to production bkmain (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:

id what
1009 PlanReviewDocuments creates plan_documents, plan_document_versions, plan_document_drafts, plan_discussions, plan_discussion_comments
1010 ProjectionThreadsParentThread ADD COLUMN parent_thread_id
1011 PlanDocumentFormat ADD COLUMN format
1012 ProjectionThreadsWorkSummary ADD COLUMN work_summary

These 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.yaml grows by +1,631/−12, adding platejs (rich-text editor, eight @platejs/* packages), mermaid 11.16.1 and date-fns. Expect a heavier deploy than a normal artifact swap and a larger web bundle.

Confidence

  • CI green on 72dd42126.
  • Soaked ~15.7 h on expbkt3 (deployed 2026-08-08 14:57:32), one clean start, zero SqlError / PersistenceDecodeError / database is locked / backfill-failure signatures.
  • The production fixes from Promote expbkmain to bkmain: startup-stall + write-amplification fixes and upstream nightly 20260807.1023 #55 are verified intact at this head: all four SQLite pragmas, the ownership-backfill fast path (still wrapped in forkParked), the slow-SQL span filter alongside upstream's truncateTraceAttributes, and .mcp.json deleted and gitignored. The only touch to serverRuntimeStartup.ts adds the archive sweeper; it does not disturb the parked backfill.

Known failing check: markers

Expect markers to fail, for the same false-positive reason it failed on #55, and unrelated to these 34 commits.

scripts/check-fork-markers.ts diffs against origin/main, the byte-pure upstream mirror — but that branch is still at a2ca89aa1, 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 nightly 23f0a1ae3 and contain zero fork code (packages/shared/src/shell.ts, packages/contracts/src/ipc.ts, patches/effect@4.0.0-beta.103.patch among 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:

git push origin 23f0a1ae3:main

With the mirror current, FORK_UPSTREAM_REF=23f0a1ae3 node scripts/check-fork-markers.ts already 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


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

tusharbhardwaj-bk and others added 30 commits August 8, 2026 00:37
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>
tusharbhardwaj-bk and others added 4 commits August 8, 2026 13:58
# 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>
@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XXL labels Aug 9, 2026
@tusharbhardwaj-bk
tusharbhardwaj-bk merged commit 387a65b into bkmain Aug 9, 2026
13 of 14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant