Repository navigation
aggregator(opencode): mapServerSessions never validates the v2 wire containers — a non-array data on any endpoint, or one null session entry, throws out of the mapper and drops every session from that server; an id-less entry fabricates an undefined-sessionId row #2543
Description
aggregator(opencode): mapServerSessions never validates the v2 wire containers — a non-array data on any endpoint, or one null session entry, throws out of the mapper and drops every session from that server; an id-less entry fabricates an undefined-sessionId row
Summary
pm-stack/aggregator/src/sources/opencode.ts maps an OpenCode v2 server's responses into feed entries with no validation of the wire containers or the session entries themselves. mapServerSessions (opencode.ts:116) trusts that every endpoint's data is an array and that every session entry is an object with an id:
- The session list is used unguarded:
const sessions = list.data(:125), the token loop dereferencess.tokenson every entry (:145-148), and the inclusion filter/sort dereferences.time/s.id(:150-157). - The permission list is iterated unguarded (:139,
for (const p of permResp?.data ?? [])). - In
mapOne, the message list is iterated unguarded (:225,for (const m of data)wheredata = msgs?.data ?? [], :213).
Any of these shapes therefore throws a TypeError out of mapServerSessions instead of returning entries or null. Both callers are deliberately fail-soft per server — the collector wraps the call in one catch and logs "sandbox server read failed; attention empty" (scripts/collect-fleet-feed.ts:464-502), and live.ts catches around laptopSessions() / laptopMapped() and marks the laptop part failed (live.ts:907-918, :1012-1019) — so the throw drops every session from that server for the run/read (attention, registry records, and pool-burn attribution), even when all but one element were well-formed. Separately, a session entry with no id passes the inclusion filter when recently updated and is mapped into an entry whose sessionId is undefined, which then keys the live byId map and registry records under undefined.
The v2 wire shapes are a moving external contract (the file header notes they were "probed live 2026-10-25" against two servers, and the two servers run different builds — the laptop's managed server and the sandbox's), so shape drift on one endpoint or one corrupt entry is exactly the failure this mapper must degrade through, the way getJson already degrades a failed fetch to null.
Evidence (verified this run, main 7d1b081)
Probe: laptop Bun 1.4.2, the real src/sources/opencode.ts (verbatim copy, md5 28215b8ddcefd61d13188b5d4663f4bf) driven against a stub v2 server (~/git/fleet/probe-2026-10-29-laneB-ocmap/probe.ts on the laptop; copy in the goal's hidden_files):
- Control (well-formed shapes):
RETURNED entries=1 ids=ses_good. - Session-list
datais an object ({ ses_good: {...} }):THREW TypeError: {} is not iterable— zero entries. - Session list
[good, null]:THREW TypeError: null is not an object (evaluating 's.tokens')— the good session is lost too. - Permission
datais an object:THREW TypeError: {} is not iterable— zero entries. - Message
datais an object for the one included session:THREW TypeError: {} is not iterable— zero entries. - Session list
[good, { title, time.updated }](noid):RETURNED entries=2 ids=ses_good,— a second entry with an empty/undefined sessionId is fabricated. - Benign controls: a string entry in the session list is excluded by the inclusion filter (no crash);
activedataas an array still works viaObject.keys.
File:line (main 7d1b081):
pm-stack/aggregator/src/sources/opencode.ts:122-125— list fetch;if (!list?.data) return null; const sessions = list.data;— truthiness is the only check.pm-stack/aggregator/src/sources/opencode.ts:137-142— permission iteration unguarded.pm-stack/aggregator/src/sources/opencode.ts:145-157— token loop and inclusion filter dereference every entry.pm-stack/aggregator/src/sources/opencode.ts:206-225—mapOnemessage iteration unguarded (forms at :199-200 index[0]only, so an objectdatathere is benign).pm-stack/aggregator/scripts/collect-fleet-feed.ts:464-502— collector catch: one throw = sandbox attention empty, no registry records, no burn attribution.pm-stack/aggregator/src/sources/live.ts:907-918,1012-1019— live catches: one throw = laptop sessions/registry contribution silently absent.
Why it matters
This mapper is the only path for live OpenCode sessions (both servers) into the attention feed, the durable session registry, and per-pool burn attribution. Its contract with callers is "null or entries, fail soft per server" — but a single malformed element or one endpoint's shape drift currently has the same blast radius as the whole server being down, with a log line that says "read failed" rather than naming the shape problem. As the v2 API evolves independently on the two servers, this is the most likely way the fleet silently loses sight of running sessions.
Acceptance criteria
- Triage pins the degradation shape (expected: each endpoint's
datais array-checked — a non-array degrades that endpoint's contribution the way a failed fetch already does; session entries that are not objects or carry no stringidare skipped per entry so valid siblings still map;mapOne's message/form reads degrade per session instead of rejecting the whole server's map — or triage pins a different shape; throwing out ofmapServerSessionsfor a parseable-but-malformed response is the one ruled-out option). - Regression tests in the aggregator suite (opencode.test.ts's stub-server home): list
dataas an object, a[valid, null]list, permissiondataas an object, and messagedataas an object each still return the valid session's entry; an id-less entry produces no entry. - No behavior change for well-formed wire shapes: the existing opencode.test.ts expectations (inclusion window, signals, source stamp, unreachable-server null) pass unchanged.
Filed by new-issue-creator-code-b (lane B), run 2026-10-29 22:20 CDT. Dedup: #2286 is iso() RangeError on out-of-range timestamps in otherwise-valid entries (different function/failure — a huge numeric timestamp, not container/entry shape); #2531 is the Jules keeper-state mapper (different source/file); the isFleetFeed gate family (#2439/#2445/#2446/#2450/#2451/#2539) covers feed-file sections in live.ts, not this live-server mapper. gh searches for mapServerSessions / "not iterable" returned no covering issue.
Activity
dezren39 commentedon Oct 30, 2026
🤖 Agent note · filed
36 remaining items
dezren39 commentedon Nov 2, 2026
agent-note:v1 — holder: parked by repo-worker-code at 2026-11-02T06:57:08Z, reason: Drew-owned canonical PR #4212 remains open. No PR content, diff, branch, worktree, CI, or review was inspected beyond metadata; nothing was changed or merged. Resume after explicit ownership release/handoff or PR terminal state, then revalidate.
dezren39 commentedon Nov 2, 2026
🤖 Agent note · picked up
~/git/fleet/5372sits on branch queue/5372-2543-opencode-wire-validation at the PR head, and the PR body is the creating workers evidence record. Re-verified on head 48f19ae: worktree clean,bun test test/opencode.test.ts6/6 pass on the laptop, diff is exactly the two intended files, Codex review completed with no findings, PR MERGEABLE. The PR-level test check (run 41396717869) is still queued in the fleet runner backlog; merging under the standing merge authority (verified change; CI infra backlog is not a failure), with main CI as the post-merge gate.Merge pull request #4212 from developing-today/queue/5372-2543-openco…
dezren39 commentedon Nov 2, 2026
Fixed by PR #4212, merged as 9a3c52d (task 5372, lane C): mapServerSessions now rejects malformed session-list envelopes, skips rows without a nonempty string id, and degrades malformed optional endpoint contributions while preserving valid sibling sessions. Verified on head 48f19ae: bun test test/opencode.test.ts 6/6 pass (laptop), targeted tsc pass, Codex review no findings.
dezren39 commentedon Nov 2, 2026
🤖 Agent note · done