Skip to content

core-client: one failed project-team read is cached as an empty team for the whole board open (#1902 regression) #1910

Closed

Description

@HugoXK

Implementation: FlyBot (ours) — PR follows within 24h. Do not pick up without a pickup-ok comment on this issue.

Written-by: FlyBot Lead
Claimed-by: FlyBot Lead
Claim-Id: 7eb8f3bc994249e382e025508180a638
Branch: dev/1-p269-team-read-retry
Base: cf8388e
At: 2026-10-31T07:47:09Z

Implementation: FlyBot (ours) — PR follows within 24h. Do not pick up without a pickup-ok comment.

Written-by: FlyBot Lead

What happens

On main at cf8388eb, one failed project-team read at the moment a Project board is opened leaves the team block (Project Chief, my agent, the Dossier control) showing "No member is named yet." for the whole life of the open board. The 5-second poll never asks again; closing and reopening the board is the only way to get the dossier back. Before this, the same failure healed on the next tick.

Why

#1902 (merged in #1900) made the dossier a single read per board open, and that read swallows its own failure:

  • core/src/client/project-board.js:884 — knownTeam !== undefined ? knownTeam : read('project-team', { id: project.id }).catch(() => null): a refusal becomes null with no sign.
  • :897 — team: team !== null && typeof team === 'object' ? team : { rows: [] }: the null is normalised into an ordinary empty team, so "we did not get an answer" and "the team is empty" become the same value.
  • :959-964 — let team / let loadBoard = () => readProjectBoard(read, wanted, team) / team = next.team: after the first read, team is that { rows: [] } object rather than undefined, so knownTeam is handed in on every later poll and project-team is never asked again.

So a single transient refusal at open (a slow persona/memory read, a channel hiccup) is cached for the session as a real answer, while the board's other columns keep refreshing every 5 s and the reader has no way to tell the team block is stale-empty.

Reproduction (read-only, no repo change)

  1. core/src/client/project-board.js readProjectBoard(read, wanted, knownTeam) with a read seam whose project-team rejects on the first call and answers one row afterwards:
    • open: project-team calls = 1, team.rows = 0
    • one poll: calls = 1, team.rows = 0 (no retry)
    • three polls: calls = 1, team.rows = 0
  2. Same seam mounted in ProjectBoardOverlay (react stub): happy path 1 call / rows 1; failed-first open 1 call / rows 0 for every tick; close and reopen → rows 1.
  3. On 2ec16870 the same failed-first open heals: the failing read rejects the whole board read, #1814's catch keeps the last good board, and the next 5-second tick asks again — the team block appears within one tick.

Expected

  • A project-team refusal at open must not be cached as an empty team: the next poll (5 s) asks again, and once an answer arrives the block fills and then stays frozen for the rest of the open.
  • Distinguish "no answer yet" from "the team really is empty": the empty answer must still print the empty state, and it must not trigger a re-read every 5 s.
  • The single-read-per-open decision of #1900 stands: after a successful read, the poll must not read project-team again.

Scope

core/src/client/project-board.js (return a freshness flag beside team, e.g. teamFresh, and only write the overlay's team when it is fresh — a refusal leaves team undefined so the next tick retries), the rebuilt core/client.js, the generated patches/ds-bot/0008-client-bundle.patch + patches/ds-bot/files/… mirror through generate, and one test in core/test/project-board-view.test.js: first project-team rejects, second answers one row → after one tick rows 1 and calls 2, and after three more ticks calls stay 2. Roughly 15 lines plus the test.

Not in scope: the dossier's freshness while a board stays open for a long time (a separate slice — re-read on opening the Dossier panel), and #1814's poll behaviour for the other three reads.

Verification expected on the PR

Red first on cf8388eb with only the new test added (calls 1 / rows 0 where 2 / 1 is expected), then green; core suite, check, canonical, a from-zero generate that leaves git status --porcelain empty, and TZ=UTC ARCH_DOCS_BASE=cf8388eb npm run verify exit 0.

Duplicate check

Searched issues for project-team, dossier and the retry wording before opening this: the closest are closed #1900 (the change that introduced this behaviour), #1864 (dossier display), #1892 (role-pool ranks) and #1814 (poll keeps the last good read) — none covers "one failed dossier read is cached as an empty team for the session", so this is not a duplicate. The read-side finding came from FlyBot Product's audit of cf8388eb.

Activity

  1. Birfy commented on Oct 31, 2026

    @Birfy
    Collaborator

    Triage

    No agent-ready: PR #1911 (open, head d89b7604, branch dev/1-p269-team-read-retry) already carries Fixes #1910 and implements the fix, so a ready tag here would invite a second pickup. Nothing else was added; both I-class and H-class states are absent, and the finding is reproducible as written.

    Verified against main cf8388eb (file blob identical to main today)

    • core/src/client/project-board.js:884 — knownTeam !== undefined ? knownTeam : read('project-team', { id: project.id }).catch(() => null); the refusal becomes null.
    • :897 — team: team !== null && typeof team === 'object' ? team : { rows: [] }; the null becomes an ordinary empty team, which :861 then prints as "No member is named yet."
    • :959 — let team; :960 const loadBoard = () => readProjectBoard(read, wanted, team); :964 team = next.team — after the open, team is that { rows: [] }, so every later poll passes it as knownTeam and project-team is never asked again.

    Reproduction (read-only, readProjectBoard extracted unchanged, read seam rejecting project-team once and answering one row afterwards)

    open:   calls 1  rows 0
    poll 1: calls 1  rows 0
    poll 2: calls 1  rows 0
    poll 3: calls 1  rows 0
    reopen: calls 2  rows 1        (closing and reopening is the only heal)
    real empty team: calls 1  rows 0   (the honest empty answer stays frozen, as it must)
    

    So the report is exact: one refusal at open is cached for the life of the open board; the other three columns keep refreshing every 5 s.

    Relationship

    Red-first test to expect

    core/test/project-board-view.test.js, one case: first project-team rejects, second answers one row — after the open rows 0 / project-team calls 1; after one 5 s tick rows 1 / calls 2; after three more ticks calls stay 2. Plus the guard already implied: a real { rows: [] } answer prints the empty state and is not re-read. Line numbers above are the ones the test should pin; the fix touches :884/:897 (freshness beside team) and :959-964 (keep team only when fresh), plus the rebuilt core/client.js and the patches/ds-bot/0008-client-bundle.patch mirror.

  2. added
    needs-humanThe agent loop stopped; a person must decide (reason in the last comment)
    on Oct 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    needs-humanThe agent loop stopped; a person must decide (reason in the last comment)

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions