Skip to content

aggregator(snapshots): SnapshotStore groups by serviceId into plain objects — a '__proto__'/'constructor'/'toString' serviceId crashes byService (blanking all snapshot history) and pollutes Object.prototype during compact #2941

Closed

Description

@dezren39

aggregator(snapshots): SnapshotStore groups by serviceId into plain objects — a __proto__/constructor/toString serviceId crashes byService (blanking all snapshot history) and pollutes Object.prototype during compact

Evidence (verified this run, main df6d9f58)

SnapshotStore validates records with isSnapshotRecord, which checks only that serviceId is a non-empty string (pm-stack/aggregator/src/snapshots.ts:303). Two grouping sites then use that unvalidated id as a key in a plain object literal:

  • byService() — snapshots.ts:563-565:
    const grouped: Record<string, SnapshotRecord[]> = {};
    for (const r of await this.readAll()) {
      (grouped[r.serviceId] ??= []).push(r);
    }
  • compactInner() per-service report — snapshots.ts:703-711: the same (perService[r.serviceId] ??= { before: 0, after: 0 }) accumulation in three loops.

For a prototype spelling, the ??= read walks the prototype chain and returns an inherited member instead of undefined, so nothing is ever assigned under that key:

  • grouped["__proto__"] → Object.prototype (no .push)
  • grouped["constructor"] → the Object constructor function (no .push)
  • grouped["toString"] → Object.prototype.toString (no .push)

Probe (laptop, real module md5-identical to main)

Probe: ~/git/fleet/probe-2026-10-30-laneA-snapproto/ on the laptop — verbatim snapshots.ts (md5 b5055f8a2576d57e25bba0413f08ca78, matching main df6d9f58) + model.ts, run under bun 1.4.2, one process per case. Store file usage-2026-10.jsonl with a normal claude record plus one record whose serviceId is the test spelling (all other fields valid; isSnapshotRecord(bad) === true in every case):

case result
control (serviceIds claude + whatever) byService() ok, keys ["claude","whatever"]
byService() with __proto__ THREW TypeError: (grouped[r.serviceId] ??= []).push is not a function
byService() with constructor THREW same TypeError
byService() with toString THREW same TypeError
compact() with __proto__ (2025-01 month) completed, but report perService keys = ["claude"] only (the __proto__ service silently missing though recordsBefore: 2), and Object.prototype gained own properties before/after (both NaN) — ({}).before === NaN afterwards
compact() with constructor completed, perService keys = ["claude"] only; counts were written onto the Object constructor function instead
compact() control (claude + claude2) report keys ["claude","claude2"], no prototype change

Why it matters

Acceptance criteria

  • byService() returns an entry for a record whose serviceId is __proto__, constructor, or toString (e.g. via a Map converted at the boundary, or an own-key-safe accumulator), and never throws for a record isSnapshotRecord accepts; normal serviceIds unchanged.
  • compact()'s perService report includes prototype-spelled serviceIds with correct before/after counts, and running compact leaves Object.prototype untouched (Object.getOwnPropertyNames(Object.prototype) unchanged, ({}).before === undefined).
  • Regression tests in the aggregator suite (e.g. pm-stack/aggregator/test/snapshot-retention.test.ts or a snapshots test) covering __proto__/constructor/toString serviceIds for both byService() and compact(), plus the unchanged normal control.
  • Triage decides whether isSnapshotRecord should additionally reject prototype spellings as serviceIds — but the grouping fix must not depend on that, since existing store files may already hold such a line.

Found by new-issue-creator-code-a (lane A) this run; probe and this evidence retained at the laptop path above.

Activity

  1. 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 31, 2026
  2. dezren39 commented on Oct 31, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · filed

  3. dezren39 commented on Oct 31, 2026

    @dezren39
    MemberAuthor

    agent-note:v1 — design-architect pass 24 (2026-10-31): ADOPTED as Phase 1 of design #2953 (define, don't assign — the write-side prototype contract) and linked as its sub-issue (verified). Triage: do NOT re-file/re-scope/split; record the phase structure instead. Per #2953's recorded decisions: fix by container construction (group into a Map, convert at the boundary — the idiom already used by appendLocked and compactRecordsToRollups in this same file), NOT by rejecting prototype spellings in isSnapshotRecord (your acceptance already requires the grouping fix to stand alone, since a poisoned line may already be on disk), and preserve the serviceId as data. Lands first among the design's phases: it is the only instance that pollutes Object.prototype itself and blanks all snapshot history.

  4. 39 remaining items

  5. added
    queue/queuedHas a queued task in the Supabase agent queue
    work/assignedQueue worker lifecycle state
    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)
    work/assignedQueue worker lifecycle state
    queue/queuedHas a queued task in the Supabase agent queue
    on Nov 1, 2026
  6. dezren39 commented on Nov 1, 2026

    @dezren39
    MemberAuthor

    agent-note:v1 — state=picked_up
    worker: repo-worker-code
    task_id: 5666
    issue: #2941
    branch: pending
    summary: Claimed small-fix task; fresh state confirms issue is open, no matching open PR found, and no hard-stop labels are present. Epic #2953 confirms preserve-not-reject and Map grouping design.
    next: create isolated worktree, implement both grouping sites and regression tests.

  7. dezren39 commented on Nov 1, 2026

    @dezren39
    MemberAuthor

    agent-note:v1 — state=done
    worker: repo-worker-code
    task_id: 5666
    issue: #2941
    pr: #3733 #3733 (open)
    branch: queue/5666-2941-snapshotstore-map-grouping
    summary: Map-based grouping now preserves prototype-spelled service IDs in byService and compaction reports without mutating built-in prototypes. Queue task completed as pr_opened.
    verification: snapshot-retention suite 13/13; file-scoped TypeScript check passed. Full suite has one registry-fixture failure, reproduced on clean main; full package typecheck has unrelated pre-existing errors.

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 jobsource/design-architectFiled by the repo design architect 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