Repository navigation
Description
Summary
mergeRecords in the aggregator's session registry wipes fields the stored record already knows whenever a later incoming record for the same session omits them. A sparse re-read of a session erases its model, title, and directory, nulls its parentId, and degrades its display label from the session title to the raw session id — and because the merged set is persisted to the durable registry store, the loss is permanent, not just in-memory.
Evidence (verified on main a120b9a9, 2026-10-27)
Root cause — pm-stack/aggregator/src/registry.ts:115-133: the merge is { ...prev, ...r, ... } with special cases only for startedAt, tokensTotal (max), taskRefs (union), and lastSeenAt (newest). The function's own doc comment (registry.ts:110-114) states the contract: "every other field takes the incoming value when present". But records built by recordFromSessionEntry (registry.ts:31-52) always carry the keys model/title/directory/tokensTotal — with undefined values when the source entry omits them — and a spread copies undefined-valued keys, so "absent" incoming fields overwrite present stored ones. label is always present on an incoming record, but recordFromSessionEntry falls back to the raw session id when the entry has no title (registry.ts:40), so a title-less re-read also replaces a good label with ses_….
Sparse incoming records are a real shape, not a hypothetical: sources/opencode.ts:171-178 (mapOne) builds entries with title: s.title, directory: s.location?.directory, model: s.model?.id — all legitimately absent for some sessions — and sources/live.ts:965 feeds those entries through recordFromSessionEntry into mergeIntoRegistry (sources/live.ts:614-631), which persists the merged set with JSON.stringify to the registry file, dropping the wiped keys from the durable store.
Reproduction — Bun 1.4.2 probe driving the real registry.ts (sha256 52c524d5…, identical on both machines; probe + log in the fleet's run records):
- Stored record (from a full entry):
model="model-a",title="Fix the thing …",directory="/home/user/project",parentId="ses_parent",label="Fix the thing …",tokensTotal=500,lastSeenAt=11:00Z. - Incoming: same session, newer
lastSeenAt=12:00Z, no title/model/directory/parent (themapOnesparse shape). - Merged result:
model=undefined,title=undefined,directory=undefined,parentId=null,label="ses_1"— while the special-cased fields behaved exactly as documented (tokensTotalstayed 500,lastSeenAtadvanced,startedAtkept). The persisted JSON has nomodelkey at all. - Control: an incoming record that does carry
model="model-b"correctly replaces the stored model (and tokens rise to 700) — present-value-wins works; only absent-value handling is broken.
Existing test coverage (test/slice3.test.ts, "merge keeps the record, maxes cumulative tokens, unions task refs") only merges records derived from the same full entry shape, so the sparse-incoming case is untested.
Why it matters
The registry is the durable identity store behind the /sessions page (#906 §3): its whole purpose is that "a session stays in the registry after it expires out of the attention feed". Every live read merges feed + laptop records into that store, so any session that is ever re-read through a sparser source permanently loses its model, title, directory, and parent linkage, and its display name regresses to a raw session id — the exact durability the store exists to provide is silently undone by the merge itself.
Acceptance criteria
- Merging an incoming record whose
model/title/directoryareundefinedpreserves the stored values; an incomingparentIdofnulldoes not erase a stored non-null parent unless that is an explicitly decided and documented semantic. - A title-less incoming record does not degrade a stored
labelthat came from a title. - Fields the incoming record actually carries still win (present-value-wins), and the existing special cases (
tokensTotalmax,lastSeenAtnewest,startedAtearliest,taskRefsunion) keep working. - A regression test in the aggregator suite merges a sparse incoming record (built via
recordFromSessionEntryfrom an entry omitting title/model/directory) over a full stored record and asserts the stored fields survive; the existing slice3 merge test still passes. - The fix's treatment of each
SessionRecordfield is consistent with the doc comment's "when present" contract (update the comment if any field is deliberately exempted).
Activity
developing-today commentedon Oct 28, 2026
CODE-606
dezren39 commentedon Oct 28, 2026
🤖 Agent note · filed
13 remaining items
dezren39 commentedon Oct 31, 2026
🤖 Agent note · parked (CI pending)
queue/1827-1560-registry-merge-sparse, worktree~/git/fleet/1827at 9813b78 + fix commit 9775b3b). Only CodeRabbit/neutral had reported after ~5min poll; main CI checks not yet in.routes uniquefail, identical on clean main); full aggregator suite 529pass/7fail (all 7 pre-existing on clean main).dezren39 commentedon Oct 31, 2026
🤖 Agent note · picked up
Merge pull request #2931 from developing-today/queue/1827-1560-regist…
dezren39 commentedon Oct 31, 2026
🤖 Agent note · done