diff --git a/bun.lock b/bun.lock
index c52d9e6b17..b4af44142a 100644
--- a/bun.lock
+++ b/bun.lock
@@ -627,6 +627,14 @@
"typescript": "catalog:",
},
},
+ "packages/quark": {
+ "name": "effect-quark",
+ "version": "0.0.0",
+ "dependencies": {
+ "alien-signals": "3.2.1",
+ "solid-js": "catalog:",
+ },
+ },
"packages/schema": {
"name": "@opencode-ai/schema",
"version": "1.17.11",
@@ -893,6 +901,7 @@
"clipboardy": "4.0.0",
"diff": "catalog:",
"effect": "catalog:",
+ "effect-quark": "workspace:*",
"fuzzysort": "catalog:",
"get-east-asian-width": "catalog:",
"open": "10.1.2",
@@ -3315,6 +3324,8 @@
"ajv-i18n": ["ajv-i18n@4.2.0", "", { "peerDependencies": { "ajv": "^8.0.0-beta.0" } }, "sha512-v/ei2UkCEeuKNXh8RToiFsUclmU+G57LO1Oo22OagNMENIw+Yb8eMwvHu7Vn9fmkjJyv6XclhJ8TbuigSglPkg=="],
+ "alien-signals": ["alien-signals@3.2.1", "", {}, "sha512-I8FjmltrfnDFoZedi5CG8DghVYNhzb/Ijluz7tCSJH0xpd0484Kowhbb1XDYOxfJpU1p5wnM2X54dA+IfGyD1g=="],
+
"ansi-align": ["ansi-align@3.0.1", "", { "dependencies": { "string-width": "^4.1.0" } }, "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w=="],
"ansi-colors": ["ansi-colors@4.1.3", "", {}, "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw=="],
@@ -3859,6 +3870,8 @@
"effect": ["effect@4.0.0-beta.98", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.9.0", "find-my-way-ts": "^0.1.6", "ini": "^7.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^2.0.4", "multipasta": "^0.2.8", "toml": "^4.1.2", "uuid": "^14.0.1", "yaml": "^2.9.0" } }, "sha512-oz+bsG5h+6RNrw4t5GMfQrk/xBS8ROoqkYsuvRhBr5O7mCOrpvH/hbw+QrDzvKIpX4HJClwm86F94c87W0sJxg=="],
+ "effect-quark": ["effect-quark@workspace:packages/quark"],
+
"ejs": ["ejs@3.1.10", "", { "dependencies": { "jake": "^10.8.5" }, "bin": { "ejs": "bin/cli.js" } }, "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA=="],
"electron": ["electron@42.3.3", "", { "dependencies": { "@electron/get": "^5.0.0", "@types/node": "^24.9.0", "extract-zip": "^2.0.1" }, "bin": { "electron": "cli.js", "install-electron": "install.js" } }, "sha512-0MwYp9wTb7TrtTalOYqeW+suqd9T/Znstr/nDLKqFGIjHdBZX339guo3mQqTPURRZ/UQmYM4uMpzKpI5wLptfQ=="],
diff --git a/docs/design/quark-architecture-review.md b/docs/design/quark-architecture-review.md
new file mode 100644
index 0000000000..45da7f1c78
--- /dev/null
+++ b/docs/design/quark-architecture-review.md
@@ -0,0 +1,303 @@
+# Quark Timeline Architecture — and How It Can Be Faster
+
+An architecture review of the `opencode-quark-timeline` experiment: what the layers are, why the design is sound, and the *mechanical* reason it can beat the Solid Store timeline — with the actual code.
+
+## The Stack in One Picture
+
+Four small layers, each with one job. The reusable core remains about 200 lines.
+
+```mermaid
+flowchart TD
+ E[Server events] --> D[DataProvider
Solid Store, unchanged]
+ D --> R[createSessionRows
row reduction policy]
+ R -->|"set / update / insert / remove"| K[Keyed<SessionRow, string>
packages/quark/src/keyed.ts]
+ K -->|"slots — structure channel"| FOR["Solid <For>"]
+ K -->|"slot — per-row value channel"| UV[useValue adapter
one Solid signal per row]
+ K -->|"slots + immutable identity"| BM[boundaries memo]
+ UV --> SRV[SessionRowView]
+ FOR --> SRV
+```
+
+```tree
+packages/quark/src
+├── reactivity.ts # 59 lines — Readable/Writable/Computed/Transaction over alien-signals
+├── keyed.ts # Keyed collection (the whole idea lives here)
+├── solid.ts # 8 lines — useValue: one quark Readable → one Solid signal
+└── index.ts # 2 lines — exports
+```
+
+The experimental boundary is disciplined: `DataProvider`, the event protocol,
+rendered row policy, and `SessionRowView` are unchanged. Row ownership and the
+incremental mutation mechanics changed; batch and live grouping now share one
+policy helper. The boundary remains narrow enough to attribute regressions or
+improvements.
+
+## The Core Idea: Identity and Equivalence Are Inputs, Not Discoveries
+
+This is the entire architectural bet. Solid Store must *discover* what changed; Quark is *told* what identity and sameness mean, once, at construction:
+
+```typescript title="packages/tui/src/routes/session/rows.ts" caption="The complete reconciliation policy is two functions"
+const state = Keyed.make({ key: rowKey, equivalent: sameRow })
+```
+
+`rowKey` answers *"which slot is this?"* — including the subtle case where a group's key is the ref that **created** it, so refs moving to `pending` can never change identity. Keys are precomputed once at row construction with length-prefixed segments, so reconciliation pays zero key-extraction work:
+
+```typescript title="packages/tui/src/routes/session/rows.ts" caption="Keys are built once, at construction — rowKey is a field read"
+export type SessionRow = { readonly id: string } & ( /* ...variants... */ )
+
+function partRow(ref: PartRef): SessionRow {
+ return { id: `p${segment(ref.messageID)}${segment(ref.partID)}`, type: "part", ref }
+}
+
+function segment(value: string) {
+ return `${value.length}:${value}` // unambiguous without trusting a delimiter
+}
+
+function rowKey(row: SessionRow) {
+ return row.id
+}
+```
+
+`sameRow` answers a different question: *"can any consumer tell these two values apart?"* It compares only render-relevant fields. If it says yes-they're-the-same, **nothing downstream runs at all**.
+
+## Two Channels Instead of One
+
+A Solid Store exposes one reactive graph; every consumer subscribes into the same proxy web. `Keyed` splits change into two independent surfaces:
+
+```typescript title="packages/quark/src/keyed.ts" start=4
+export interface Keyed {
+ readonly slots: Readable[]> // fires ONLY on insert/remove/reorder
+ readonly values: Readable // fires on any current value change
+ has(key: Key): boolean
+ get(key: Key): Readable | undefined
+ set(values: readonly A[]): void
+ update(value: A): boolean
+ insert(value: A, position?: "end" | { before: Key } | { after: Key }): Readable
+ remove(key: Key): boolean
+ move(key: Key, position?: "end" | { before: Key } | { after: Key }): boolean
+}
+```
+
+```definitions
+[
+ { "term": "slots", "definition": "The structure channel. An array of stable per-row readables. Its identity changes only when membership or order changes. subscribes here." },
+ { "term": "slot", "definition": "The value channel. One writable signal per row identity. A streaming delta touches exactly one slot. The row's component subscribes here via useValue." },
+ { "term": "values", "definition": "The aggregate channel. A lazy computed mapping slots to current values. The TUI uses it only for internal mutation snapshots and does not subscribe boundaries to it." }
+]
+```
+
+This separation is what a proxy-based store cannot give you for free: **a value change and a structure change are different events**, published to different audiences.
+
+## How It CAN Be Faster: One Unique Row Update, Step by Step
+
+The highest-frequency reducer workload is a repeated streaming ordinal, which
+the seen-part set rejects in expected `O(1)` before reading the aggregate. The
+next useful comparison is a unique event that extends or completes an existing
+group row. Trace that value update through both systems.
+
+### Before: Solid Store path
+
+```typescript caption="Old hot path — every access and write crosses a proxy"
+setRows(produce((draft) => {
+ // 1. draft is a proxy — every property read is a trap
+ // 2. finding the group row walks proxied array elements
+ // 3. the mutation writes through proxy machinery
+ // 4. Solid records fine-grained dependencies per touched path
+ append(draft, ref, part, queuedStart(draft))
+}))
+// ...and on reconnect / revert / rebuild:
+setRows(reconcile(reduce()))
+// reconcile must re-derive identity from item references,
+// walk every row's shape, and diff against the proxy graph
+```
+
+Solid's generality — arbitrary nested access, partial path writes, plain objects — is paid for on **every operation** with proxy traps, shape inspection, and dependency bookkeeping.
+
+### After: Quark path
+
+The whole collection is a closure over three pieces of state — the structure signal, the identity map, and the declared equivalence. `update` reads directly against them:
+
+```typescript title="packages/quark/src/keyed.ts" caption="The closure state update() operates on, plus the whole hot path"
+export function make(options: {
+ readonly key: (value: A) => Key
+ readonly equivalent?: (left: A, right: A) => boolean
+}): Keyed {
+ const slots = State.make[]>([]) // structure channel: one signal holding the slot array
+ const byKey = new Map>() // identity → slot address, maintained by set/insert/remove
+ const equivalent = options.equivalent ?? Object.is // declared sameness (sameRow in the TUI)
+ const values = Computed.make((previous) => {
+ const next = slots().map((slot) => slot()) // aggregate channel, lazy until subscribed
+ return same(previous, next) ? previous! : next
+ })
+
+ return {
+ slots,
+ values,
+ // ...
+ update(value) {
+ const key = options.key(value) // 1. one key extraction (a field read: row.id)
+ const slot = byKey.get(key) // 2. one Map lookup — an address, not a search
+ if (!slot) throw new Error(`Keyed value does not exist: ${String(key)}`)
+ if (equivalent(slot(), value)) return false // 3. one equivalence check — early cutoff
+ slot.set(value) // 4. one signal write; slots is untouched
+ return true
+ },
+ // ...
+ }
+}
+```
+
+Note what `update` never touches: `slots`. A value change writes one slot signal and the structure channel stays reference-identical — that single fact is the structural cutoff below.
+
+Four operations. No proxies, no shape discovery, no graph diff. Then the adapter forwards the change to exactly one Solid signal:
+
+```typescript title="packages/quark/src/solid.ts" caption="The entire Solid boundary — 8 lines"
+export function useValue(readable: Readable): Accessor {
+ const [value, setValue] = createSignal(readable())
+ onCleanup(readable.subscribe((next) => setValue(() => next)))
+ return value
+}
+```
+
+### The structural cutoff — why `` never even wakes up
+
+Because `slots` only fires on structural change, a value-only update leaves the outer array **reference-identical**. The `` signal never fires, so keyed-list diffing never runs and the component owner survives:
+
+```typescript caption="Value-only transition: structure untouched, owner preserved"
+const structure = rows.slots()
+const groupSlot = structure[0]
+
+rows.update({ ...initial, completed: true })
+
+rows.slots() === structure // true — receives no change at all
+rows.slots()[0] === groupSlot // true — component owner survives
+groupSlot().completed === true // true — the one subscribed row re-renders
+```
+
+This is also the **beauty** mechanism, not just speed: an expanded reasoning group keeps its local state through permission repartitioning, because refs moving between `refs` and `pending` is a value change on a stable identity — never a remount.
+
+### The membership cutoff — duplicate deltas avoid the aggregate entirely
+
+A route-scoped set contains every visible part identity, including refs nested
+inside groups. A duplicate streaming delta dies at `seenParts.has(id)` before
+reading `state.values()`. Duplicate message and footer events use `Keyed.has`
+against the collection's existing key map. For a replacement that reaches
+`update`, declared equivalence remains the final publication cutoff.
+
+## More Before / After, From the Actual Diff
+
+### Wiring the renderer: one ``, two subscription levels
+
+Before, every row component read its row through the store proxy, and any reconcile could disturb the list:
+
+```tsx title="packages/tui/src/routes/session/index.tsx" caption="Before — one reactive graph for everything"
+
+ {(row, index) => (
+
+ )}
+
+```
+
+After, `KeyedFor` subscribes to the structure channel and each row component subscribes to exactly one slot:
+
+```tsx title="packages/tui/src/routes/session/index.tsx" caption="After — structure and value subscriptions are separate"
+
+ {(row, index) => (
+
+ )}
+
+```
+
+### Appending a footer row
+
+```typescript caption="Before — mutate a proxied draft; Solid infers what changed"
+setRows(
+ produce((draft) => {
+ if (draft.some((row) => row.type === "assistant-footer" && row.messageID === messageID)) return
+ const index = queuedStart(draft)
+ completePrevious(draft, index)
+ draft.splice(index, 0, { type: "assistant-footer", messageID })
+ }),
+)
+```
+
+```typescript caption="After — say exactly what happened: maybe one value update, then one insert"
+mutate(() => {
+ if (state.has(footerRowID(messageID))) return
+ const current = state.values()
+ const index = queuedStart(current)
+ complete(current, index) // one state.update on the previous group, if open
+ insert(current, index, footerRow(messageID)) // one new slot + one structural publication
+})
+```
+
+Same policy, but the after version *names* its effects: at most one slot value change plus one structural change, flushed together by `Transaction.run`. Nothing has to diff anything to figure that out afterward.
+
+### Removing a row
+
+```typescript caption="Before — linear search, then splice through the proxy"
+setRows(
+ produce((draft) => {
+ const index = draft.findIndex((row) => row.type === "assistant-footer" && row.messageID === messageID)
+ if (index !== -1) draft.splice(index, 1)
+ }),
+)
+```
+
+```typescript caption="After — remove by identity; the ownership law does the rest"
+mutate(() => {
+ state.remove(footerRowID(messageID))
+})
+```
+
+### Full rebuild (reconnect, revert, compaction)
+
+```typescript caption="Before — reconcile re-derives identity from scratch"
+setRows(reconcile(reduce()))
+```
+
+```typescript caption="After — set() reconciles with declared identity and equivalence"
+setRows(reduce()) // → state.set(next): Map lookups + sameRow checks; unchanged rows publish nothing
+```
+
+This is the one place the two systems do comparable O(N) work — and it's exactly the workload the checked benchmark measured. Every path above it is where Quark does structurally *less*.
+
+## The Cost Ledger
+
+| Per operation | Solid Store (`produce`/`reconcile`) | Quark `Keyed` |
+| --- | --- | --- |
+| Find the row | proxied array walk / identity re-derivation | one `Map.get` |
+| Detect "no change" | proxy-graph diff per touched path | one `equivalent()` call |
+| Value change | proxy writes + per-path invalidation | one signal write → one Solid signal |
+| Structure unchanged | reconcile still walks the list | outer array identity preserved; `` silent |
+| Structure changed | full reconcile | one new array; retained slots reused by reference |
+| Batch of writes | store batching | one `Transaction.run` — settled publication |
+
+Same asymptotics — `O(N)` for a whole-array `set` — but far fewer instructions, allocations, and invalidations per unit of change. **The speedup is a constant-factor win purchased with a stronger contract** (unique stable keys, declared equivalence), not framework magic. The checked-in claim: ~4.7× on sparse value publication, ~6.4× on reorder, at 1,000 rows.
+
+## Architecture Verdict
+
+### What's genuinely good
+
+- **Deep module, tiny surface.** One small `keyed.ts` module carries the whole idea; the laws (unique key, stable key, equivalence, structural cutoff, settled publication, ownership) are explicit and testable.
+- **The adapter is honest.** `useValue` is 8 lines and creates no second reconciliation system — the failure mode the design doc itself warns about.
+- **Identity-as-input is the right call** for this domain: OpenCode *has* real identities (messageID, partID) and was previously throwing that information away for Solid to rediscover.
+- **Falsifiable framing.** The docs predict where Quark should lose. That's rare and worth preserving.
+
+### Where the architecture leaked — all fixed during review
+
+Every leak identified in the first pass has since been closed:
+
+- ~~**`JSON.stringify` keys.**~~ Keys are precomputed `id` fields built once at row construction with length-prefixed segments (`rows.ts` smart constructors); `rowKey` is a field read.
+- ~~**The aggregate channel reintroduces O(N) per delta.**~~ `boundaries` now derives from the structure channel (`rows.slots()`) and reads slot values untracked (quark reads are invisible to Solid's tracker); the raw `values` readable is no longer mirrored into Solid. Per-delta boundary cost is zero; the O(N) recompute fires only on structural change or tracked `messages()` field changes.
+- ~~**Boundary staleness as an implicit invariant.**~~ The untracked-read trick is now type-enforced: `messageBoundaryIDs` accepts `readonly BoundaryRow[]`, a projection of `Pick`ed identity-immutable fields, so a future dependence on a mutable row field is a compile error.
+- ~~**Two grouping engines.**~~ Join-vs-insert policy is unified in `appendDecision` + `groupKind`; both the batch `reduce()` path and the incremental `appendPart` path consume it, and row construction goes through shared smart constructors.
+- ~~**Benchmark not checked in / wrong hot path.**~~ `packages/quark/bench/` now exists and covers incremental `keyed.update` — including against Solid's *direct path write*, the adversarial case — plus dense updates and reorders.
+- ~~**No work counters.**~~ `Keyed` accepts an optional `Metrics` sink; `OPENCODE_QUARK_METRICS=1` reports timeline counters on cleanup.
+- ~~**Opaque positioning and adapter boilerplate.**~~ `insert` and `move` now share an explicit `Position` vocabulary (`"end"`, `before`, or `after`), `get` exposes stable slot addresses, and `KeyedFor` encapsulates the two-level Solid subscription.
+
+The remaining asymmetry — `complete` (immutable `state.update`) vs `completePrevious` (in-place mutation on a plain array under construction) — is inherent to the two contexts, three lines each, and shares the same policy. Not worth abstracting.
+
+None of these are structural flaws — they are integration debts. The layering itself (laws → Keyed → thin adapter → unchanged renderer) is the right shape, and it's the shape worth keeping even if every number in the current doc had to be re-measured.
+
+See `quark-review-recommendations.md` for the prioritized fix list.
diff --git a/docs/design/quark-performance-model.md b/docs/design/quark-performance-model.md
new file mode 100644
index 0000000000..84abf5175e
--- /dev/null
+++ b/docs/design/quark-performance-model.md
@@ -0,0 +1,550 @@
+# Why Quark Can Be Faster Than Solid Store
+
+Status: experimental performance model
+
+## Conclusion
+
+Quark can be faster than the current Solid Store timeline because it accepts a
+stronger contract and uses that contract to do less runtime work.
+
+The advantage does **not** come from TypeScript types by themselves. TypeScript
+types disappear at runtime. The useful information comes from explicit runtime
+inputs and API laws:
+
+- A row key is unique and cannot change during its ownership lifetime.
+- Row equivalence is supplied before updates begin.
+- A keyed list has one stable slot per key.
+- Value changes and structural changes publish through separate channels.
+- A transaction publishes only settled graph state.
+
+Those guarantees remove choices from the hot path. Quark does not need to
+rediscover identity, shape, equivalence, index membership, or ownership on
+every update. Less discovery means fewer comparisons, proxy traps,
+allocations, writes, and downstream invalidations.
+
+This is a conditional claim, not a claim that Quark is universally faster than
+Solid. Solid can be as fast or faster when an application performs a precise
+direct store write, when lists are tiny, or when Quark receives no useful
+identity information. The relevant comparison is the current OpenCode path:
+whole projected records and ordered rows are reconciled into a Solid Store.
+
+## The Physical Argument Is “Do Less Work”
+
+Software cannot escape the physical costs of instructions, memory reads,
+allocations, pointer writes, cache misses, and notification fan-out. An
+abstraction can only become faster by reducing those costs or arranging them
+more favorably.
+
+Quark's proposed advantage has four concrete sources:
+
+1. **Declare repeated decisions once.** Identity and equivalence become
+ preconfigured functions rather than per-event discovery.
+2. **Replace searches with addresses.** Stable keys resolve to persistent
+ slots through a `Map`.
+3. **Separate unrelated change dimensions.** An item value can change without
+ publishing a new list structure.
+4. **Stop propagation early.** Schema equivalence and slot equivalence suppress
+ writes before they reach Solid or the terminal renderer.
+
+These are ordinary computer costs, not framework mythology. If profiling shows
+that Quark executes the same work as Solid plus an adapter, the hypothesis is
+false. If counters show fewer structural publications, fewer component-owner
+reconciliations, and fewer proxy/store operations, the speedup has a mechanical
+explanation.
+
+## Solid Store Must Preserve Generality
+
+The current timeline asks Solid Store to accept ordinary JavaScript objects and
+reconcile new projections into a proxy-backed graph.
+
+```mermaid
+flowchart LR
+ A[Projected rows] --> B[Solid reconcile]
+ B --> C[Inspect keys and shape]
+ C --> D[Proxy-backed Store writes]
+ D --> E[Dependency invalidation]
+ E --> F[For reconciliation]
+ F --> G[SessionRowView]
+```
+
+Solid's generality is valuable. It can react to arbitrary nested property
+access, partial path writes, and plain objects without requiring a schema-owned
+model. That generality also means the runtime must maintain proxy metadata and
+interpret each reconciliation against the current store graph.
+
+Solid is not inherently inefficient. A precise call such as
+`setStore(index, "value", next)` can avoid whole-list reconciliation. OpenCode's
+timeline frequently receives whole projected messages or rebuilt row arrays,
+however, so its current implementation uses `produce` and `reconcile` to
+recover identity and preserve nested owners.
+
+## Quark Makes Identity an Input
+
+`Keyed` receives the identity and equivalence functions when the collection is
+created. In the TUI, every row carries a collision-safe primitive `id`. Groups
+also retain their immutable origin for message-boundary projection:
+
+```typescript
+type PartRef = {
+ messageID: string
+ partID: string
+}
+
+type SessionRow = { readonly id: string } & (
+ | { type: "message"; messageID: string }
+ | { type: "compaction-queued"; inputID: string }
+ | { type: "part"; ref: PartRef }
+ | {
+ type: "group"
+ kind: "reasoning"
+ origin: PartRef
+ refs: PartRef[]
+ completed: boolean
+ }
+ | {
+ type: "group"
+ kind: "exploration"
+ origin: PartRef
+ refs: PartRef[]
+ pending: PartRef[]
+ completed: boolean
+ }
+ | { type: "assistant-footer"; messageID: string }
+)
+```
+
+Factories build the ID once with length-prefixed segments. Length prefixes
+avoid delimiter collisions without invoking a serializer:
+
+```typescript
+function segment(value: string) {
+ return `${value.length}:${value}`
+}
+
+function groupRow(kind: "reasoning" | "exploration", origin: PartRef): SessionRow {
+ const id = `g${kind === "reasoning" ? "r" : "e"}${segment(origin.messageID)}${segment(origin.partID)}`
+ if (kind === "reasoning") return { id, type: "group", kind, origin, refs: [origin], completed: false }
+ return { id, type: "group", kind, origin, refs: [origin], pending: [], completed: false }
+}
+
+function rowKey(row: SessionRow) {
+ return row.id
+}
+```
+
+`sameRow` answers a different question: can consumers distinguish these two
+values for the same identity? It compares only fields that affect row
+rendering.
+
+```typescript
+function sameRow(left: SessionRow, right: SessionRow) {
+ if (left.type !== right.type) return false
+
+ if (left.type === "message" && right.type === "message") return left.messageID === right.messageID
+
+ if (left.type === "compaction-queued" && right.type === "compaction-queued") return left.inputID === right.inputID
+
+ if (left.type === "part" && right.type === "part") return sameRef(left.ref, right.ref)
+
+ if (left.type === "assistant-footer" && right.type === "assistant-footer") return left.messageID === right.messageID
+
+ if (left.type !== "group" || right.type !== "group") return false
+ if (left.kind !== right.kind) return false
+ if (left.completed !== right.completed) return false
+ if (!sameRefs(left.refs, right.refs)) return false
+
+ if (left.kind === "reasoning" || right.kind === "reasoning") return true
+ return sameRefs(left.pending, right.pending)
+}
+
+function sameRefs(left: PartRef[], right: PartRef[]) {
+ return left.length === right.length && left.every((ref, index) => sameRef(ref, right[index]))
+}
+
+function sameRef(left: PartRef, right: PartRef) {
+ return left.messageID === right.messageID && left.partID === right.partID
+}
+```
+
+Those functions form the complete reconciliation policy:
+
+```typescript
+const rows = Keyed.make({
+ key: rowKey,
+ equivalent: sameRow,
+})
+
+rows.set(nextRows)
+```
+
+It exposes two reactive surfaces:
+
+```typescript
+rows.slots // changes only when keys are inserted, removed, or reordered
+rows.values // changes when any current value changes
+```
+
+Each slot is itself a readable value. Solid `` receives `slots`, so a
+group can grow or complete without replacing its component owner. Generic
+aggregate consumers can subscribe to `values` when they need every value
+change.
+
+The TUI's message-boundary projection deliberately does not subscribe to
+`values`. Boundary identity uses only immutable row fields, so it tracks the
+structural `slots` array and reads each slot value untracked. Value-only deltas
+therefore perform no boundary work; inserts, removals, reorders, or message
+changes recompute boundaries.
+
+Here is a complete value-only transition. The group changes, but its key and
+position do not:
+
+```typescript
+const origin = { messageID: "assistant-1", partID: "call-read" }
+const initial = groupRow("exploration", origin)
+
+rows.set([initial])
+
+const structure = rows.slots()
+const groupSlot = structure[0]
+
+rows.update({
+ ...initial,
+ completed: true,
+})
+
+rows.slots() === structure // true: receives no structural change
+rows.slots()[0] === groupSlot // true: component owner survives
+groupSlot().completed === true // true: row value updated
+```
+
+A structural insertion changes the outer list but preserves every retained
+slot:
+
+```typescript
+const footer: SessionRow = {
+ id: "f11:assistant-1",
+ type: "assistant-footer",
+ messageID: "assistant-1",
+}
+
+const footerSlot = rows.insert(footer)
+
+rows.slots().length === 2 // true
+rows.slots()[0] === groupSlot // true
+rows.slots()[1] === footerSlot // true
+```
+
+Removing and later reinserting a key begins a new ownership lifetime:
+
+```typescript
+rows.remove(footer.id)
+const nextFooterSlot = rows.insert(footer)
+
+nextFooterSlot === footerSlot // false
+```
+
+```mermaid
+flowchart LR
+ A[Next rows] --> B[Keyed.set]
+ B --> C{Same keys and order?}
+ C -->|yes| D[Keep slots array]
+ C -->|no| E[Publish slots array]
+ B --> F{Equivalent value?}
+ F -->|yes| G[No slot write]
+ F -->|no| H[Publish one slot]
+ D --> I[Solid For unchanged]
+ E --> J[Solid For reconciles structure]
+ H --> K[Existing SessionRowView updates]
+```
+
+The key distinction is that **row identity is not inferred from row value**.
+The timeline gives every reasoning or exploration group an immutable creation
+key. Permission partitioning may move refs between `refs` and `pending`, but it
+cannot change the group slot or reset local expanded state.
+
+## The `Keyed.set` Algorithm
+
+The current algorithm is deliberately small:
+
+```text
+set(next):
+ 1. Compute every next key and reject duplicates.
+ 2. Build Map.
+ 3. For each next value:
+ a. Reuse the previous slot for its key, or create one.
+ b. Compare previous and next values.
+ c. Write only a changed slot.
+ 4. Publish the outer slot array only if membership or order changed.
+ 5. Flush slot and structural writes in one transaction.
+```
+
+The aggregate `values` readable maps the current slots to their values. It is a
+computed node, so it remains lazy without subscribers and publishes one settled
+array when subscribed.
+
+The algorithm preflights duplicate keys before any slot mutation. A rejected
+set therefore cannot partially update the graph. `Map` and `Set` give keys
+SameValueZero semantics, including consistent treatment of `0`/`-0` and
+`NaN`.
+
+## The Laws That Unlock the Optimizations
+
+```definitions
+[
+ {
+ "term": "Unique key law",
+ "definition": "No two current values have the same key. This permits one Map entry and one slot per identity."
+ },
+ {
+ "term": "Stable key law",
+ "definition": "An entity or logical row keeps its key for its lifetime. This permits component ownership and subscriptions to survive value changes."
+ },
+ {
+ "term": "Equivalence law",
+ "definition": "If equivalent(left, right) is true, publishing right cannot change any consumer-visible meaning. This permits early cutoff."
+ },
+ {
+ "term": "Structural cutoff law",
+ "definition": "Value-only updates preserve the exact outer slots-array identity. This prevents keyed-list reconciliation for non-structural changes."
+ },
+ {
+ "term": "Settled publication law",
+ "definition": "Subscribers observe the state after every slot and structural write in the operation, never a partial combination."
+ },
+ {
+ "term": "Ownership law",
+ "definition": "Removing a key removes its slot from the collection. Reinserting that key creates a fresh slot; old external references cannot attach to a new lifetime."
+ }
+]
+```
+
+These laws are stronger than a plain `Array