Skip to content

aggregator: registry merge wipes a session's model/title/directory when a later sparse record omits them — the durable store forgets what it knew #1560

Closed

Description

@dezren39

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 (the mapOne sparse shape).
  • Merged result: model=undefined, title=undefined, directory=undefined, parentId=null, label="ses_1" — while the special-cased fields behaved exactly as documented (tokensTotal stayed 500, lastSeenAt advanced, startedAt kept). The persisted JSON has no model key 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/directory are undefined preserves the stored values; an incoming parentId of null does 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 label that came from a title.
  • Fields the incoming record actually carries still win (present-value-wins), and the existing special cases (tokensTotal max, lastSeenAt newest, startedAt earliest, taskRefs union) keep working.
  • A regression test in the aggregator suite merges a sparse incoming record (built via recordFromSessionEntry from 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 SessionRecord field is consistent with the doc comment's "when present" contract (update the comment if any field is deliberately exempted).

Activity

  1. developing-today commented on Oct 28, 2026

    @developing-today
    Contributor
  2. added
    kind/bugSomething isn't working (prefixed form of the classic bug label)
    queue/queuedHas a queued task in the Supabase agent queue
    source/creatorFiled by the new-issue-creator job
    task/triageQueue packet kind: verify, reproduce, and route this item
    priority/normalOrdinary priority (the default)
    app/aggregatorPackage/app: aggregator (pm-stack in this repo)
    effort/sSmall: expected under 2 hours of work
    on Oct 28, 2026
  3. dezren39 commented on Oct 28, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · filed

    • Agent: new-issue-creator job (lane A, slug new-issue-creator-code-a)
    • Session / ID: cron new-issue-creator-code-a run 2026-10-27 20:35 CDT
    • Found via: filed this run
    • Task: Filed this issue after verifying the merge-wipe with a Bun probe driving the real registry.ts on the laptop (sparse incoming record erases model/title/directory, nulls parentId, degrades label; control with present values passes).
    • Expectation: effort/s — triage pins the per-field "when present" semantics (esp. parentId null), then the fix + sparse-merge regression test land via the queue lane.
    • Links: queue packet packet-1560.json (repo-health-code-pilot, triage)
  4. added
    queue/claimedQueue worker lifecycle state
    work/assignedQueue worker lifecycle state
    and removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Oct 28, 2026
  5. 13 remaining items

  6. added
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    status/parkedDeliberately paused with a recorded resume condition
    and removed
    queue/claimedQueue worker lifecycle state
    on Oct 31, 2026
  7. dezren39 commented on Oct 31, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · parked (CI pending)

    • Agent: queue worker (repo-worker-code), openchamber scheduled lane
    • Session / ID: queue task 1827
    • State: fix implemented + regression test green; PR fix(aggregator): sparse registry merge preserves stored fields (#1560) #2931 open (branch queue/1827-1560-registry-merge-sparse, worktree ~/git/fleet/1827 at 9813b78 + fix commit 9775b3b). Only CodeRabbit/neutral had reported after ~5min poll; main CI checks not yet in.
    • Resume: read PR check state fresh; merge only if checks green + diff matches intent; else address failures in the same worktree.
    • Tests: slice3 19pass/1fail (pre-existing routes unique fail, identical on clean main); full aggregator suite 529pass/7fail (all 7 pre-existing on clean main).
  8. added
    queue/claimedQueue worker lifecycle state
    and removed
    status/parkedDeliberately paused with a recorded resume condition
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    on Oct 31, 2026
  9. dezren39 commented on Oct 31, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · picked up

  10. added a commit that references this issue on Oct 31, 2026
  11. dezren39 commented on Oct 31, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · done

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

app/aggregatorPackage/app: aggregator (pm-stack in this repo)effort/sSmall: expected under 2 hours of workkind/bugSomething isn't working (prefixed form of the classic bug label)priority/normalOrdinary priority (the default)source/creatorFiled by the new-issue-creator jobtask/triageQueue packet kind: verify, reproduce, and route this item

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions