Repository navigation
Description
aggregator(registry): a non-string lastSeenAt passes isSessionRecord and crashes mergeRecords' sort — /sessions and /api/registry.json go down once the registry is big enough
Bug
The session registry's hygiene check validates only three fields, but the merge that every live registry read runs sorts on a fourth, unchecked field with a string-only method:
pm-stack/aggregator/src/registry.ts:138-142—isSessionRecordchecks onlyid,source, andlabelare strings.lastSeenAt(andstartedAt) may be any JSON value: a number, a boolean, an object.pm-stack/aggregator/src/registry.ts:134—mergeRecordsreturns[...byKey.values()].sort((a, b) => (b.lastSeenAt ?? "").localeCompare(a.lastSeenAt ?? "")).localeCompareexists only on strings, so a record whoselastSeenAtis e.g.12345throwsTypeError: (b.lastSeenAt ?? "").localeCompare is not a functionwhenever the sort compares it in that position.- The unchecked value reaches the merge from three directions, all filtered only by
isSessionRecord: the durable store file (sources/live.ts:611,loadRegistryStore), the fleet feed's registry section (sources/live.ts:980), and Claude launch records (registry.ts:82,recordFromLaunchcopiesl.lastSeenAtverbatim;parseLaunchRecordschecks only thatidis a string).
registry() (sources/live.ts:976-1000) calls mergeIntoRegistry → mergeRecords (sources/live.ts:619) with no guard around it whenever any incoming record exists. The callers split: /sessions and /api/registry.json await source.registry() uncaught (server.ts:543, server.ts:552), so the request fails; the facet-priming callers catch and silently substitute [] (server.ts:147, :160, :744), so the registry reads as empty there.
Evidence (verified this run, main 5a34fa7)
Sandbox Bun probe (/tmp/probe-laneB-regsort/probe.ts, probe2.ts) importing the real registry.ts:
isSessionRecord(good)= true
isSessionRecord(badNum)= true (lastSeenAt: 12345)
isSessionRecord(badBool)= true (lastSeenAt: true)
good+missing OK (lastSeenAt absent is fine: ?? "" )
big THROWS TypeError (b.lastSeenAt ?? "").localeCompare is not a function
rev THROWS TypeError (same, reversed input order)
The latency is part of the bug: with only two records Bun's sort happened to compare in the direction that coerces the number as the localeCompare argument, so a small registry can accept — and mergeIntoRegistry can persist (sources/live.ts:627-631) — a poisoned record without error. The same record throws reliably once the registry holds several records (8-record probe, both input orders). The durable store therefore accumulates the trigger silently and the failure switches on later, on an unrelated read, once enough sessions exist.
Why it matters
One malformed record — a hand-edited or torn store file, a collector/feed entry with a numeric epoch instead of an ISO string, a launch-record line with a numeric lastSeenAt — permanently breaks the registry surface: every live /sessions and /api/registry.json request fails while that record is present, and there is no signal about which record or field is responsible. The neighbouring stores learned this exact lesson in #2347 (comments/actions: load filter checked only id, all() sorted via createdAt.localeCompare); the registry's filter is the same shape with a different field. This is distinct from #1560 (same file: sparse incoming records wipe fields via undefined spread — a data-loss semantics bug, no type check or crash involved) and its acceptance criteria do not cover this.
Acceptance criteria
- A record with a missing or non-string
lastSeenAtcan no longer crashmergeRecords: eitherisSessionRecordrejects/coerces it at both load sites (store + feed), or the sort (and the>=newest-comparison at registry.ts:131) treats a non-string value as absent — the triage pin records which layer owns the rule, applied consistently torecordFromLaunch's unchecked copy as well. - A poisoned record already in the durable store does not wedge the registry:
/sessionsand/api/registry.jsonkeep serving the remaining records. - Regression tests (aggregator suite, e.g.
test/slice3.test.ts) cover:mergeRecordsover several records including one with numeric and one with booleanlastSeenAt, and a store/feed load containing such a record; existing merge tests keep passing. - No behaviour change for well-formed records: newest-
lastSeenAtordering and the merge special cases from 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's contract are preserved.
Related
- aggregator(comments+actions): a store record that passes the load filter but lacks a string createdAt crashes every comments/actions read — and persist() re-writes it forever #2347 — same load-filter/sort-field mismatch in the comments + actions stores (
createdAt), different stores and validator. - 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 —
mergeRecordsfield-wipe on sparse records; different defect in the same function (spread semantics, not unchecked types). - aggregator: concurrent writes to the JSON stores share one temp path — most fail with ENOENT (returned as '400 invalid'), and persisted records don't match the calls that succeeded #1509 — registry store write atomicity; unrelated.
Activity
developing-today commentedon Oct 29, 2026
CODE-1205
dezren39 commentedon Oct 29, 2026
🤖 Agent note · filed
9 remaining items
dezren39 commentedon Nov 1, 2026
🤖 Agent note · picked up
Merge pull request #3414 from developing-today/queue/5253-2376-regist…
dezren39 commentedon Nov 1, 2026
🤖 Agent note · done
1d6e3e427) · branchqueue/5253-2376-registry-lastseenat-filterDone.
isSessionRecordnow requireslastSeenAt/startedAtto be string-or-absent (rejection, not coercion), andparseLaunchRecordsroutes its output through the same filter. Newtest/registry-hygiene-2376.test.ts: poisoned records (numeric/boolean/object) dropped on the store/feed filter path and the launch path, survivors merge/sort unchanged. Verified on the laptop: 46/46 tests pass (6 new + slice3 + registry-view),tscclean for touched files, Codex review completed with no findings.