refactor(tui): extract session timeline state

This commit is contained in:
Kit Langton 2026-07-17 21:47:34 -04:00
commit a653744d78
15 changed files with 1897 additions and 586 deletions

View file

@ -1,6 +1,6 @@
# 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.
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
@ -34,13 +34,13 @@ 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:
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:
`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... */ )
@ -58,7 +58,7 @@ function rowKey(row: SessionRow) {
}
```
`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**.
`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
@ -66,8 +66,8 @@ A Solid Store exposes one reactive graph; every consumer subscribes into the sam
```typescript title="packages/quark/src/keyed.ts" start=4
export interface Keyed<A, Key> {
readonly slots: Readable<readonly Readable<A>[]> // fires ONLY on insert/remove/reorder
readonly values: Readable<readonly A[]> // fires on any current value change
readonly slots: Readable<readonly Readable<A>[]> // fires ONLY on insert/remove/reorder
readonly values: Readable<readonly A[]> // fires on any current value change
has(key: Key): boolean
get(key: Key): Readable<A> | undefined
set(values: readonly A[]): void
@ -98,13 +98,15 @@ 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))
}))
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,
@ -123,10 +125,10 @@ export function make<A, Key>(options: {
readonly equivalent?: (left: A, right: A) => boolean
}): Keyed<A, Key> {
const slots = State.make<readonly Writable<A>[]>([]) // structure channel: one signal holding the slot array
const byKey = new Map<Key, Writable<A>>() // identity → slot address, maintained by set/insert/remove
const equivalent = options.equivalent ?? Object.is // declared sameness (sameRow in the TUI)
const byKey = new Map<Key, Writable<A>>() // identity → slot address, maintained by set/insert/remove
const equivalent = options.equivalent ?? Object.is // declared sameness (sameRow in the TUI)
const values = Computed.make<readonly A[]>((previous) => {
const next = slots().map((slot) => slot()) // aggregate channel, lazy until subscribed
const next = slots().map((slot) => slot()) // aggregate channel, lazy until subscribed
return same(previous, next) ? previous! : next
})
@ -135,11 +137,11 @@ export function make<A, Key>(options: {
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
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
slot.set(value) // 4. one signal write; slots is untouched
return true
},
// ...
@ -169,20 +171,21 @@ const groupSlot = structure[0]
rows.update({ ...initial, completed: true })
rows.slots() === structure // true — <For> receives no change at all
rows.slots()[0] === groupSlot // true — component owner survives
groupSlot().completed === true // true — the one subscribed row re-renders
rows.slots() === structure // true — <For> 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.
The compiled `parts` members index contains every visible part identity,
including refs nested inside groups. A duplicate streaming delta dies at
`state.hasMember("parts", 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 `modify`, generated equivalence remains the
final publication cutoff.
## More Before / After, From the Actual Diff
@ -226,12 +229,12 @@ 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
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.
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
@ -260,18 +263,18 @@ setRows(reconcile(reduce()))
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*.
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; `<For>` silent |
| Structure changed | full reconcile | one new array; retained slots reused by reference |
| Batch of writes | store batching | one `Transaction.run` — settled publication |
| 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; `<For>` 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.
@ -281,7 +284,7 @@ Same asymptotics — `O(N)` for a whole-array `set` — but far fewer instructio
- **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.
- **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
@ -292,7 +295,7 @@ Every leak identified in the first pass has since been closed:
- ~~**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.
- ~~**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.

View file

@ -80,37 +80,45 @@ 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
`Layout.collection` compiles identity, equivalence, and indexes once when the
timeline is created. 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
}
const PartRefLayout = Layout.struct({
messageID: Layout.string,
partID: Layout.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 }
)
const SessionRowLayout = Layout.keyedUnion({
key: Layout.key("id", Layout.string),
tag: "type",
variants: {
message: Layout.struct({ messageID: Layout.string }),
"compaction-queued": Layout.struct({ inputID: Layout.string }),
part: Layout.struct({ ref: PartRefLayout }),
group: Layout.union({
tag: "kind",
variants: {
reasoning: Layout.struct({
origin: Layout.immutable(PartRefLayout),
refs: Layout.array(PartRefLayout),
completed: Layout.boolean,
}),
exploration: Layout.struct({
origin: Layout.immutable(PartRefLayout),
refs: Layout.array(PartRefLayout),
pending: Layout.array(PartRefLayout),
completed: Layout.boolean,
}),
},
}),
"assistant-footer": Layout.struct({ messageID: Layout.string }),
},
})
type PartRef = Layout.Type<typeof PartRefLayout>
type SessionRow = Layout.Type<typeof SessionRowLayout>
```
Factories build the ID once with length-prefixed segments. Length prefixes
@ -126,56 +134,28 @@ function groupRow(kind: "reasoning" | "exploration", origin: PartRef): SessionRo
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.
Generated equivalence answers a different question: can consumers distinguish
these two values for the same identity? The layout compares rendered fields,
dispatches through the row and group tags, and ignores immutable `origin`.
```typescript
function sameRow(left: SessionRow, right: SessionRow) {
if (left.type !== right.type) return false
const SessionRows = Layout.collection(
SessionRowLayout,
({ members }) => ({
parts: members(partIDs),
}),
{ backend: "generated" },
)
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
}
const rows = SessionRows.make()
```
Those functions form the complete reconciliation policy:
```typescript
const rows = Keyed.make<SessionRow, string>({
key: rowKey,
equivalent: sameRow,
})
rows.set(nextRows)
```
The `parts` index contains standalone and grouped part IDs. Duplicate stream
deltas return through `hasMember` before aggregate projection. Arbitrary
replacements derive membership automatically; the hot group-append path applies
the exact one-member delta already known from the event.
It exposes two reactive surfaces:
@ -207,10 +187,7 @@ rows.set([initial])
const structure = rows.slots()
const groupSlot = structure[0]
rows.update({
...initial,
completed: true,
})
rows.modify(initial.id, (group) => ({ ...group, completed: true }))
rows.slots() === structure // true: <For> receives no structural change
rows.slots()[0] === groupSlot // true: component owner survives
@ -269,13 +246,14 @@ The current algorithm is deliberately small:
```text
set(next):
1. Compute every next key and reject duplicates.
2. Build Map<key, previousSlot>.
2. Look up retained slots in the persistent key map.
3. For each next value:
a. Reuse the previous slot for its key, or create one.
b. Compare previous and next values.
b. Run generated equivalence over 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.
5. Synchronize declared collection indexes.
6. Flush slot, index, and structural writes in one transaction.
```
The aggregate `values` readable maps the current slots to their values. It is a
@ -322,54 +300,31 @@ These laws are stronger than a plain `Array<object>` contract. They are also
testable. Quark should reject a violated unique-key law and should have direct
tests for every other law.
## Fixed Layout Research Is Deferred
## Layout Compiles Trusted Collection Metadata
The standalone laboratory applies the same idea to records. The initial prototype
used Effect Schema, but its derived equivalence measured `1.621x` the hand
comparator's direct-update cost. Quark now experiments with a smaller trusted
in-memory `Layout` that compiles:
The timeline now uses Quark's trusted in-memory `Layout`. It supports the scope
required by this experiment:
- field names into numeric positions;
- field changes into a bit mask;
- field equivalence into precomputed functions;
- entity keys and indexed fields into fixed metadata.
- primitive, array, struct, discriminated-union, and keyed-union fields;
- immutable fields excluded from rendered equivalence;
- compiled key extraction and closure or generated equivalence;
- imperative multi-value membership and ordered first-match indexes;
- automatic index derivation for arbitrary replacements;
- typed member deltas for event-native mutations that know exact changes.
The minimal API marks the key in the shape itself:
Generated nested union comparison measured `1.06x-1.10x` handwritten cost,
versus `1.19x-1.22x` for closure Layout across three runs. Generation occurs
once when `SessionRows` is declared. The older `1.322x` closure result motivated
this backend; it no longer describes the production comparator.
```typescript
const ItemLayout = Layout.struct({
id: Layout.key(Layout.number),
value: Layout.number,
})
Automatic membership derivation is linear in an affected row's members. That
is the safe default, but it made repeated append to one growing group roughly
`27x-30x` the handwritten index path. The event already identifies the new
part, so `SessionTimeline.appendPart` supplies a typed one-member delta. Generic
completion and permission repartition retain automatic derivation.
const ItemPlan = Layout.compile(ItemLayout)
const items = ItemPlan.make([
{ id: 1, value: 10 },
{ id: 2, value: 20 },
])
items.update({ id: 2, value: 21 })
```
`ItemPlan` exposes the original field metadata, compiled key field, compiled
equivalence, and collection factory. Simple struct comparison has measured
near handwritten speed, but a representative nested session-row union measured
`1.322x` the handwritten keyed-update cost after closure specialization.
Generated comparison can remove more indirection, but it is optional research,
not a requirement for this timeline experiment.
A one-field record replacement can first compute the change mask, then write
only changed field slots. An indexed-field write knows exactly which index
buckets to remove from and add to. An entity upsert resolves directly through
its immutable key.
The static information is therefore not “the compiler knows the type.” It is
“the runtime has already compiled the layout and the application has agreed to
obey its laws.” Effect Schema can still validate or transform data at an
admission boundary without participating in collection updates. None of this
`Layout`, model, mask, index, or columnar-storage machinery is vendored into the
TUI fork. The current landing remains the small `Keyed` abstraction plus the
route reducer; broader promotion requires separate evidence.
Numeric field positions, change masks, per-field reactive slots, models, and
columnar storage remain future research. They are not required by the TUI.
## Asymptotics and Constants
@ -388,11 +343,12 @@ complexity**:
- no general nested proxy reconciliation inside Quark.
For direct collection operations, stronger bounds are possible. An immutable
key lets `Keyed.update` or `Collection.upsert` find an entity in expected `O(1)`
time, after which work is proportional to changed fields and affected declared
indexes rather than total collection size. The TUI now uses event-native
`update`, `insert`, and `remove` operations for live events; full synchronization
and revert rebuilds still use linear `set`.
key lets `has`, `get`, `hasMember`, and `modify` find their target in expected
`O(1)` time. Work is then proportional to the changed row, affected declared
indexes, and any ordered structural array copy. `SessionTimeline` caches the
active group and queued boundary IDs; inserts and removals remain `O(N)` because
the ordered slots array must be copied. Full synchronization and revert rebuilds
remain linear `set` operations.
## Controlled Benchmark Evidence
@ -402,6 +358,9 @@ The benchmarks are checked into the fork and run directly:
cd packages/quark
bun run bench:keyed
bun run bench:row-key
cd ../tui
bun run bench:timeline
```
Each invocation interleaves and rotates variants over nine measured samples
@ -425,20 +384,21 @@ Across these two invocations:
The checksum was stable, and both variants performed the same next-array
construction inside their timed workloads.
The refreshed integration benchmark measures event-native updates with the
aggregate `values` channel subscribed, plus adverse workloads. The July 17,
2026 evidence run produced:
The refreshed lower-level `Keyed` benchmark measures event-native updates with
the aggregate `values` channel subscribed, plus adverse workloads. It does not
include `SessionTimeline`, generated row equivalence, or compiled index
maintenance. The July 17, 2026 evidence run produced:
| Workload | Quark | Solid | Quark / Solid |
| ----------------------------------------- | ----------: | ----------: | ------------: |
| Direct update, no subscribers, 1,000 | 46.8 ns | 761.6 ns | 0.068x |
| Precise Solid path write, 1,000 | 54.3 ns | 327.5 ns | 0.170x |
| Subscribed aggregate, 10 rows | 608.0 ns | 7.4 us | 0.085x |
| Subscribed aggregate, 100 rows | 4.4 us | 80.0 us | 0.055x |
| Subscribed aggregate, 1,000 rows | 37.4 us | 703.1 us | 0.052x |
| Subscribed aggregate, 10,000 rows | 388.5 us | 8.1 ms | 0.044x |
| Dense 1,000-row update | 270.2 us | 2.3 ms | 0.132x |
| Unstable keys, 100 rows | 68.0 us | 245.2 us | 0.282x |
| Workload | Quark | Solid | Quark / Solid |
| ------------------------------------ | -------: | -------: | ------------: |
| Direct update, no subscribers, 1,000 | 46.8 ns | 761.6 ns | 0.068x |
| Precise Solid path write, 1,000 | 54.3 ns | 327.5 ns | 0.170x |
| Subscribed aggregate, 10 rows | 608.0 ns | 7.4 us | 0.085x |
| Subscribed aggregate, 100 rows | 4.4 us | 80.0 us | 0.055x |
| Subscribed aggregate, 1,000 rows | 37.4 us | 703.1 us | 0.052x |
| Subscribed aggregate, 10,000 rows | 388.5 us | 8.1 ms | 0.044x |
| Dense 1,000-row update | 270.2 us | 2.3 ms | 0.132x |
| Unstable keys, 100 rows | 68.0 us | 245.2 us | 0.282x |
A second independent invocation produced paired ratios of `0.084x`,
`0.170x`, `0.077x`, `0.059x`, `0.061x`, `0.042x`, `0.130x`, and `0.266x`
@ -455,6 +415,24 @@ falsified prediction for this setup, not evidence that Solid can never win a
direct-write comparison. The production TUI has no aggregate subscriber; it
tracks structural slots and immutable boundary metadata instead.
The checked-in `SessionTimeline` benchmark compares the final domain module to
the previous handwritten `Keyed + Set` reducer mechanics and the original Solid
Store `produce` path. Across three runs:
| Workload | SessionTimeline result |
| ----------------------------------------------------- | ----------------------: |
| Growing reasoning-group append / handwritten Quark | about `1.3x-1.5x` |
| Growing reasoning-group append / Solid `produce` | about `0.013x` |
| Duplicate part in a 1,000-ref group / Solid `produce` | about `0.0004x-0.0005x` |
The final module pays roughly 30%-50% over the minimal handwritten Quark path
for compiled layout dispatch, domain policy, and maintained index ownership.
It remains roughly 74x-79x faster than the baseline Solid append in this
growing-group stress workload. Duplicate lookup is expected `O(1)` through the
compiled members index; the Solid baseline scans 1,000 refs, producing an
approximately 1,960x-2,500x stress-case difference. These are state-operation
results, not claims about total OpenCode response latency.
Key extraction was measured independently over 10,000 representative rows:
| Key strategy | Median | Paired ratio to JSON |
@ -468,8 +446,8 @@ The TUI now uses the precomputed primitive ID strategy.
## What the Benchmark Does Not Prove
The suite compares whole-array reconciliation, direct point updates, subscribed
aggregate projection, dense changes, unstable keys, and key extraction. It
does not prove that Quark beats:
aggregate projection, dense changes, unstable keys, key extraction, growing
timeline groups, and duplicate deltas. It does not prove that Quark beats:
- every Solid keyed-list primitive;
- rendering, terminal layout, or paint;
@ -500,19 +478,18 @@ These are falsifiable predictions. The current adverse suite did not find a
Solid win, including the newly added precise path write, but it preserves these
cases so future changes cannot optimize only the favorable sparse path.
## The Next Tests Must Measure Work, Not Just Time
## Deterministic Tests Measure Work, Not Just Time
The strongest next experiment is a single-binary A/B implementation driven by
one deterministic event trace. It should count:
The single-binary deterministic event trace counts:
| Deterministic transition | Slot delta | Structure delta | Observed ownership behavior |
| ------------------------------ | ---------: | --------------: | ------------------------------------------- |
| First exploration part | +0 | +1 | New group slot |
| Extend exploration group | +1 | +0 | Existing group slot retained |
| Permission repartition | +1 | +0 | Existing group slot retained |
| Insert queued user row | +0 | +1 | Group remains mounted and incomplete |
| Complete promoted-input group | +1 | +0 | Existing group slot retained |
| Duplicate text delta | +0 | +0 | Returns before aggregate materialization |
| Deterministic transition | Slot delta | Structure delta | Observed ownership behavior |
| ------------------------------ | ---------: | --------------: | -------------------------------------------- |
| First exploration part | +0 | +1 | New group slot |
| Extend exploration group | +1 | +0 | Existing group slot retained |
| Permission repartition | +1 | +0 | Existing group slot retained |
| Insert queued user row | +0 | +1 | Group remains mounted and incomplete |
| Complete promoted-input group | +1 | +0 | Existing group slot retained |
| Duplicate text delta | +0 | +0 | Returns through compiled membership index |
| Full unchanged two-row rebuild | +0 | +0 | Two equivalence suppressions, no publication |
`Keyed` exposes optional counters for slot publications, structural
@ -524,6 +501,11 @@ deltas` records the duplicate path. The direct Keyed law test records the full
unchanged rebuild. Boundary tests independently assert that value-only changes
do not invalidate structural boundary projection.
Pure timeline tests additionally cover cached queue advancement, out-of-order
promotion, duplicate message/footer idempotence, stable group ownership, and
parity between event-native operations and snapshot reduction. Unique append
uses cached IDs and never reads `state.values()`.
## Decision Rule
Continue the Quark timeline experiment if deterministic replay confirms all of

View file

@ -4,15 +4,17 @@ Status: experiment in progress
## Summary
The V2 TUI currently stores its rendered session-row list in a Solid Store.
This experiment changes only that owner: `createSessionRows` stores an ordered
array of stable row slots in Quark state. Each slot owns one `SessionRow`, and
an owner-aware adapter exposes both levels to the existing Solid renderer.
The V2 TUI previously stored its rendered session-row list in a Solid Store.
This experiment changes only that owner: `SessionTimeline` stores an ordered
array of stable row slots in a compiled Quark collection. Each slot owns one
`SessionRow`, and an owner-aware adapter exposes both levels to the existing
Solid renderer.
The event protocol, durable session data, `DataProvider`, row reduction rules,
and `SessionRowView` remain unchanged. This boundary makes the experiment easy
to compare and easy to remove. It does not yet move messages or secondary
indexes into Quark collections.
to compare and easy to remove. Message content remains in `DataProvider`;
`SessionTimeline` owns a finite imperative index of part IDs for its current
rows.
The experiment succeeds only if all three checks pass:
@ -56,10 +58,13 @@ server events
DataProvider Solid Store
|
v
createSessionRows
createSessionRows adapter
|
v
Quark State<RowSlot[]>
SessionTimeline
|
v
Layout.collection<SessionRow>
|
v
KeyedFor outer + slot adapters
@ -74,15 +79,17 @@ would make a regression or improvement impossible to attribute.
## Components and Responsibilities
| Component | Responsibility |
| ------------------------------------------- | --------------------------------------------------------------------------------------------------- |
| `packages/tui/src/context/data.tsx` | Preserve the existing server-event projection and message lookup API. |
| `packages/tui/src/routes/session/rows.ts` | Reduce loaded messages, apply incremental timeline events, and own ordered rows. |
| `packages/quark/src/reactivity.ts` | Provide synchronous state updates, subscriptions, and transaction batching. |
| `packages/quark/src/keyed.ts` | Reuse per-key slots and separate value changes from structural changes. |
| `packages/quark/src/solid.ts` | Bridge Quark readables into Solid ownership and compose stable slots with `KeyedFor`. |
| `packages/tui/src/routes/session/index.tsx` | Render stable row accessors through `KeyedFor` and the existing `SessionRowView` components. |
| `script/quark-timeline-drive.ts` | Exercise the same streamed timeline scenario against baseline and fork binaries. |
| Component | Responsibility |
| --------------------------------------------- | -------------------------------------------------------------------------------------------------- |
| `packages/tui/src/context/data.tsx` | Preserve the existing server-event projection and message lookup API. |
| `packages/tui/src/routes/session/rows.ts` | Translate DataProvider state and events into timeline operations; own the Solid adapter lifecycle. |
| `packages/tui/src/routes/session/timeline.ts` | Declare row layout, reduce snapshots, cache cursors, and implement timeline domain mutations. |
| `packages/quark/src/layout.ts` | Compile row keys/equivalence and maintain declared imperative collection indexes. |
| `packages/quark/src/reactivity.ts` | Provide synchronous state updates, subscriptions, and transaction batching. |
| `packages/quark/src/keyed.ts` | Reuse per-key slots and separate value changes from structural changes. |
| `packages/quark/src/solid.ts` | Bridge Quark readables into Solid ownership and compose stable slots with `KeyedFor`. |
| `packages/tui/src/routes/session/index.tsx` | Render stable row accessors through `KeyedFor` and the existing `SessionRowView` components. |
| `script/quark-timeline-drive.ts` | Exercise the same streamed timeline scenario against baseline and fork binaries. |
## Quark State Publishes One Synchronous Value
@ -187,10 +194,11 @@ Most live events update the current row array directly:
| Step started | Remove the previous retry footer. |
| Permission changed | Repartition exploration refs between visible and pending lists. |
`queuedStart(rows)` finds the first queued compaction or pending message. New
assistant parts and completed messages insert before that boundary. Pending
user inputs append after queued work. A running compaction inserts at the start
of the queued region.
`SessionTimeline` caches the first queued row ID and the active incomplete group
ID. Full replacement derives both once. Live operations maintain them as work
is queued or promoted. New assistant parts and completed messages insert before
the cached queue boundary; pending user inputs append after queued work; a
running compaction becomes the new boundary.
`append(rows, ref, part, index)` implements grouping:
@ -214,13 +222,14 @@ because call IDs can look like generated text or reasoning IDs.
The old Solid `produce` path created a mutable draft of the row list. The first
Quark implementation also rebuilt a complete next array, then reconciled it.
The event-native path now uses the information already present in each event:
The event-native path now calls domain operations backed by the compiled
collection:
```text
value change -> Keyed.update(row)
new row -> Keyed.insert(row, { before })
removed footer -> Keyed.remove(row.id)
full sync / revert -> Keyed.set(nextRows)
group change -> state.modify(groupID, update)
new row -> state.insert(row, { before })
removed footer -> state.remove(row.id)
full sync / revert -> state.set(nextRows)
```
Extending or completing a group creates one new group value with copied refs
@ -229,9 +238,11 @@ structure. Permission repartitioning first checks whether a group's visible or
pending membership would actually change, then allocates arrays only for those
groups.
Duplicate message, part, or footer events return before a Keyed operation.
Same-ordinal streaming deltas therefore produce no slot or structural
publication after the first part.
Message and footer duplicates use the keyed map. Part duplicates use the
compiled `parts` members index and return before aggregate projection. Unique
group appends use cached IDs and do not read `state.values()`. The append event
also supplies the exact one-member index delta, avoiding a scan of the growing
group; arbitrary replacements retain automatic index derivation.
## `Keyed` Preserves Row Identity
@ -240,24 +251,28 @@ to preserve child ownership. A changed group value must not remount its
`SessionRowView`, because that would reset the group's local expanded and hover
state. `<For>` therefore receives stable Quark slots rather than row values.
`Keyed.set(nextRows)` preserves ownership in four passes:
`Keyed.set(nextRows)` preserves ownership through its persistent key-to-slot
map:
```text
1. Index previous slots by the semantic key of their current row.
2. For each next row, find the previous slot with that key.
3. Update that slot only when the rendered row fields changed.
1. Validate the next keys.
2. Find each retained slot through the persistent map.
3. Update that slot only when generated row equivalence reports a change.
4. Reuse the previous outer slot array when membership and order are unchanged.
```
Every row receives one collision-safe length-prefixed primitive ID when it is
created. `Keyed` reads `row.id`; reconciliation performs no serialization:
created. `SessionRowLayout` declares that key and generates discriminated-union
equivalence. Group `origin` is immutable metadata; `refs`, `pending`, and
`completed` participate in equivalence. Reconciliation performs no
serialization:
| Row | ID shape |
| ----------------- | ----------------------------------------------------- |
| Message | `m{length}:{messageID}` |
| Queued compaction | `c{length}:{inputID}` |
| Part | `p{message segment}{part segment}` |
| Assistant footer | `f{length}:{messageID}` |
| Row | ID shape |
| ----------------- | ------------------------------------------------------ |
| Message | `m{length}:{messageID}` |
| Queued compaction | `c{length}:{inputID}` |
| Part | `p{message segment}{part segment}` |
| Assistant footer | `f{length}:{messageID}` |
| Group | `g{kind}{origin message segment}{origin part segment}` |
Every group records the ref that created it as immutable `origin` metadata.
@ -269,10 +284,13 @@ partitions cannot change its ID or boundary identity. Group equivalence compares
order changes, so Solid `<For>` does no structural work for a group-value
update. `values` depends on both the outer list and every slot for consumers
that require aggregate values. The TUI has no reactive `values` subscriber; it
uses the lazy aggregate only as an internal snapshot for unique mutation paths.
uses the lazy aggregate for bulk permission repartitioning and snapshot/test
consumers.
Message boundaries track structural slots and messages, then read immutable
row identity fields untracked. A changed group publishes through its existing
slot and preserves the mounted Solid owner without recalculating boundaries.
The lazy aggregate is read by permission repartitioning and snapshot/test
consumers, not by unique append paths.
Slot updates and the outer-array update run inside both a Quark transaction and
a Solid batch. The Quark transaction settles the alien-signals graph; the
@ -285,23 +303,25 @@ from one ordering and an outer slot array from another.
Let `R` be visible rows, `M` loaded messages, `P` assistant content parts, and
`C` the total refs contained by visible groups.
| Operation | Time | Allocation |
| -------------------------- | -----------------------: | ----------------------------------------------: |
| Full rebuild | `O(M + P + R)` | New reduction plus reconciled slot array |
| Unique incremental append | `O(R + C)` | One structural array or one changed group array |
| Duplicate part append | `O(1)` | None |
| Duplicate message/footer | `O(1)` | None |
| Footer removal | `O(R)` | One structural slot array |
| Permission repartition | `O(R + C)` | Arrays only for groups whose split moves |
| Quark-to-Solid publication | `O(1)` per changed level | One slot write and, when required, outer write |
| Operation | Time | Allocation |
| ---------------------------- | -----------------------: | ---------------------------------------------: |
| Full rebuild | `O(M + P + R)` | New reduction plus reconciled slot array |
| Active group / queue lookup | Expected `O(1)` | None |
| Queue boundary advancement | `O(R)` | None |
| Group extension | `O(G)` | One copied group array; `O(1)` index delta |
| Structural insertion/removal | `O(R)` | One structural slots array |
| Duplicate part append | `O(1)` | One key string |
| Duplicate message/footer | `O(1)` | One key string |
| Footer removal | `O(R)` | One structural slot array |
| Permission repartition | `O(R + C)` | Arrays only for groups whose split moves |
| Quark-to-Solid publication | `O(1)` per changed level | One slot write and, when required, outer write |
`Keyed.has` handles message and footer membership through the collection's
existing key map. A route-scoped `Set` contains every visible part ID, including
refs nested inside groups. Full rebuilds repopulate it and unique appends add to
it once. Duplicate streaming deltas therefore return before reading the lazy
aggregate or scanning rows. Unique appends still materialize the current row
snapshot and scan for the queued boundary; that lower-frequency path remains
linear deliberately rather than introducing a secondary-index layer.
existing key map. `Layout.collection` maintains every visible part ID,
including refs nested inside groups. Full replacement rebuilds the finite index;
inserts and arbitrary modifications derive membership automatically; the hot
group append applies one typed member delta. Duplicate streaming deltas return
before reading the lazy aggregate or scanning rows.
## Solid Owns the Adapter Lifecycle
@ -377,7 +397,7 @@ microbenchmark. Provider simulation, terminal polling, and process scheduling
also contribute to it. A performance decision requires repeated paired runs
and row-update or invalidation counters in addition to this behavioral check.
Three alternating smoke pairs completed successfully:
Seven baseline/fork behavior pairs completed successfully:
| Pair | Baseline | Quark fork | Fork / baseline |
| ---------------------: | --------: | ---------: | --------------: |
@ -386,7 +406,8 @@ Three alternating smoke pairs completed successfully:
| 3, fork first | 9,296 ms | 7,563 ms | 0.813x |
| 4, API assertions | 4,283 ms | 3,114 ms | 0.727x |
| 5, current `origin/v2` | 3,636 ms | 2,074 ms | 0.570x |
| 6, current Drive API | 2,233 ms | 1,413 ms | 0.633x |
| 6, current Drive API | 2,233 ms | 1,413 ms | 0.633x |
| 7, timeline extraction | 973 ms | 825 ms | 0.848x |
The range is too wide to support a speed claim. These runs establish that the
same streamed scenario renders and terminates on both implementations. They do
@ -394,22 +415,22 @@ not establish that either implementation is faster.
## Validation Status
| Check | Current result |
| ------------------------------------ | -------------------------------------------------------------------------- |
| TUI package typecheck | Pass |
| Full TUI test suite | 267 pass, 1 skip |
| Focused data and row tests | 48 pass |
| Vendored Quark tests | 13 pass |
| Shared drive scripts typecheck | Pass |
| Shared Drive behavior | Six baseline/fork pairs pass; latest pair verifies projected content |
| Publication-counter trace | Pass; exact slot/structure deltas recorded in performance model |
| Paired performance evidence | Pass for controlled workloads; end-to-end wall time remains non-conclusive |
| Check | Current result |
| ------------------------------ | -------------------------------------------------------------------------- |
| TUI package typecheck | Pass |
| Full TUI test suite | 274 pass, 1 skip |
| Focused data and row tests | 55 pass |
| Vendored Quark tests | 24 pass |
| Shared drive scripts typecheck | Pass |
| Shared Drive behavior | Seven baseline/fork pairs pass; latest pair verifies projected content |
| Publication-counter trace | Pass; exact slot/structure deltas recorded in performance model |
| Paired performance evidence | Pass for controlled workloads; end-to-end wall time remains non-conclusive |
## Collection Cache Growth Is Outside This Phase
## Reactive Query Caches Remain Outside This Phase
The standalone `effect-quark` prototype includes reactive missing-key and
secondary-index query caches without eviction. This branch vendors only Quark
state and the Solid adapter; it does not create those caches.
The timeline's imperative members index is finite: entries follow current rows
and are removed synchronously. It is not a reactive query cache and renderers
do not subscribe to it.
A later message-collection phase must not delete empty cache entries while a
live readable still references them. Safe eviction therefore needs explicit

View file

@ -1,2 +1,3 @@
export { Keyed } from "./keyed"
export { Layout } from "./layout"
export { Computed, State, Transaction, type Readable, type Writable } from "./reactivity"

View file

@ -14,11 +14,14 @@ export namespace Keyed {
readonly values: Readable<readonly A[]>
has(key: Key): boolean
get(key: Key): Readable<A> | undefined
set(values: readonly A[]): void
set(values: readonly A[]): boolean
update(value: A): boolean
modify(key: Key, f: (value: A) => A): boolean
insert(value: A, position?: Position<Key>): Readable<A>
remove(key: Key): boolean
move(key: Key, position?: Position<Key>): boolean
before(key: Key): Readable<A> | undefined
after(key: Key): Readable<A> | undefined
}
export function make<A, Key>(options: {
@ -44,7 +47,8 @@ export namespace Keyed {
const retained = new Set(keys)
if (retained.size !== keys.length) throw new Error("Keyed values must have unique keys")
Transaction.run(() => {
return Transaction.run(() => {
let changed = false
const previous = slots()
const reconciled = next.map((value, index) => {
const key = keys[index]
@ -54,12 +58,7 @@ export namespace Keyed {
byKey.set(key, created)
return created
}
if (!equivalent(slot(), value)) {
slot.set(value)
if (options.metrics) options.metrics.slotPublications++
} else if (options.metrics) {
options.metrics.equivalenceSuppressions++
}
if (publish(slot, value)) changed = true
return slot
})
byKey.forEach((_slot, key) => {
@ -68,20 +67,23 @@ export namespace Keyed {
if (!same(previous, reconciled)) {
slots.set(reconciled)
if (options.metrics) options.metrics.structuralPublications++
changed = true
}
return changed
})
},
update(value) {
const key = options.key(value)
const slot = byKey.get(key)
if (!slot) throw new Error(`Keyed value does not exist: ${String(key)}`)
if (equivalent(slot(), value)) {
if (options.metrics) options.metrics.equivalenceSuppressions++
return false
}
slot.set(value)
if (options.metrics) options.metrics.slotPublications++
return true
return publish(slot, value)
},
modify(key, f) {
const slot = byKey.get(key)
if (!slot) throw new Error(`Keyed value does not exist: ${String(key)}`)
const value = f(slot())
if (byKey.get(options.key(value)) !== slot) throw new Error("Keyed modify must preserve the value key")
return publish(slot, value)
},
insert(value, position) {
const key = options.key(value)
@ -118,6 +120,22 @@ export namespace Keyed {
if (options.metrics) options.metrics.structuralPublications++
return true
},
before(key) {
return neighbor(key, -1)
},
after(key) {
return neighbor(key, 1)
},
}
function publish(slot: Writable<A>, value: A) {
if (equivalent(slot(), value)) {
if (options.metrics) options.metrics.equivalenceSuppressions++
return false
}
slot.set(value)
if (options.metrics) options.metrics.slotPublications++
return true
}
function positionIndex(current: readonly Writable<A>[], position?: Position<Key>) {
@ -131,6 +149,13 @@ export namespace Keyed {
if (!target) throw new Error(`Keyed value does not exist: ${String(key)}`)
return current.indexOf(target)
}
function neighbor(key: Key, offset: -1 | 1) {
const slot = byKey.get(key)
if (!slot) throw new Error(`Keyed value does not exist: ${String(key)}`)
const current = slots()
return current[current.indexOf(slot) + offset]
}
}
export function metrics(): Metrics {

View file

@ -0,0 +1,660 @@
import { Keyed } from "./keyed"
import { Transaction, type Readable } from "./reactivity"
export namespace Layout {
export interface Field<A> {
readonly isKey?: true
readonly primitive?: true
readonly immutable?: true
equivalent(left: A, right: A): boolean
}
export interface KeyField<A> extends Field<A> {
readonly isKey: true
}
export interface NamedKey<Name extends PropertyKey, A> {
readonly name: Name
readonly field: Field<A>
}
export type Type<Field> = Field extends Layout.Field<infer A> ? A : never
type Fields = Readonly<Record<PropertyKey, Field<unknown>>>
type Value<StructFields> = { readonly [Key in keyof StructFields]: Type<StructFields[Key]> }
type KeyName<StructFields> = {
readonly [Key in keyof StructFields]: StructFields[Key] extends KeyField<unknown> ? Key : never
}[keyof StructFields]
type Variant<Tag extends PropertyKey, Variants> = {
readonly [Name in keyof Variants & string]: { readonly [Key in Tag]: Name } & Type<Variants[Name]>
}[keyof Variants & string]
type KeyedVariant<Name extends PropertyKey, A, Tag extends PropertyKey, Variants> = {
readonly [Key in Name]: A
} & Variant<Tag, Variants>
export interface Struct<StructFields extends Fields> extends Field<Value<StructFields>> {
readonly type: "struct"
readonly fields: StructFields
}
export interface Union<Tag extends PropertyKey, Variants extends Readonly<Record<string, Field<unknown>>>>
extends Field<Variant<Tag, Variants>> {
readonly type: "union"
readonly tag: Tag
readonly variants: Variants
}
export interface KeyedUnion<
Name extends PropertyKey,
A,
Tag extends PropertyKey,
Variants extends Readonly<Record<string, Field<unknown>>>,
> extends Field<KeyedVariant<Name, A, Tag, Variants>> {
readonly type: "keyed-union"
readonly key: NamedKey<Name, A>
readonly tag: Tag
readonly variants: Variants
}
export interface Plan<A, Key> {
readonly key: PropertyKey
readonly fields?: Fields
readonly equivalent: (left: A, right: A) => boolean
readonly keyOf: (value: A) => Key
make(initial?: readonly A[], options?: { readonly metrics?: Keyed.Metrics }): Keyed.Keyed<A, Key>
}
export interface MembersIndex<A, Member> {
readonly type: "members"
readonly extract: (value: A) => Iterable<Member>
}
export interface FirstIndex<A> {
readonly type: "first"
readonly matches: (value: A) => boolean
}
export type Index<A> = MembersIndex<A, unknown> | FirstIndex<A>
type Indexes<A> = Readonly<Record<PropertyKey, Index<A>>>
type MembersNames<Definitions> = {
readonly [Name in keyof Definitions]: Definitions[Name] extends { readonly type: "members" } ? Name : never
}[keyof Definitions]
type FirstNames<Definitions> = {
readonly [Name in keyof Definitions]: Definitions[Name] extends { readonly type: "first" } ? Name : never
}[keyof Definitions]
type Member<Definition> = Definition extends { readonly extract: (value: never) => Iterable<infer A> } ? A : never
type MemberChanges<Definitions> = {
readonly [Name in MembersNames<Definitions>]?: {
readonly add?: readonly Member<Definitions[Name]>[]
readonly remove?: readonly Member<Definitions[Name]>[]
}
}
export interface IndexBuilder<A> {
members<Member>(extract: (value: A) => Iterable<Member>): MembersIndex<A, Member>
first(matches: (value: A) => boolean): FirstIndex<A>
}
export interface Collection<A, Key, Definitions extends Indexes<A>> extends Keyed.Keyed<A, Key> {
modify(key: Key, f: (value: A) => A, changes?: { readonly members?: MemberChanges<Definitions> }): boolean
hasMember<Name extends MembersNames<Definitions>>(name: Name, member: Member<Definitions[Name]>): boolean
first<Name extends FirstNames<Definitions>>(name: Name): Readable<A> | undefined
}
export interface CollectionPlan<A, Key, Definitions extends Indexes<A>> extends Plan<A, Key> {
make(initial?: readonly A[], options?: { readonly metrics?: Keyed.Metrics }): Collection<A, Key, Definitions>
}
export const string: Field<string> = primitive()
export const number: Field<number> = primitive()
export const boolean: Field<boolean> = primitive()
export function array<A>(item: Field<A>): Field<readonly A[]> {
return make((left, right) => {
if (left.length !== right.length) return false
for (let index = 0; index < left.length; index++) {
if (!item.equivalent(left[index], right[index])) return false
}
return true
})
}
export function immutable<A>(field: Field<A>): Field<A> {
return { ...field, immutable: true, equivalent: () => true }
}
export function key<A>(field: Field<A>): KeyField<A>
export function key<const Name extends PropertyKey, A>(name: Name, field: Field<A>): NamedKey<Name, A>
export function key<A>(name: Field<A> | PropertyKey, field?: Field<A>): KeyField<A> | NamedKey<PropertyKey, A> {
if (field) return { name: name as PropertyKey, field }
return { ...(name as Field<A>), isKey: true }
}
export function struct<const StructFields extends Fields>(fields: StructFields): Struct<StructFields> {
return {
type: "struct",
fields,
equivalent: compileFields<Value<StructFields>>(fields),
}
}
export function union<
const Tag extends PropertyKey,
const Variants extends Readonly<Record<string, Field<unknown>>>,
>(options: { readonly tag: Tag; readonly variants: Variants }): Union<Tag, Variants> {
const equivalent = compileUnion<Tag, Variants>(options.tag, options.variants)
return { type: "union", ...options, equivalent }
}
export function keyedUnion<
const Name extends PropertyKey,
A,
const Tag extends PropertyKey,
const Variants extends Readonly<Record<string, Field<unknown>>>,
>(options: {
readonly key: NamedKey<Name, A>
readonly tag: Tag
readonly variants: Variants
}): KeyedUnion<Name, A, Tag, Variants> {
const equivalentVariant = compileUnion<Tag, Variants>(options.tag, options.variants)
const key = (value: KeyedVariant<Name, A, Tag, Variants>) => value[options.key.name]
const equivalent = (left: KeyedVariant<Name, A, Tag, Variants>, right: KeyedVariant<Name, A, Tag, Variants>) =>
options.key.field.equivalent(key(left), key(right)) && equivalentVariant(left, right)
return { type: "keyed-union", ...options, equivalent }
}
export function compile<const StructFields extends Fields>(
layout: Struct<StructFields>,
options?: { readonly backend?: "closure" | "generated" },
): Plan<Value<StructFields>, Value<StructFields>[KeyName<StructFields>]>
export function compile<
const Name extends PropertyKey,
A,
const Tag extends PropertyKey,
const Variants extends Readonly<Record<string, Field<unknown>>>,
>(
layout: KeyedUnion<Name, A, Tag, Variants>,
options?: { readonly backend?: "closure" | "generated" },
): Plan<KeyedVariant<Name, A, Tag, Variants>, A>
export function compile(input: unknown, options: { readonly backend?: "closure" | "generated" } = {}): unknown {
const layout = input as
| Struct<Fields>
| KeyedUnion<PropertyKey, unknown, PropertyKey, Readonly<Record<string, Field<unknown>>>>
if (layout.type === "keyed-union") {
return makePlan(
layout.key.name,
(value: unknown) => (value as Record<PropertyKey, unknown>)[layout.key.name],
(options.backend === "generated"
? generateUnion(layout.tag, layout.variants)
: compileUnion(layout.tag, layout.variants)) as (left: unknown, right: unknown) => boolean,
)
}
const keys = Reflect.ownKeys(layout.fields).filter((name) => layout.fields[name].isKey)
if (keys.length !== 1) throw new Error("Keyed layout must declare exactly one key field")
const key = keys[0]
const fields = Reflect.ownKeys(layout.fields)
.filter((name) => name !== key && !layout.fields[name].immutable)
.map((name) => ({ name, field: layout.fields[name] }))
const equivalent =
options.backend === "generated"
? generateEquivalent<unknown>(generated(fields))
: compileEquivalent<unknown>(fields)
return {
fields: layout.fields,
...makePlan(key, (value: unknown) => (value as Record<PropertyKey, unknown>)[key], equivalent),
}
}
export function collection<const StructFields extends Fields, const Definitions extends Indexes<Value<StructFields>>>(
layout: Struct<StructFields>,
define: (index: IndexBuilder<Value<StructFields>>) => Definitions,
options?: { readonly backend?: "closure" | "generated" },
): CollectionPlan<Value<StructFields>, Value<StructFields>[KeyName<StructFields>], Definitions>
export function collection<
const Name extends PropertyKey,
A,
const Tag extends PropertyKey,
const Variants extends Readonly<Record<string, Field<unknown>>>,
const Definitions extends Indexes<KeyedVariant<Name, A, Tag, Variants>>,
>(
layout: KeyedUnion<Name, A, Tag, Variants>,
define: (index: IndexBuilder<KeyedVariant<Name, A, Tag, Variants>>) => Definitions,
options?: { readonly backend?: "closure" | "generated" },
): CollectionPlan<KeyedVariant<Name, A, Tag, Variants>, A, Definitions>
export function collection(
input: unknown,
define: unknown,
options?: { readonly backend?: "closure" | "generated" },
): unknown {
const plan = compile(input as never, options) as Plan<unknown, unknown>
const definitions = (define as (index: IndexBuilder<unknown>) => Indexes<unknown>)({
members: (extract) => ({ type: "members", extract }),
first: (matches) => ({ type: "first", matches }),
})
return {
...plan,
make(initial: readonly unknown[] = [], makeOptions?: { readonly metrics?: Keyed.Metrics }) {
return makeCollection(plan, definitions, initial, makeOptions)
},
}
}
function makePlan<A, Key>(key: PropertyKey, getKey: (value: A) => Key, equivalent: (left: A, right: A) => boolean) {
return {
key,
equivalent,
keyOf: getKey,
make(initial: readonly A[] = [], options?: { readonly metrics?: Keyed.Metrics }) {
const values = Keyed.make({ key: getKey, equivalent, metrics: options?.metrics })
values.set(initial)
return values
},
}
}
function makeCollection<A, Key, Definitions extends Indexes<A>>(
plan: Plan<A, Key>,
definitions: Definitions,
initial: readonly A[],
options?: { readonly metrics?: Keyed.Metrics },
): Collection<A, Key, Definitions> {
const values = plan.make([], options)
type MemberEntry = {
readonly type: "members"
readonly name: PropertyKey
readonly extract: (value: A) => Iterable<unknown>
readonly counts: Map<unknown, number>
readonly byKey: Map<Key, { readonly source: Iterable<unknown>; readonly members: Set<unknown> }>
}
type FirstEntry = {
readonly type: "first"
readonly name: PropertyKey
readonly matches: (value: A) => boolean
readonly matching: Set<Key>
slot?: Readable<A>
}
type Entry = MemberEntry | FirstEntry
type Inspection =
| {
readonly type: "members"
readonly value: { readonly source: Iterable<unknown>; readonly members: Set<unknown> }
}
| { readonly type: "members-change"; readonly add: readonly unknown[]; readonly remove: readonly unknown[] }
| { readonly type: "first"; readonly value: boolean }
const entries: Entry[] = Reflect.ownKeys(definitions).map((name) => {
const definition = definitions[name]
if (definition.type === "members") {
return { type: "members", name, extract: definition.extract, counts: new Map(), byKey: new Map() }
}
return { type: "first", name, matches: definition.matches, matching: new Set() }
})
const byName = new Map(entries.map((entry) => [entry.name, entry]))
const emptyMembers: readonly unknown[] = []
const collection: Collection<A, Key, Definitions> = {
...values,
set(next) {
const keys = next.map(plan.keyOf)
if (new Set(keys).size !== keys.length) return values.set(next)
const prepared = new Map(
next.map((value, index) => {
const key = keys[index]
const previous = values.get(key)?.()
return [key, previous && plan.equivalent(previous, value) ? current(key) : inspect(value, key)]
}),
)
return Transaction.run(() => {
const changed = values.set(next)
if (!changed) return false
entries.forEach(clear)
values.slots().forEach((slot) => {
const key = plan.keyOf(slot())
const inspection = prepared.get(key)!
entries.forEach((entry, index) => add(entry, key, slot, inspection[index]))
})
entries.forEach((entry) => entry.type === "first" && findFirst(entry))
return true
})
},
update(value) {
const key = plan.keyOf(value)
const slot = values.get(key)
if (!slot) throw new Error(`Keyed value does not exist: ${String(key)}`)
const inspection = inspect(value, key)
return Transaction.run(() => {
const changed = values.update(value)
if (changed) entries.forEach((entry, index) => replace(entry, key, slot, inspection[index]))
return changed
})
},
modify(key, f, changes) {
const slot = values.get(key)
if (!slot) throw new Error(`Keyed value does not exist: ${String(key)}`)
let inspection: readonly Inspection[] | undefined
return Transaction.run(() => {
const changed = values.modify(key, (previous) => {
const value = f(previous)
if (values.get(plan.keyOf(value)) !== slot) throw new Error("Keyed modify must preserve the value key")
inspection = inspect(value, key, changes?.members)
return value
})
if (changed) entries.forEach((entry, index) => replace(entry, key, slot, inspection![index]))
return changed
})
},
insert(value, position) {
const key = plan.keyOf(value)
if (values.has(key)) return values.insert(value, position)
requirePosition(position)
const inspection = inspect(value, key)
return Transaction.run(() => {
const slot = values.insert(value, position)
entries.forEach((entry, index) => add(entry, key, slot, inspection[index]))
entries.forEach((entry) => entry.type === "first" && updateFirstAfterPlacement(entry, slot))
return slot
})
},
remove(key) {
const slot = values.get(key)
if (!slot) return false
return Transaction.run(() => {
const removed = values.remove(key)
entries.forEach((entry) => remove(entry, key))
entries.forEach((entry) => entry.type === "first" && entry.slot === slot && findFirst(entry))
return removed
})
},
move(key, position) {
const slot = values.get(key)
if (!slot) throw new Error(`Keyed value does not exist: ${String(key)}`)
return Transaction.run(() => {
const moved = values.move(key, position)
if (moved) entries.forEach((entry) => entry.type === "first" && updateFirstAfterPlacement(entry, slot))
return moved
})
},
hasMember(name, member) {
const entry = byName.get(normalizeName(name))
return entry?.type === "members" && entry.counts.has(member)
},
first(name) {
const entry = byName.get(normalizeName(name))
return entry?.type === "first" ? entry.slot : undefined
},
}
collection.set(initial)
return collection
function inspect(value: A, key: Key, changes?: Readonly<Record<PropertyKey, unknown>>): readonly Inspection[] {
return entries.map((entry) =>
entry.type === "members"
? (() => {
const change = changes?.[entry.name] as
| { readonly add?: readonly unknown[]; readonly remove?: readonly unknown[] }
| undefined
if (change)
return {
type: "members-change" as const,
add: change.add ?? emptyMembers,
remove: change.remove ?? emptyMembers,
}
const source = entry.extract(value)
const previous = entry.byKey.get(key)
return {
type: "members" as const,
value: { source, members: source === previous?.source ? previous.members : new Set(source) },
}
})()
: { type: "first", value: entry.matches(value) },
)
}
function current(key: Key): readonly Inspection[] {
return entries.map((entry) => {
if (entry.type === "members") return { type: "members", value: entry.byKey.get(key)! }
return { type: "first", value: entry.matching.has(key) }
})
}
function clear(entry: Entry) {
if (entry.type === "members") entry.byKey.clear()
if (entry.type === "first") entry.matching.clear()
if (entry.type === "members") entry.counts.clear()
if (entry.type === "first") entry.slot = undefined
}
function add(entry: Entry, key: Key, slot: Readable<A>, inspection: Inspection) {
if (entry.type === "members" && inspection.type === "members") {
entry.byKey.set(key, inspection.value)
inspection.value.members.forEach((member) => entry.counts.set(member, (entry.counts.get(member) ?? 0) + 1))
return
}
if (entry.type === "first" && inspection.type === "first" && inspection.value) entry.matching.add(key)
}
function remove(entry: Entry, key: Key) {
if (entry.type === "first") {
entry.matching.delete(key)
return
}
entry.byKey.get(key)?.members.forEach((member) => adjust(entry.counts, member, -1))
entry.byKey.delete(key)
}
function replace(entry: Entry, key: Key, slot: Readable<A>, inspection: Inspection) {
if (entry.type === "members" && inspection.type === "members-change") {
const current = entry.byKey.get(key)!
const members = current.members
inspection.remove.forEach((member) => {
if (!members.delete(member)) return
adjust(entry.counts, member, -1)
})
inspection.add.forEach((member) => {
if (members.has(member)) return
members.add(member)
adjust(entry.counts, member, 1)
})
entry.byKey.set(key, { source: members, members })
return
}
if (entry.type === "members" && inspection.type === "members") {
const previous = entry.byKey.get(key)!.members
if (previous === inspection.value.members) {
entry.byKey.set(key, inspection.value)
return
}
inspection.value.members.forEach((member) => !previous.has(member) && adjust(entry.counts, member, 1))
previous.forEach((member) => !inspection.value.members.has(member) && adjust(entry.counts, member, -1))
entry.byKey.set(key, inspection.value)
return
}
if (entry.type !== "first" || inspection.type !== "first") return
const previous = entry.matching.has(key)
if (inspection.value) entry.matching.add(key)
if (!inspection.value) entry.matching.delete(key)
if (entry.slot === slot && !inspection.value) findFirst(entry)
if (entry.slot !== slot && !previous && inspection.value) updateFirstAfterPlacement(entry, slot)
}
function adjust(counts: Map<unknown, number>, member: unknown, amount: 1 | -1) {
const count = (counts.get(member) ?? 0) + amount
if (count === 0) counts.delete(member)
if (count > 0) counts.set(member, count)
}
function updateFirstAfterPlacement(entry: FirstEntry, slot: Readable<A>) {
if (!entry.matching.has(plan.keyOf(slot()))) return
if (!entry.slot) {
entry.slot = slot
return
}
if (entry.slot === slot) return findFirst(entry)
const slots = values.slots()
if (slots.indexOf(slot) < slots.indexOf(entry.slot)) entry.slot = slot
}
function findFirst(entry: FirstEntry) {
entry.slot = values.slots().find((slot) => entry.matching.has(plan.keyOf(slot())))
}
function requirePosition(position?: Keyed.Position<Key>) {
if (!position || position === "end") return
const key = "before" in position ? position.before : position.after
if (!values.has(key)) throw new Error(`Keyed value does not exist: ${String(key)}`)
}
function normalizeName(name: PropertyKey) {
return typeof name === "number" ? String(name) : name
}
}
function make<A>(equivalent: (left: A, right: A) => boolean): Field<A> {
return { equivalent }
}
function primitive<A>(): Field<A> {
return { primitive: true, equivalent: Object.is }
}
function compileUnion<Tag extends PropertyKey, Variants extends Readonly<Record<string, Field<unknown>>>>(
tag: Tag,
variants: Variants,
) {
type A = Variant<Tag, Variants>
return (left: A, right: A) => {
const name = left[tag]
if (name !== right[tag] || typeof name !== "string") return false
const variant = variants[name]
return variant ? variant.equivalent(left, right) : false
}
}
function compileFields<A>(fields: Fields) {
return compileEquivalent<A>(
Reflect.ownKeys(fields)
.filter((name) => !fields[name].immutable)
.map((name) => ({ name, field: fields[name] })),
)
}
function generateEquivalent<A>(
fields: ReadonlyArray<{ readonly name: PropertyKey; readonly field: Field<unknown> }>,
) {
if (fields.some((field) => typeof field.name === "symbol")) return compileEquivalent<A>(fields)
const custom: Array<Field<unknown>["equivalent"]> = []
const comparisons = fields.map((field) => {
const name = JSON.stringify(String(field.name))
if (field.field.primitive) return `Object.is(left[${name}], right[${name}])`
const index = custom.push(field.field.equivalent) - 1
return `custom[${index}](left[${name}], right[${name}])`
})
const factory = Function("custom", `return (left, right) => ${comparisons.join(" && ") || "true"}`) as (
custom: ReadonlyArray<Field<unknown>["equivalent"]>,
) => (left: A, right: A) => boolean
return factory(custom)
}
function generateUnion<Tag extends PropertyKey, Variants extends Readonly<Record<string, Field<unknown>>>>(
tag: Tag,
variants: Variants,
) {
if (typeof tag === "symbol") return compileUnion(tag, variants)
const names = Object.keys(variants)
const custom = names.map((name) => generateField(variants[name]))
const cases = names
.map((name, index) => `case ${JSON.stringify(name)}: return custom[${index}](left, right)`)
.join(";")
const property = JSON.stringify(String(tag))
const factory = Function(
"custom",
`return (left, right) => { if (left[${property}] !== right[${property}]) return false; switch (left[${property}]) { ${cases}; default: return false } }`,
) as (custom: ReadonlyArray<Field<unknown>["equivalent"]>) => (left: unknown, right: unknown) => boolean
return factory(custom)
}
function generateField(field: Field<unknown>): Field<unknown>["equivalent"] {
if (field.immutable) return () => true
const layout = field as Field<unknown> & {
readonly type?: "struct" | "union" | "keyed-union"
readonly fields?: Fields
readonly tag?: PropertyKey
readonly variants?: Readonly<Record<string, Field<unknown>>>
readonly key?: NamedKey<PropertyKey, unknown>
}
if (layout.type === "struct") {
const fields = Reflect.ownKeys(layout.fields!)
.filter((name) => !layout.fields![name].immutable)
.map((name) => ({ name, field: layout.fields![name] }))
return generateEquivalent(generated(fields))
}
if (layout.type === "union") return generateUnion(layout.tag!, layout.variants!)
if (layout.type !== "keyed-union") return field.equivalent
const equivalent = generateUnion(layout.tag!, layout.variants!) as (left: unknown, right: unknown) => boolean
return (left, right) => {
const a = left as Record<PropertyKey, unknown>
const b = right as Record<PropertyKey, unknown>
return layout.key!.field.equivalent(a[layout.key!.name], b[layout.key!.name]) && equivalent(left, right)
}
}
function generated(fields: ReadonlyArray<{ readonly name: PropertyKey; readonly field: Field<unknown> }>) {
return fields.map((field) => ({ ...field, field: { ...field.field, equivalent: generateField(field.field) } }))
}
function compileEquivalent<A>(fields: ReadonlyArray<{ readonly name: PropertyKey; readonly field: Field<unknown> }>) {
const value = (input: A) => input as Record<PropertyKey, unknown>
if (fields.length === 0) return (_left: A, _right: A) => true
if (fields.length === 1) {
const first = fields[0]
return (left: A, right: A) => first.field.equivalent(value(left)[first.name], value(right)[first.name])
}
if (fields.length === 2) {
const first = fields[0]
const second = fields[1]
return (left: A, right: A) => {
const a = value(left)
const b = value(right)
return (
first.field.equivalent(a[first.name], b[first.name]) &&
second.field.equivalent(a[second.name], b[second.name])
)
}
}
if (fields.length === 3) {
const first = fields[0]
const second = fields[1]
const third = fields[2]
return (left: A, right: A) => {
const a = value(left)
const b = value(right)
return (
first.field.equivalent(a[first.name], b[first.name]) &&
second.field.equivalent(a[second.name], b[second.name]) &&
third.field.equivalent(a[third.name], b[third.name])
)
}
}
if (fields.length === 4) {
const first = fields[0]
const second = fields[1]
const third = fields[2]
const fourth = fields[3]
return (left: A, right: A) => {
const a = value(left)
const b = value(right)
return (
first.field.equivalent(a[first.name], b[first.name]) &&
second.field.equivalent(a[second.name], b[second.name]) &&
third.field.equivalent(a[third.name], b[third.name]) &&
fourth.field.equivalent(a[fourth.name], b[fourth.name])
)
}
}
return (left: A, right: A) => {
const a = value(left)
const b = value(right)
return fields.every((field) => field.field.equivalent(a[field.name], b[field.name]))
}
}
}

View file

@ -131,6 +131,20 @@ describe("Keyed", () => {
dispose()
})
it("modifies one existing slot while preserving its key", () => {
const keyed = Keyed.make<Item, number>({ key: (value) => value.id })
keyed.set([item(1, "one")])
const slot = keyed.slots()[0]
expect(keyed.modify(1, (value) => ({ ...value, label: "ONE" }))).toBe(true)
expect(keyed.modify(1, (value) => value)).toBe(false)
expect(keyed.slots()[0]).toBe(slot)
expect(slot()).toEqual(item(1, "ONE"))
expect(() => keyed.modify(1, (value) => ({ ...value, id: 2 }))).toThrow("Keyed modify must preserve the value key")
expect(() => keyed.modify(2, (value) => value)).toThrow("Keyed value does not exist: 2")
})
it("checks key membership without reading the aggregate", () => {
const keyed = Keyed.make<Item, number>({ key: (value) => value.id })
keyed.set([item(1, "one")])
@ -139,6 +153,8 @@ describe("Keyed", () => {
expect(keyed.has(2)).toBe(false)
expect(keyed.get(1)).toBe(keyed.slots()[0])
expect(keyed.get(2)).toBeUndefined()
expect(keyed.before(1)).toBeUndefined()
expect(keyed.after(1)).toBeUndefined()
keyed.remove(1)
expect(keyed.has(1)).toBe(false)
expect(keyed.get(1)).toBeUndefined()
@ -154,6 +170,8 @@ describe("Keyed", () => {
expect(keyed.slots()).toEqual([one, two, three])
const four = keyed.insert(item(4, "four"), { after: 3 })
expect(keyed.slots()).toEqual([one, two, three, four])
expect(keyed.before(3)).toBe(two)
expect(keyed.after(3)).toBe(four)
expect(keyed.move(3, { before: 1 })).toBe(true)
expect(keyed.slots()).toEqual([three, one, two, four])
expect(keyed.move(3, { before: 1 })).toBe(false)

View file

@ -0,0 +1,253 @@
import { describe, expect, it } from "bun:test"
import { Layout } from "../src"
const Item = Layout.struct({
id: Layout.key(Layout.number),
label: Layout.string,
})
const Job = Layout.struct({
id: Layout.key(Layout.string),
labels: Layout.array(Layout.string),
status: Layout.string,
})
const Jobs = Layout.collection(Job, ({ members, first }) => ({
labels: members((job) => job.labels),
nextRetry: first((job) => job.status === "retrying"),
}))
describe("Layout", () => {
it("compiles a keyed collection from trusted structural metadata", () => {
const plan = Layout.compile(Item)
const original = { id: 1, label: "one" }
const values = plan.make([original])
const slot = values.slots()[0]
expect(plan.key).toBe("id")
expect(plan.fields).toBe(Item.fields)
expect(values.update({ id: 1, label: "one" })).toBe(false)
expect(slot()).toBe(original)
expect(values.update({ id: 1, label: "ONE" })).toBe(true)
expect(slot()).toEqual({ id: 1, label: "ONE" })
})
it("requires exactly one key field", () => {
expect(() => Layout.compile(Layout.struct({ value: Layout.number }))).toThrow(
"Keyed layout must declare exactly one key field",
)
expect(() =>
Layout.compile(Layout.struct({ left: Layout.key(Layout.number), right: Layout.key(Layout.number) })),
).toThrow("Keyed layout must declare exactly one key field")
})
it("generates the same trusted equivalence as the closure backend", () => {
const closure = Layout.compile(Item)
const generated = Layout.compile(Item, { backend: "generated" })
const values = [
{ id: 1, label: "one" },
{ id: 1, label: "ONE" },
{ id: 2, label: "one" },
]
values.forEach((left) => {
values.forEach((right) => {
expect(generated.equivalent(left, right)).toBe(closure.equivalent(left, right))
})
})
})
it("compiles nested discriminated unions and skips immutable fields", () => {
const Ref = Layout.struct({ messageID: Layout.string, partID: Layout.string })
const Row = Layout.keyedUnion({
key: Layout.key("id", Layout.string),
tag: "type",
variants: {
message: Layout.struct({ messageID: Layout.string }),
group: Layout.union({
tag: "kind",
variants: {
reasoning: Layout.struct({
origin: Layout.immutable(Ref),
refs: Layout.array(Ref),
completed: Layout.boolean,
}),
exploration: Layout.struct({
origin: Layout.immutable(Ref),
refs: Layout.array(Ref),
pending: Layout.array(Ref),
completed: Layout.boolean,
}),
},
}),
},
})
const plan = Layout.compile(Row)
const generated = Layout.compile(Row, { backend: "generated" })
const group = {
id: "group-1",
type: "group" as const,
kind: "exploration" as const,
origin: { messageID: "assistant-1", partID: "read-1" },
refs: [{ messageID: "assistant-1", partID: "read-1" }],
pending: [] as Array<{ messageID: string; partID: string }>,
completed: false,
}
expect(plan.equivalent(group, { ...group, origin: { messageID: "ignored", partID: "ignored" } })).toBe(true)
expect(plan.equivalent(group, { ...group, completed: true })).toBe(false)
expect(plan.equivalent(group, { ...group, pending: [{ messageID: "assistant-1", partID: "read-1" }] })).toBe(false)
expect(
plan.equivalent(group, {
id: "group-1",
type: "group",
kind: "reasoning",
origin: group.origin,
refs: group.refs,
completed: false,
}),
).toBe(false)
const candidates = [
group,
{ ...group, origin: { messageID: "ignored", partID: "ignored" } },
{ ...group, completed: true },
{ ...group, pending: [{ messageID: "assistant-1", partID: "read-1" }] },
{
id: "group-1",
type: "group" as const,
kind: "reasoning" as const,
origin: group.origin,
refs: group.refs,
completed: false,
},
]
candidates.forEach((left) => {
candidates.forEach((right) => expect(generated.equivalent(left, right)).toBe(plan.equivalent(left, right)))
})
})
it("includes nested keys in structural equivalence", () => {
const Child = Layout.struct({ id: Layout.key(Layout.number), value: Layout.string })
const Parent = Layout.struct({ id: Layout.key(Layout.number), child: Child })
const parent = Layout.compile(Parent)
expect(
parent.equivalent({ id: 1, child: { id: 1, value: "same" } }, { id: 1, child: { id: 2, value: "same" } }),
).toBe(false)
})
it("composes indexed collections without domain-specific behavior", () => {
const jobs = Jobs.make([
{ id: "one", labels: ["billing", "urgent"], status: "running" },
{ id: "two", labels: ["billing"], status: "retrying" },
{ id: "three", labels: [], status: "retrying" },
])
expect(jobs.hasMember("labels", "billing")).toBe(true)
expect(jobs.hasMember("labels", "missing")).toBe(false)
expect(jobs.first("nextRetry")?.().id).toBe("two")
expect(jobs.before("two")?.().id).toBe("one")
expect(jobs.after("two")?.().id).toBe("three")
jobs.modify("one", (job) => ({ ...job, labels: [], status: "retrying" }))
expect(jobs.hasMember("labels", "urgent")).toBe(false)
expect(jobs.hasMember("labels", "billing")).toBe(true)
expect(jobs.first("nextRetry")?.().id).toBe("one")
jobs.remove("two")
expect(jobs.hasMember("labels", "billing")).toBe(false)
jobs.move("three", { before: "one" })
expect(jobs.first("nextRetry")?.().id).toBe("three")
})
it("keeps indexes synchronized across inserts, updates, and replacement", () => {
const jobs = Jobs.make([{ id: "one", labels: ["one"], status: "running" }])
jobs.insert({ id: "three", labels: ["shared"], status: "retrying" })
jobs.insert({ id: "two", labels: ["shared"], status: "retrying" }, { before: "three" })
expect(jobs.first("nextRetry")?.().id).toBe("two")
jobs.update({ id: "two", labels: [], status: "done" })
expect(jobs.first("nextRetry")?.().id).toBe("three")
expect(jobs.hasMember("labels", "shared")).toBe(true)
jobs.remove("three")
expect(jobs.first("nextRetry")).toBeUndefined()
expect(jobs.hasMember("labels", "shared")).toBe(false)
jobs.set([
{ id: "four", labels: ["replacement"], status: "retrying" },
{ id: "five", labels: [], status: "running" },
])
expect(jobs.hasMember("labels", "replacement")).toBe(true)
expect(jobs.hasMember("labels", "one")).toBe(false)
expect(jobs.first("nextRetry")?.().id).toBe("four")
})
it("applies explicit member deltas without re-extracting unchanged membership", () => {
let extractions = 0
const IndexedJobs = Layout.collection(Job, ({ members }) => ({
labels: members((job) => {
extractions++
return job.labels
}),
}))
const jobs = IndexedJobs.make([
{ id: "one", labels: ["billing"], status: "running" },
{ id: "two", labels: ["billing"], status: "running" },
])
const before = extractions
jobs.modify("one", (job) => ({ ...job, labels: ["urgent"] }), {
members: { labels: { add: ["urgent"], remove: ["billing"] } },
})
expect(extractions).toBe(before)
expect(jobs.hasMember("labels", "billing")).toBe(true)
expect(jobs.hasMember("labels", "urgent")).toBe(true)
jobs.modify("two", (job) => ({ ...job, labels: [] }), { members: { labels: { remove: ["billing"] } } })
expect(jobs.hasMember("labels", "billing")).toBe(false)
if (false) {
// @ts-expect-error Member deltas require arrays so string members are not split into characters.
jobs.modify("one", (job) => job, { members: { labels: { add: "urgent" } } })
}
})
it("does not commit mutations when an index callback throws", () => {
const ThrowingJobs = Layout.collection(Job, ({ members, first }) => ({
labels: members((job) => {
if (job.labels.includes("boom")) throw new Error("boom")
return job.labels
}),
nextRetry: first((job) => job.status === "retrying"),
}))
const jobs = ThrowingJobs.make([{ id: "one", labels: ["safe"], status: "running" }])
expect(() => jobs.update({ id: "one", labels: ["boom"], status: "retrying" })).toThrow("boom")
expect(() => jobs.set([{ id: "two", labels: ["boom"], status: "retrying" }])).toThrow("boom")
expect(jobs.values()).toEqual([{ id: "one", labels: ["safe"], status: "running" }])
expect(jobs.hasMember("labels", "safe")).toBe(true)
expect(jobs.hasMember("labels", "boom")).toBe(false)
expect(jobs.first("nextRetry")).toBeUndefined()
expect(() => jobs.insert({ id: "two", labels: ["boom"], status: "retrying" }, { before: "missing" })).toThrow(
"Keyed value does not exist: missing",
)
})
it("tracks first matches whose key is undefined", () => {
const undefinedField: Layout.Field<undefined> = { equivalent: Object.is }
const OptionalJobs = Layout.collection(
Layout.struct({ id: Layout.key(undefinedField), status: Layout.string }),
({ first }) => ({ retry: first((job) => job.status === "retrying") }),
)
const jobs = OptionalJobs.make([{ id: undefined, status: "running" }])
jobs.update({ id: undefined, status: "retrying" })
expect(jobs.first("retry")?.()).toEqual({ id: undefined, status: "retrying" })
})
})

View file

@ -0,0 +1,120 @@
import { Keyed } from "effect-quark"
import { createStore, produce } from "solid-js/store"
import { SessionTimeline, type PartRef } from "../src/routes/session/timeline"
import { createHarness, type Workload } from "../../quark/bench/harness"
type Group = {
readonly id: "group"
readonly type: "group"
readonly refs: readonly PartRef[]
}
const bench = createHarness({ warmup: 500 })
function timelineAppend(): Workload {
const timeline = SessionTimeline.make()
let ordinal = 0
return {
run() {
timeline.appendPart({ messageID: "assistant", partID: `reasoning:${ordinal++}` }, { type: "reasoning" })
},
consume: () => {
const row = timeline.values()[0]
return row?.type === "group" ? row.refs.length : 0
},
}
}
function keyedAppend(): Workload {
const seen = new Set<string>()
const rows = Keyed.make<Group, Group["id"]>({
key: (row) => row.id,
equivalent: (left, right) =>
left.refs.length === right.refs.length &&
left.refs.every(
(ref, index) => ref.messageID === right.refs[index].messageID && ref.partID === right.refs[index].partID,
),
})
rows.set([{ id: "group", type: "group", refs: [] }])
let ordinal = 0
return {
run() {
const ref = { messageID: "assistant", partID: `reasoning:${ordinal++}` }
if (seen.has(ref.partID)) return
rows.modify("group", (group) => ({ ...group, refs: [...group.refs, ref] }))
seen.add(ref.partID)
},
consume: () => rows.get("group")!().refs.length,
}
}
function solidAppend(): Workload {
const [rows, setRows] = createStore<Array<{ type: "group"; refs: PartRef[] }>>([{ type: "group", refs: [] }])
let ordinal = 0
return {
run() {
const ref = { messageID: "assistant", partID: `reasoning:${ordinal++}` }
setRows(
produce((draft) => {
if (draft[0].refs.some((item) => item.messageID === ref.messageID && item.partID === ref.partID)) return
draft[0].refs.push(ref)
}),
)
},
consume: () => rows[0].refs.length,
}
}
function timelineDuplicate(size: number): Workload {
const timeline = SessionTimeline.make()
Array.from({ length: size }, (_, ordinal) =>
timeline.appendPart({ messageID: "assistant", partID: `reasoning:${ordinal}` }, { type: "reasoning" }),
)
const duplicate = { messageID: "assistant", partID: `reasoning:${size - 1}` }
return {
run: () => timeline.appendPart(duplicate, { type: "reasoning" }),
consume: () => timeline.values().length,
}
}
function solidDuplicate(size: number): Workload {
const refs = Array.from(
{ length: size },
(_, ordinal): PartRef => ({ messageID: "assistant", partID: `reasoning:${ordinal}` }),
)
const [rows, setRows] = createStore([{ type: "group" as const, refs }])
const duplicate = refs.at(-1)!
return {
run() {
setRows(
produce((draft) => {
if (draft[0].refs.some((item) => item.messageID === duplicate.messageID && item.partID === duplicate.partID))
return
draft[0].refs.push(duplicate)
}),
)
},
consume: () => rows.length,
}
}
console.log(`Session timeline benchmark (${bench.samples} samples)\n`)
const append = bench.compare(2_000, [
{ name: "SessionTimeline grouped append", make: timelineAppend },
{ name: "Handwritten Keyed + Set append", make: keyedAppend },
{ name: "Solid Store produce append", make: solidAppend },
])
const duplicate = bench.compare(10_000, [
{ name: "SessionTimeline duplicate 1000", make: () => timelineDuplicate(1_000) },
{ name: "Solid Store duplicate 1000", make: () => solidDuplicate(1_000) },
])
console.log("\nRatios (lower is faster)")
console.log(`Timeline / handwritten append: ${append.ratio(0, 1).toFixed(3)}x`)
console.log(`Timeline / Solid append: ${append.ratio(0, 2).toFixed(3)}x`)
console.log(`Timeline / Solid duplicate: ${duplicate.ratio(0, 1).toFixed(3)}x`)
console.log(`METRIC timeline_handwritten_append_ratio=${append.ratio(0, 1).toFixed(6)}`)
console.log(`METRIC timeline_solid_append_ratio=${append.ratio(0, 2).toFixed(6)}`)
console.log(`METRIC timeline_solid_duplicate_ratio=${duplicate.ratio(0, 1).toFixed(6)}`)
bench.finish()

View file

@ -6,6 +6,7 @@
"type": "module",
"license": "MIT",
"scripts": {
"bench:timeline": "bun --conditions=browser bench/session-timeline.ts",
"test": "bun test --timeout 30000 --only-failures",
"typecheck": "tsgo --noEmit"
},

View file

@ -81,7 +81,8 @@ import { PluginSlot } from "../../plugin/context"
import { Keymap, type KeymapCommand } from "../../context/keymap"
import { usePathFormatter } from "../../context/path-format"
import { useLocation } from "../../context/location"
import { createSessionRows, messageBoundaryIDs, resolvePart, type PartRef, type SessionRow } from "./rows"
import { createSessionRows } from "./rows"
import { messageBoundaryIDs, resolvePart, type PartRef, type SessionRow } from "./timeline"
import { switchLabel } from "../../util/model"
import { findMessageBoundary, messageNavigationSlack } from "./message-navigation"
import { stringWidth } from "../../util/string-width"
@ -214,7 +215,12 @@ export function Session() {
})
const editor = useEditorContext()
const rows = createSessionRows(() => route.sessionID)
const boundaries = createMemo(() => messageBoundaryIDs(rows.slots().map((slot) => slot()), messages()))
const boundaries = createMemo(() =>
messageBoundaryIDs(
rows.slots().map((slot) => slot()),
messages(),
),
)
const [navigationMessage, setNavigationMessage] = createSignal<string>()
const [navigationSlack, setNavigationSlack] = createSignal(0)
@ -1165,7 +1171,7 @@ function SessionPartView(props: { partRef: PartRef; message: (messageID: string)
}
function SessionReasoningGroupView(props: {
refs: PartRef[]
refs: readonly PartRef[]
completed: boolean
message: (messageID: string) => SessionMessageInfo | undefined
}) {
@ -1286,8 +1292,8 @@ function SessionReasoningGroupView(props: {
}
function SessionGroupView(props: {
refs: PartRef[]
pending: PartRef[]
refs: readonly PartRef[]
pending: readonly PartRef[]
completed: boolean
message: (messageID: string) => SessionMessageInfo | undefined
}) {
@ -1296,7 +1302,7 @@ function SessionGroupView(props: {
const renderer = useRenderer()
const [expanded, setExpanded] = createSignal(false)
const [hover, setHover] = createSignal(false)
const parts = (refs: PartRef[]) =>
const parts = (refs: readonly PartRef[]) =>
refs.flatMap((ref) => {
const message = props.message(ref.messageID)
if (message?.type !== "assistant") return []

View file

@ -1,79 +1,48 @@
import type { SessionMessageAssistant, SessionMessageInfo } from "@opencode-ai/client"
import { Keyed, Transaction } from "effect-quark"
import { Keyed } from "effect-quark"
import { useValue } from "effect-quark/solid"
import { batch, createEffect, on, onCleanup, type Accessor } from "solid-js"
import { useData } from "../../context/data"
import { useClient } from "../../context/client"
export type PartRef = {
readonly messageID: string
readonly partID: string
}
export 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 }
)
import {
SessionTimeline,
compactionQueuedRow,
isTerminalFinish,
reduceSessionRows,
type AppendPart,
type PartRef,
type SessionRow,
} from "./timeline"
export function createSessionRows(sessionID: Accessor<string>, options?: { readonly metrics?: Keyed.Metrics }) {
const data = useData()
const client = useClient()
const reportMetrics = process.env.OPENCODE_QUARK_METRICS === "1"
const metrics = options?.metrics ?? (reportMetrics ? Keyed.metrics() : undefined)
const state = Keyed.make({ key: rowKey, equivalent: sameRow, metrics })
const seenParts = new Set<string>()
const state = SessionTimeline.make({ metrics })
const rows = {
slots: useValue(state.slots),
values: state.values,
}
const revertBoundary = () => data.session.get(sessionID())?.revert?.messageID
const isPending = (messageID: string) => {
const message = data.session.message.get(sessionID(), messageID)
if (message?.type === "user" || message?.type === "synthetic") return data.session.input.has(sessionID(), messageID)
return message?.type === "compaction" && message.status === "running"
}
const setRows = (value: SessionRow[]) => {
batch(() => {
state.set(value)
seenParts.clear()
value.forEach((row) => {
if (row.type === "part") {
seenParts.add(row.id)
return
}
if (row.type !== "group") return
row.refs.forEach((ref) => seenParts.add(partRowID(ref)))
if (row.kind === "exploration") row.pending.forEach((ref) => seenParts.add(partRowID(ref)))
})
state.replace(value, isPending, pendingPermissions())
})
}
const mutate = (f: () => void) => batch(() => Transaction.run(f))
const insert = (current: readonly SessionRow[], index: number, row: SessionRow) =>
state.insert(row, index === current.length ? "end" : { before: current[index].id })
const complete = (current: readonly SessionRow[], index: number) => {
const previous = current[index - 1]
if (previous?.type === "group" && !previous.completed) state.update({ ...previous, completed: true })
}
const mutate = (f: () => void) => batch(f)
function reduce() {
const messages = data.session.message.list(sessionID())
const inputs = new Set(data.session.input.list(sessionID()))
const boundary = revertBoundary()
const rows = reduceSessionRows(boundary ? messages.filter((message) => message.id < boundary) : messages, inputs)
partitionPending(rows, pendingPermissions())
const position = rows.findIndex((row) => row.type === "message" && inputs.has(row.messageID))
rows.splice(
position === -1 ? rows.length : position,
@ -96,20 +65,7 @@ export function createSessionRows(sessionID: Accessor<string>, options?: { reado
createEffect(() => {
const pending = pendingPermissions()
mutate(() => {
state.values().forEach((row) => {
if (row.type !== "group" || row.kind !== "exploration") return
const changed =
row.refs.some((ref) => pending.has(ref.partID)) || row.pending.some((ref) => !pending.has(ref.partID))
if (!changed) return
const refs = [...row.refs, ...row.pending]
state.update({
...row,
refs: refs.filter((ref) => !pending.has(ref.partID)),
pending: refs.filter((ref) => pending.has(ref.partID)),
})
})
})
mutate(() => state.repartition(pending))
})
createEffect(
@ -162,6 +118,8 @@ export function createSessionRows(sessionID: Accessor<string>, options?: { reado
{
id: message.id,
created: message.time.created,
input: false,
status: message.status,
},
]
: [],
@ -172,64 +130,16 @@ export function createSessionRows(sessionID: Accessor<string>, options?: { reado
const appendMessage = (messageID: string) =>
mutate(() => {
if (state.has(messageRowID(messageID))) return
const current = state.values()
const pending = isPending(messageID)
const message = data.session.message.get(sessionID(), messageID)
const index =
message?.type === "compaction" && pending
? queuedStart(current)
: pending
? current.length
: queuedStart(current)
if (!pending) complete(current, index)
insert(current, index, messageRow(messageID))
state.appendMessage(messageID, { pending, compaction: message?.type === "compaction" })
})
const appendPart = (ref: PartRef, part: AppendPart) =>
mutate(() => {
const id = partRowID(ref)
if (seenParts.has(id)) return
const current = state.values()
const index = queuedStart(current)
const previous = current[index - 1]
const decision = appendDecision(previous, ref, part)
if (decision.type === "join") {
state.update({ ...decision.group, refs: [...decision.group.refs, ref] })
seenParts.add(id)
return
}
complete(current, index)
insert(current, index, decision.row)
seenParts.add(id)
})
const appendPart = (ref: PartRef, part: AppendPart) => mutate(() => state.appendPart(ref, part))
const appendFooter = (messageID: string) =>
mutate(() => {
if (state.has(footerRowID(messageID))) return
const current = state.values()
const index = queuedStart(current)
complete(current, index)
insert(current, index, footerRow(messageID))
})
const appendFooter = (messageID: string) => mutate(() => state.appendFooter(messageID))
const removeFooter = (messageID: string) =>
mutate(() => {
state.remove(footerRowID(messageID))
})
const isPending = (messageID: string) => {
const message = data.session.message.get(sessionID(), messageID)
if (message?.type === "user" || message?.type === "synthetic") return data.session.input.has(sessionID(), messageID)
return message?.type === "compaction" && message.status === "running"
}
const queuedStart = (rows: readonly SessionRow[]) => {
const index = rows.findIndex(
(row) => row.type === "compaction-queued" || (row.type === "message" && isPending(row.messageID)),
)
return index === -1 ? rows.length : index
}
const removeFooter = (messageID: string) => mutate(() => state.removeFooter(messageID))
const message = (event: { id: string; data: { sessionID: string } }) => {
if (event.data.sessionID === sessionID()) appendMessage(event.id.replace(/^evt_/, "msg_"))
@ -296,7 +206,7 @@ export function createSessionRows(sessionID: Accessor<string>, options?: { reado
if (event.data.sessionID === sessionID()) removeFooter(event.data.assistantMessageID)
}),
data.on("session.step.ended", (event) => {
if (event.data.sessionID !== sessionID() || ["tool-calls", "unknown"].includes(event.data.finish)) return
if (event.data.sessionID !== sessionID() || !isTerminalFinish(event.data.finish)) return
appendFooter(event.data.assistantMessageID)
}),
data.on("session.step.failed", (event) => {
@ -310,182 +220,3 @@ export function createSessionRows(sessionID: Accessor<string>, options?: { reado
return rows
}
export function reduceSessionRows(messages: SessionMessageInfo[], inputs = new Set<string>()) {
const isInput = (message: SessionMessageInfo) => inputs.has(message.id)
const pendingCompactions = messages.filter((message) => message.type === "compaction" && message.status === "running")
const pending = new Set([...pendingCompactions.map((message) => message.id), ...inputs])
return [
...messages.filter((message) => !pending.has(message.id)),
...pendingCompactions,
...messages.filter(isInput),
].reduce<SessionRow[]>((rows, message) => {
if (message.type !== "assistant") {
if (message.type === "synthetic" && !message.description?.trim()) return rows
if (!pending.has(message.id)) completePrevious(rows)
rows.push(messageRow(message.id))
return rows
}
const ordinals = { text: 0, reasoning: 0 }
message.content.forEach((part) => {
const partID = part.type === "tool" ? part.id : `${part.type}:${ordinals[part.type]++}`
if ((part.type === "text" || part.type === "reasoning") && !part.text.trim()) return
append(rows, { messageID: message.id, partID }, part)
})
if ((message.finish && !["tool-calls", "unknown"].includes(message.finish)) || message.error || message.retry) {
completePrevious(rows)
rows.push(footerRow(message.id))
}
return rows
}, [])
}
type BoundaryRow =
| Pick<Extract<SessionRow, { type: "message" }>, "type" | "messageID">
| Pick<Extract<SessionRow, { type: "compaction-queued" }>, "type">
| Pick<Extract<SessionRow, { type: "part" }>, "type" | "ref">
| Pick<Extract<SessionRow, { type: "group" }>, "type" | "origin">
| Pick<Extract<SessionRow, { type: "assistant-footer" }>, "type" | "messageID">
export function messageBoundaryIDs(rows: readonly BoundaryRow[], messages: SessionMessageInfo[]) {
const byID = new Map(messages.map((message) => [message.id, message]))
const seen = new Set<string>()
return rows.map((row) => {
const id = rowBoundaryMessageID(row, byID)
if (!id || seen.has(id)) return undefined
seen.add(id)
return id
})
}
function rowBoundaryMessageID(row: BoundaryRow, messages: Map<string, SessionMessageInfo>) {
if (row.type === "message") {
const message = messages.get(row.messageID)
if (message?.type === "user" && message.text.trim()) return message.id
return undefined
}
const messageID =
row.type === "part"
? row.ref.messageID
: row.type === "group"
? row.origin.messageID
: row.type === "assistant-footer"
? row.messageID
: undefined
if (!messageID) return undefined
const message = messages.get(messageID)
if (message?.type === "assistant") return message.id
}
export function resolvePart(message: SessionMessageAssistant, partID: string) {
const tool = message.content.find((part) => part.type === "tool" && part.id === partID)
if (tool) return tool
const match = /^(text|reasoning):(\d+)$/.exec(partID)
if (!match) return
const ordinal = Number(match[2])
return message.content.filter((part) => part.type === match[1])[ordinal]
}
type AppendPart = { type: "text" } | { type: "reasoning" } | { type: "tool"; name: string }
function append(rows: SessionRow[], ref: PartRef, part: AppendPart, index = rows.length) {
const previous = rows[index - 1]
const decision = appendDecision(previous, ref, part)
if (decision.type === "join") {
decision.group.refs.push(ref)
return
}
completePrevious(rows, index)
rows.splice(index, 0, decision.row)
}
function appendDecision(previous: SessionRow | undefined, ref: PartRef, part: AppendPart) {
const kind = groupKind(part)
if (kind && previous?.type === "group" && previous.kind === kind) return { type: "join" as const, group: previous }
return { type: "insert" as const, row: kind ? groupRow(kind, ref) : partRow(ref) }
}
function groupKind(part: AppendPart) {
if (part.type === "reasoning") return "reasoning" as const
if (part.type === "tool" && exploration(part.name)) return "exploration" as const
}
function completePrevious(rows: SessionRow[], index = rows.length) {
const previous = rows[index - 1]
if (previous?.type === "group") previous.completed = true
}
function partitionPending(rows: SessionRow[], pending: Set<string>) {
rows.forEach((row) => {
if (row.type !== "group" || row.kind !== "exploration") return
const refs = [...row.refs, ...row.pending]
row.refs = refs.filter((ref) => !pending.has(ref.partID))
row.pending = refs.filter((ref) => pending.has(ref.partID))
})
}
function exploration(name: string) {
return ["read", "glob", "grep"].includes(name.toLowerCase())
}
function messageRow(messageID: string): SessionRow {
return { id: messageRowID(messageID), type: "message", messageID }
}
function messageRowID(messageID: string) {
return `m${segment(messageID)}`
}
function compactionQueuedRow(inputID: string): SessionRow {
return { id: `c${segment(inputID)}`, type: "compaction-queued", inputID }
}
function partRow(ref: PartRef): SessionRow {
return { id: partRowID(ref), type: "part", ref }
}
function partRowID(ref: PartRef) {
return `p${segment(ref.messageID)}${segment(ref.partID)}`
}
function groupRow(kind: "reasoning" | "exploration", ref: PartRef): SessionRow {
const id = `g${kind === "reasoning" ? "r" : "e"}${segment(ref.messageID)}${segment(ref.partID)}`
if (kind === "reasoning") return { id, type: "group", kind, origin: ref, refs: [ref], completed: false }
return { id, type: "group", kind, origin: ref, refs: [ref], pending: [], completed: false }
}
function footerRow(messageID: string): SessionRow {
return { id: footerRowID(messageID), type: "assistant-footer", messageID }
}
function footerRowID(messageID: string) {
return `f${segment(messageID)}`
}
function segment(value: string) {
return `${value.length}:${value}`
}
function rowKey(row: SessionRow) {
return row.id
}
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 || left.completed !== right.completed || !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
}

View file

@ -0,0 +1,328 @@
import type { SessionMessageAssistant, SessionMessageInfo } from "@opencode-ai/client"
import { Keyed, Layout, Transaction } from "effect-quark"
const PartRefLayout = Layout.struct({
messageID: Layout.string,
partID: Layout.string,
})
const SessionRowLayout = Layout.keyedUnion({
key: Layout.key("id", Layout.string),
tag: "type",
variants: {
message: Layout.struct({ messageID: Layout.string }),
"compaction-queued": Layout.struct({ inputID: Layout.string }),
part: Layout.struct({ ref: PartRefLayout }),
group: Layout.union({
tag: "kind",
variants: {
reasoning: Layout.struct({
origin: Layout.immutable(PartRefLayout),
refs: Layout.array(PartRefLayout),
completed: Layout.boolean,
}),
exploration: Layout.struct({
origin: Layout.immutable(PartRefLayout),
refs: Layout.array(PartRefLayout),
pending: Layout.array(PartRefLayout),
completed: Layout.boolean,
}),
},
}),
"assistant-footer": Layout.struct({ messageID: Layout.string }),
},
})
export type PartRef = Layout.Type<typeof PartRefLayout>
export type SessionRow = Layout.Type<typeof SessionRowLayout>
export type AppendPart = { type: "text" } | { type: "reasoning" } | { type: "tool"; name: string }
const SessionRows = Layout.collection(
SessionRowLayout,
({ members }) => ({
parts: members((row) => {
if (row.type === "part") return [row.id]
if (row.type !== "group") return []
if (row.kind === "reasoning") return row.refs.map(partRowID)
return [...row.refs, ...row.pending].map(partRowID)
}),
}),
{ backend: "generated" },
)
export namespace SessionTimeline {
export function make(options?: { readonly metrics?: Keyed.Metrics }) {
const state = SessionRows.make([], options)
let activeGroupID: string | undefined
let queuedBoundaryID: string | undefined
const queuedRowIDs = new Set<string>()
const insert = (row: SessionRow) => state.insert(row, queuedBoundaryID ? { before: queuedBoundaryID } : "end")
const complete = () => {
if (!activeGroupID) return
state.modify(activeGroupID, (row) => (row.type === "group" ? { ...row, completed: true } : row))
activeGroupID = undefined
}
const nextQueued = (key: string) => {
const row = state.after(key)?.()
return row && queuedRowIDs.has(row.id) ? row.id : undefined
}
const replace = (
rows: readonly SessionRow[],
isQueued: (messageID: string) => boolean,
pending: ReadonlySet<string>,
) =>
Transaction.run(() => {
queuedBoundaryID = undefined
activeGroupID = undefined
queuedRowIDs.clear()
state.set(
rows.map((row) => {
const next = partition(row, pending)
const queued = next.type === "compaction-queued" || (next.type === "message" && isQueued(next.messageID))
if (queued) {
queuedRowIDs.add(next.id)
queuedBoundaryID ??= next.id
return next
}
if (queuedBoundaryID) return next
activeGroupID = next.type === "group" && !next.completed ? next.id : undefined
return next
}),
)
})
const appendMessage = (messageID: string, status: { readonly pending: boolean; readonly compaction: boolean }) => {
const id = messageRowID(messageID)
const exists = state.has(id)
if (exists && (status.pending || !queuedRowIDs.has(id))) return
Transaction.run(() => {
const row = messageRow(messageID)
if (status.pending) {
queuedRowIDs.add(row.id)
if (!status.compaction) {
state.insert(row, "end")
queuedBoundaryID ??= row.id
return
}
insert(row)
queuedBoundaryID = row.id
return
}
if (!exists) {
complete()
insert(row)
return
}
queuedRowIDs.delete(row.id)
complete()
if (queuedBoundaryID === row.id) {
queuedBoundaryID = nextQueued(row.id)
return
}
if (queuedBoundaryID) state.move(row.id, { before: queuedBoundaryID })
})
}
const appendPart = (ref: PartRef, part: AppendPart) => {
const id = partRowID(ref)
if (state.hasMember("parts", id)) return
Transaction.run(() => {
const kind = groupKind(part)
const active = activeGroupID ? state.get(activeGroupID)?.() : undefined
if (kind && active?.type === "group" && active.kind === kind) {
state.modify(active.id, (row) => (row.type === "group" ? { ...row, refs: [...row.refs, ref] } : row), {
members: { parts: { add: [id] } },
})
return
}
complete()
const row = kind ? groupRow(kind, ref) : partRow(ref)
insert(row)
activeGroupID = row.type === "group" ? row.id : undefined
})
}
const appendFooter = (messageID: string) => {
const id = footerRowID(messageID)
if (state.has(id)) return
Transaction.run(() => {
const row = footerRow(messageID)
complete()
insert(row)
})
}
const removeFooter = (messageID: string) => state.remove(footerRowID(messageID))
const repartition = (pending: ReadonlySet<string>) =>
Transaction.run(() => {
state.values().forEach((row) => {
if (row.type !== "group" || row.kind !== "exploration") return
const next = partition(row, pending)
if (next !== row) state.modify(row.id, () => next)
})
})
return {
slots: state.slots,
values: state.values,
replace,
appendMessage,
appendPart,
appendFooter,
removeFooter,
repartition,
}
}
}
export function reduceSessionRows(messages: SessionMessageInfo[], inputs = new Set<string>()) {
const isInput = (message: SessionMessageInfo) => inputs.has(message.id)
const pendingCompactions = messages.filter((message) => message.type === "compaction" && message.status === "running")
const pending = new Set([...pendingCompactions.map((message) => message.id), ...inputs])
return [
...messages.filter((message) => !pending.has(message.id)),
...pendingCompactions,
...messages.filter(isInput),
].reduce<SessionRow[]>((rows, message) => {
if (message.type !== "assistant") {
if (message.type === "synthetic" && !message.description?.trim()) return rows
if (!pending.has(message.id)) completePrevious(rows)
rows.push(messageRow(message.id))
return rows
}
const ordinals = { text: 0, reasoning: 0 }
message.content.forEach((part) => {
const partID = part.type === "tool" ? part.id : `${part.type}:${ordinals[part.type]++}`
if ((part.type === "text" || part.type === "reasoning") && !part.text.trim()) return
append(rows, { messageID: message.id, partID }, part)
})
if (isTerminalFinish(message.finish) || message.error || message.retry) {
completePrevious(rows)
rows.push(footerRow(message.id))
}
return rows
}, [])
}
export function messageBoundaryIDs(rows: readonly SessionRow[], messages: SessionMessageInfo[]) {
const byID = new Map(messages.map((message) => [message.id, message]))
const seen = new Set<string>()
return rows.map((row) => {
const id = rowBoundaryMessageID(row, byID)
if (!id || seen.has(id)) return undefined
seen.add(id)
return id
})
}
function rowBoundaryMessageID(row: SessionRow, messages: Map<string, SessionMessageInfo>) {
if (row.type === "message") {
const message = messages.get(row.messageID)
if (message?.type === "user" && message.text.trim()) return message.id
return undefined
}
const messageID =
row.type === "part"
? row.ref.messageID
: row.type === "group"
? row.origin.messageID
: row.type === "assistant-footer"
? row.messageID
: undefined
if (!messageID) return undefined
const message = messages.get(messageID)
if (message?.type === "assistant") return message.id
}
export function resolvePart(message: SessionMessageAssistant, partID: string) {
const tool = message.content.find((part) => part.type === "tool" && part.id === partID)
if (tool) return tool
const match = /^(text|reasoning):(\d+)$/.exec(partID)
if (!match) return
const ordinal = Number(match[2])
return message.content.filter((part) => part.type === match[1])[ordinal]
}
export function isTerminalFinish(finish: string | undefined) {
return !!finish && !["tool-calls", "unknown"].includes(finish)
}
function append(rows: SessionRow[], ref: PartRef, part: AppendPart, index = rows.length) {
const previous = rows[index - 1]
const kind = groupKind(part)
if (kind && previous?.type === "group" && previous.kind === kind) {
rows[index - 1] = { ...previous, refs: [...previous.refs, ref] }
return
}
completePrevious(rows, index)
rows.splice(index, 0, kind ? groupRow(kind, ref) : partRow(ref))
}
function groupKind(part: AppendPart) {
if (part.type === "reasoning") return "reasoning" as const
if (part.type === "tool" && exploration(part.name)) return "exploration" as const
}
function completePrevious(rows: SessionRow[], index = rows.length) {
const previous = rows[index - 1]
if (previous?.type === "group") rows[index - 1] = { ...previous, completed: true }
}
function partition(row: SessionRow, pending: ReadonlySet<string>): SessionRow {
if (row.type !== "group" || row.kind !== "exploration") return row
const changed = row.refs.some((ref) => pending.has(ref.partID)) || row.pending.some((ref) => !pending.has(ref.partID))
if (!changed) return row
const refs = [...row.refs, ...row.pending]
return {
...row,
refs: refs.filter((ref) => !pending.has(ref.partID)),
pending: refs.filter((ref) => pending.has(ref.partID)),
}
}
function exploration(name: string) {
return ["read", "glob", "grep"].includes(name.toLowerCase())
}
function messageRow(messageID: string): SessionRow {
return { id: messageRowID(messageID), type: "message", messageID }
}
function messageRowID(messageID: string) {
return `m${segment(messageID)}`
}
export function compactionQueuedRow(inputID: string): SessionRow {
return { id: `c${segment(inputID)}`, type: "compaction-queued", inputID }
}
function partRow(ref: PartRef): SessionRow {
return { id: partRowID(ref), type: "part", ref }
}
function partRowID(ref: PartRef) {
return `p${segment(ref.messageID)}${segment(ref.partID)}`
}
function groupRow(kind: "reasoning" | "exploration", ref: PartRef): SessionRow {
const id = `g${kind === "reasoning" ? "r" : "e"}${segment(ref.messageID)}${segment(ref.partID)}`
if (kind === "reasoning") return { id, type: "group", kind, origin: ref, refs: [ref], completed: false }
return { id, type: "group", kind, origin: ref, refs: [ref], pending: [], completed: false }
}
function footerRow(messageID: string): SessionRow {
return { id: footerRowID(messageID), type: "assistant-footer", messageID }
}
function footerRowID(messageID: string) {
return `f${segment(messageID)}`
}
function segment(value: string) {
return `${value.length}:${value}`
}

View file

@ -8,7 +8,8 @@ import { createEffect, onMount, type ParentProps } from "solid-js"
import { ClientProvider, useClient } from "../../../src/context/client"
import { DataProvider as DataProviderBase, useData } from "../../../src/context/data"
import { LocationProvider, useLocation } from "../../../src/context/location"
import { createSessionRows, type SessionRow } from "../../../src/routes/session/rows"
import { createSessionRows } from "../../../src/routes/session/rows"
import type { SessionRow } from "../../../src/routes/session/timeline"
import { createApi, createEventStream, createFetch, directory, json } from "../../fixture/tui-client"
import { TestTuiContexts } from "../../fixture/tui-environment"

View file

@ -1,6 +1,11 @@
import { expect, test } from "bun:test"
import type { SessionMessageAssistant, SessionMessageInfo } from "@opencode-ai/client"
import { messageBoundaryIDs, reduceSessionRows, type SessionRow } from "../../../src/routes/session/rows"
import {
SessionTimeline,
messageBoundaryIDs,
reduceSessionRows,
type SessionRow,
} from "../../../src/routes/session/timeline"
const withoutIDs = (rows: ReturnType<typeof reduceSessionRows>) =>
rows.map(({ id: _id, ...row }) => {
@ -317,6 +322,162 @@ test("places a running compaction barrier before every queued user message", ()
])
})
test("matches snapshot reduction through direct timeline operations", () => {
const timeline = SessionTimeline.make()
const response = assistant("assistant-1", [
{ type: "reasoning", text: "First" },
{ type: "reasoning", text: "Second" },
{ type: "tool", id: "read-1", name: "read", state: pending(), time: { created: 2 } },
{ type: "tool", id: "grep-1", name: "grep", state: pending(), time: { created: 3 } },
{ type: "text", text: "Done" },
])
response.finish = "stop"
const messages: SessionMessageInfo[] = [
{ type: "user", id: "user-1", text: "Explore", time: { created: 0 } },
response,
{ type: "user", id: "user-queued", text: "Continue", time: { created: 4 } },
]
timeline.appendMessage("user-1", { pending: false, compaction: false })
timeline.appendPart({ messageID: "assistant-1", partID: "reasoning:0" }, { type: "reasoning" })
timeline.appendPart({ messageID: "assistant-1", partID: "reasoning:1" }, { type: "reasoning" })
timeline.appendPart({ messageID: "assistant-1", partID: "read-1" }, { type: "tool", name: "read" })
timeline.appendPart({ messageID: "assistant-1", partID: "grep-1" }, { type: "tool", name: "grep" })
timeline.appendPart({ messageID: "assistant-1", partID: "text:0" }, { type: "text" })
timeline.appendFooter("assistant-1")
timeline.appendMessage("user-queued", { pending: true, compaction: false })
expect(timeline.values()).toEqual(reduceSessionRows(messages, new Set(["user-queued"])))
})
test("ignores a duplicate part through the parts membership index", () => {
const timeline = SessionTimeline.make()
const ref = { messageID: "assistant-1", partID: "read-1" }
timeline.appendPart(ref, { type: "tool", name: "read" })
const values = timeline.values()
const slots = timeline.slots()
timeline.appendPart(ref, { type: "text" })
expect(timeline.values()).toBe(values)
expect(timeline.slots()).toBe(slots)
})
test("inserts output before the earliest queued compaction and prompt", () => {
const timeline = SessionTimeline.make()
timeline.appendPart({ messageID: "assistant-1", partID: "read-1" }, { type: "tool", name: "read" })
timeline.appendMessage("user-1", { pending: true, compaction: false })
timeline.appendMessage("user-2", { pending: true, compaction: false })
timeline.appendMessage("compaction-1", { pending: true, compaction: true })
timeline.appendPart({ messageID: "assistant-1", partID: "text:0" }, { type: "text" })
expect(withoutIDs([...timeline.values()])).toEqual([
{
type: "group",
kind: "exploration",
pending: [],
completed: true,
refs: [{ messageID: "assistant-1", partID: "read-1" }],
},
{ type: "part", ref: { messageID: "assistant-1", partID: "text:0" } },
{ type: "message", messageID: "compaction-1" },
{ type: "message", messageID: "user-1" },
{ type: "message", messageID: "user-2" },
])
})
test("keeps a group slot stable through join, repartition, and completion", () => {
const timeline = SessionTimeline.make()
timeline.appendPart({ messageID: "assistant-1", partID: "read-1" }, { type: "tool", name: "read" })
const slot = timeline.slots()[0]
timeline.appendPart({ messageID: "assistant-2", partID: "grep-1" }, { type: "tool", name: "grep" })
expect(timeline.slots()[0]).toBe(slot)
timeline.repartition(new Set(["read-1"]))
expect(timeline.slots()[0]).toBe(slot)
expect(slot()).toMatchObject({
refs: [{ messageID: "assistant-2", partID: "grep-1" }],
pending: [{ messageID: "assistant-1", partID: "read-1" }],
completed: false,
})
timeline.appendMessage("user-queued", { pending: true, compaction: false })
expect(slot()).toMatchObject({ completed: false })
timeline.appendMessage("user-queued", { pending: false, compaction: false })
expect(timeline.slots()[0]).toBe(slot)
expect(slot()).toMatchObject({ completed: true })
})
test("does not complete an active group for duplicate messages or footers", () => {
const timeline = SessionTimeline.make()
timeline.appendMessage("user-1", { pending: false, compaction: false })
timeline.appendPart({ messageID: "assistant-1", partID: "reasoning:0" }, { type: "reasoning" })
const first = timeline.slots()[1]
timeline.appendMessage("user-1", { pending: false, compaction: false })
expect(first()).toMatchObject({ completed: false })
timeline.appendFooter("assistant-1")
timeline.appendPart({ messageID: "assistant-2", partID: "reasoning:0" }, { type: "reasoning" })
const second = timeline.slots()[3]
timeline.appendFooter("assistant-1")
expect(second()).toMatchObject({ completed: false })
})
test("moves a promoted queued message before the remaining queue", () => {
const timeline = SessionTimeline.make()
timeline.appendPart({ messageID: "assistant-1", partID: "reasoning:0" }, { type: "reasoning" })
timeline.appendMessage("user-1", { pending: true, compaction: false })
timeline.appendMessage("user-2", { pending: true, compaction: false })
timeline.appendMessage("user-2", { pending: false, compaction: false })
timeline.appendPart({ messageID: "assistant-2", partID: "text:0" }, { type: "text" })
expect(withoutIDs([...timeline.values()])).toEqual([
{
type: "group",
kind: "reasoning",
completed: true,
refs: [{ messageID: "assistant-1", partID: "reasoning:0" }],
},
{ type: "message", messageID: "user-2" },
{ type: "part", ref: { messageID: "assistant-2", partID: "text:0" } },
{ type: "message", messageID: "user-1" },
])
})
test("advances the queue boundary when a running compaction completes", () => {
const timeline = SessionTimeline.make()
timeline.appendPart({ messageID: "assistant-1", partID: "read-1" }, { type: "tool", name: "read" })
timeline.appendMessage("compaction-1", { pending: true, compaction: true })
timeline.appendMessage("user-1", { pending: true, compaction: false })
timeline.appendMessage("compaction-1", { pending: false, compaction: true })
timeline.appendPart({ messageID: "assistant-2", partID: "grep-1" }, { type: "tool", name: "grep" })
expect(withoutIDs([...timeline.values()])).toEqual([
{
type: "group",
kind: "exploration",
pending: [],
completed: true,
refs: [{ messageID: "assistant-1", partID: "read-1" }],
},
{ type: "message", messageID: "compaction-1" },
{
type: "group",
kind: "exploration",
pending: [],
completed: false,
refs: [{ messageID: "assistant-2", partID: "grep-1" }],
},
{ type: "message", messageID: "user-1" },
])
})
function assistant(id: string, content: SessionMessageAssistant["content"]): SessionMessageAssistant {
return {
type: "assistant",