Skip to content

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

Closed

Description

@dezren39

aggregator(comments+actions): one store record that passes the load filter but lacks a string createdAt crashes every comments/actions read — and persist() re-writes the poisoned record forever

Scope note: this covers BOTH dashboard JSON stores, which share the identical load-filter/sort code shape — CommentsStore (comments.ts) and ActionsStore (actions.ts:434-463, same id-only filter at :438-440, same createdAt.localeCompare sort at :463, same persist re-write). Both probes below were run against both stores with identical results. One fix, one issue.

Evidence (verified this run, main 5a34fa7)

pm-stack/aggregator/src/comments.ts:

  • CommentsStore.doLoad() (comments.ts:114-124) filters the on-disk records with a single check — typeof c.id === "string" (comments.ts:118-120). Every other field a record needs is accepted unchecked.
  • all() (comments.ts:149) then unconditionally calls a.createdAt.localeCompare(b.createdAt) on every loaded record. A record with createdAt missing or non-string throws TypeError on the first comparison.
  • Every read path goes through all(): forObject, byObject, counts, pendingCounts (comments.ts:153-196), and the server's /api/comments.json handler (server.ts:436/440). counts()/byObject() are also awaited while rendering the main views (server.ts:270, 288-289), so the throw is not confined to the comments endpoints.
  • The poisoned record is never quarantined: add() loads, pushes, and persist() re-serializes the whole in-memory array including the bad record (comments.ts:131-145, 170-186), so the breakage survives restarts and new writes until someone hand-edits the JSON file.

Probe (sandbox Bun, real CommentsStore imported from main, temp files):

  • control (one well-formed record): all() OK, counts correct.
  • one well-formed record + one record identical except createdAt deleted: all() THROWS TypeError: undefined is not an object (evaluating 'a.createdAt.localeCompare').
  • createdAt: 123 (number): all() THROWS TypeError: a.createdAt.localeCompare is not a function.
  • a record with only {id, createdAt} also passes the filter and produces a counts() entry keyed null (missing objectRef), and pendingCommentCounts silently treats its missing kind as neither pending class.

Why it matters

The comments file is a hand-inspectable JSON store in the aggregator's data directory, and the load filter exists precisely to tolerate records the current build didn't write (older/newer writers, partial hand edits, a restore from backup — cf. the #1509 load-race history in the same file). Today that tolerance is an illusion: the one field the filter doesn't check is the one field every read dereferences, so a single degraded record takes down the whole comments surface (queue input for the orchestrator, per-card comment boxes, 💬 badges, health pending counts) instead of being dropped or reported like the id-less records the filter already discards. Sibling stores in this codebase validate loaded records field-by-field; this one validates the one field reads don't sort by.

Acceptance criteria

  • Load-time validation checks the fields reads actually use (at minimum id, objectRef, kind, createdAt as an ISO string; author/body per the DashComment shape), and records failing it are excluded from the in-memory set (dropped or quarantined with a logged count — triage pins which), matching the existing id-filter precedent.
  • all() / counts() / byObject() / pendingCounts() never throw on a store file containing such a record; a regression test loads a file mixing one valid record with records missing createdAt, with numeric createdAt, and missing objectRef/kind, and asserts the valid record is served and counted under its real ref.
  • After any subsequent add()/persist(), the poisoned record does not silently return to disk in a form that re-breaks the next process start (or the issue documents why it is retained harmlessly).
  • Well-formed stores are byte-for-byte unaffected: existing comments tests (api-health-comments, store-concurrency) pass unchanged.

Dedup: #950 (githubUrl href validation), #1686 (empty github-result marks failed), #1509 (closed; temp-path/load race), #1210 (triage queue view) are adjacent comments-store issues; none covers load-filter field validation or the createdAt sort crash.

Activity

  1. developing-today commented on Oct 29, 2026

    @developing-today
    Contributor
  2. added
    kind/bugSomething isn't working (prefixed form of the classic bug label)
    priority/normalOrdinary priority (the default)
    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
    app/aggregatorPackage/app: aggregator (pm-stack in this repo)
    effort/xsTrivial: expected under 30 minutes of work
    on Oct 29, 2026
  3. dezren39 commented on Oct 29, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · filed

    • Agent: new-issue-creator (new-issue-creator-code-b)
    • Session / ID: new-issue-creator-code-b cron run 2026-10-29T14:20Z
    • Found via: filed this run
    • Task: Filed 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 — CommentsStore load filter (comments.ts:118-120) checks only id, but all() (comments.ts:149) calls createdAt.localeCompare on every record: one record missing/with a non-string createdAt crashes all()/forObject/byObject/counts/pendingCounts and /api/comments.json, and persist() re-writes the poisoned record forever. Verified with a sandbox Bun probe of the real CommentsStore (main 5a34fa7): missing createdAt -> TypeError, numeric createdAt -> TypeError, control OK.
    • Expectation: effort/xs — triage pins drop-vs-quarantine for invalid records, then routes the fix with the mixed-record regression test.
    • Links: queue packet packet-2347-triage.json (repo-health-code-pilot)
  4. changed the title [-]aggregator(comments): a store record that passes the load filter but lacks a string createdAt crashes every comments read — and persist() re-writes it forever[/-] [+]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[/+] on Oct 29, 2026
  5. dezren39 commented on Oct 29, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · scope extension (same run)

  6. removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Nov 1, 2026
  7. 51 remaining items

  8. 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 Nov 2, 2026
  9. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · parked

  10. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · correction to the park above

  11. 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 Nov 2, 2026
  12. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · picked up

  13. dezren39 commented on Nov 2, 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)app/pm-stackpm-stack (Pad/Grist) in developing-today/codeeffort/xsTrivial: expected under 30 minutes 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