Repository navigation
Description
aggregator(provider-display): underscore strip breaks one-shot iterables and collapses renamed+plain providers to one display name
Summary
The leading-underscore display strip added to providerDisplayNames in PR #6889 (commit 8dc7e75af7, the #1823 provider rename) introduced two defects in pm-stack/aggregator/src/provider-display.ts:
- The function now spreads its
Iterable<string>parameter twice, so a one-shot iterable produces a garbage map.providerDisplayNamesis declared overIterable<string>(line 37), and the pre-rename implementation spread it exactly once. The new code spreads it at line 38 to build the stripped list and again at line 44 (const originals = [...names]) to recover the keys. For a one-shot iterable — a generator, aMap.keys()/Set.values()iterator — the second spread is empty,originals[i]isundefined, and the result is a single-entry map keyed byundefined. Every lookup misses, andproviderDisplayNamesilently falls back to the unstripped canonical id. - The underscore strip is not collision-aware, unlike the
-directrule in the same function. The-directstrip keeps the suffix when a plain base provider exists in the same set, expressly "so the two stay distinguishable" (module header). The underscore strip has no such guard: a set containing both the renamed id and its plain base — e.g._claudeandclaude, or the new pool series_commandcodeand the pre-rename seriescommandcodethat the rename deliberately kept in the store — renders both providers under the identical display string.
Evidence
pm-stack/aggregator/src/provider-display.ts:37-58 at 4cd2927643 (origin/main):
export function providerDisplayNames(names: Iterable<string>): Map<string, string> {
const list = [...names].map((name) => { ... }); // first spread (line 38)
...
const originals = [...names]; // second spread (line 44)Verified this run on the laptop with the real file verbatim (md5 f16c0bdb00963613ca7b19399f667520, probe ~/git/fleet/probe-provider-display, bun 1.4.4):
| Input | Result |
|---|---|
["_claude", "_antigravity.gemini", "meta"] (array, control) |
correct map, underscore stripped |
generator yielding "_claude", "meta", "_antigravity.gemini" |
Map { undefined: "claude" } — size 1, get("_claude") and get("meta") both undefined |
Map.keys() iterator over _claude/meta |
same single undefined-keyed entry |
providerDisplayName("_claude", generator) |
returns "_claude" (fallback, unstripped) |
["_claude", "claude"] |
both display "claude" |
["_commandcode", "commandcode"] |
both display "commandcode" |
["_antigravity.gemini", "antigravity.gemini"] |
both display "antigravity.gemini" |
["openai", "openai-direct"] (contrast) |
-direct correctly kept: the existing rule is collision-aware |
Control: the shipped suite pm-stack/aggregator/test/provider-display.test.ts passes 11/11 on the same tree (run via git archive origin/main) — it only ever passes arrays, and has no renamed+plain pair case.
All current call sites (render.ts:2167, render.ts:3364, render.ts:3714) pass arrays, so defect 1 is latent today — but the exported signature promises Iterable<string>, and the singular helper providerDisplayName(name, names) invites exactly the iterator inputs that break.
Why it matters
This module is the dashboard's one display-name rule, and its header states the contract: display strings change, ids/keys never do, and names that would collide stay distinguishable. Defect 1 breaks the type contract silently (no throw — wrong markup), and defect 2 makes two different providers/series indistinguishable on the quota board precisely in the migration window the rename created, where old and new ids coexist in the store by design.
Acceptance criteria
-
providerDisplayNamesreads its input exactly once: a generator orMap.keys()iterator yields the same map as the equivalent array, keyed by the exact input strings. - When stripping the leading underscore would make a name's display equal to another distinct input's display in the same set (renamed + plain base both present, plain or dotted), the renamed name keeps a distinguishable display (mirroring the
-directrule's keep-on-collision behaviour); when there is no collision, the underscore is stripped as today. - New regression tests in
pm-stack/aggregator/test/provider-display.test.tscover: a one-shot iterable input, and a renamed+plain pair (_claude+claudeand/or_commandcode+commandcode). -
bun test test/provider-display.test.tspasses (existing 11 tests unchanged in behaviour).
Related: #1823 / PR #6889 (the rename that introduced the strip); #6125 (the original -direct display rule).
Activity
developing-today commentedon Nov 7, 2026
CODE-2153
dezren39 commentedon Nov 7, 2026
🤖 Agent note · filed
developing-today commentedon Nov 8, 2026
🤖 Agent note · handed off
opencode(§1b); handed off to the claimant. Issue aggregator(provider-display): underscore strip breaks one-shot iterables and collapses renamed+plain providers to one display name #6891 OPEN, no hard-stop labels (fresh check this shift).dezren39 commentedon Nov 8, 2026
🤖 Agent note · picked up
dezren39 commentedon Nov 8, 2026
🤖 Agent note · parked
basic: proxy port fallbacks,basic: scripts-testPython 3.12/3.14). Queue task 11288 is parked for 20 minutes. No follow-up edits were made after opening the PR.