Skip to content

fix(inbox): make background batch poll reconciliation tell the truth - #2567

Merged
Chris0Jeky merged 5 commits into
mainfrom
issue-2305/inbox-poll-truth
Sep 4, 2026
Merged

fix(inbox): make background batch poll reconciliation tell the truth#2567
Chris0Jeky merged 5 commits into
mainfrom
issue-2305/inbox-poll-truth

Conversation

@Chris0Jeky

@Chris0Jeky Chris0Jeky commented Sep 4, 2026

Copy link
Copy Markdown
Owner

Summary

Four Inbox defects reported against the bounded batch-triage poll share one root cause: the
background reconciliation in captureStore writes store-wide state as if it were a foreground
load, and it reads that state as if nothing else could have moved it. All four fixes land in
pollBatchTriageCompletion, refreshTerminalDetails and the generation guards they read, so they
ship as one narrow change to one production file with one spec file.

  • A batch whose immediate post-POST refresh exhausted its retries left listError set. Both skins
    hide every row behind that message, so the later successful quiet poll replaced the rows but the
    user still saw a broken empty inbox until pressing Retry.
  • The single-item triage poll and an explicit detail load write a fresher summary through
    upsertSummary without recording a capture write, so the list write generation added in fix(inbox): preserve capture writes across list races #2517
    did not protect them. A slower batch-list response landing afterwards put the row back to its
    pre-poll status.
  • Workload counts were refreshed only when every tracked item was terminal, so one lagging item
    held the sidebar and Home counts stale for up to a minute, and the 60-second deadline stop left
    them stale for good.
  • refreshTerminalDetails reads details for items the user may not have open, and each read took
    the store-wide loadingDetail flag. The Legacy detail panel renders from it, so an unrelated open
    capture lost its body to a "Refreshing detail..." spinner and had its Refresh Detail button
    disabled by background work.

Closes #2305
Closes #2301
Closes #2303
Closes #2304

Changes

frontend/taskdeck-web/src/store/captureStore.ts

frontend/taskdeck-web/src/tests/store/captureStore.spec.ts — a new background batch poll list truth block with 10 it declarations, 11 cases (one it.each covers 401 and 403). Each defect
spec was confirmed red against the current implementation before its fix.

Test plan

Verified (worktree .worktrees/codex-2305-inbox-poll-truth, head of this branch):

  • npx vitest --run --maxWorkers=2 src/tests/store/captureStore.spec.ts — 76 passed (65 cases at the
    base commit, 11 added).
  • npx vitest --run --maxWorkers=2 src/tests/store/captureStore.spec.ts src/tests/store/captureStore.integration.spec.ts src/tests/composables/useInboxOrchestrator.spec.ts src/tests/views/InboxView.spec.ts src/tests/views/paper/PaperInboxView.spec.ts src/tests/components/paper/PaperSidebar.inboxBadge.spec.ts — 6 files, 303 passed.
  • npx vitest --run --maxWorkers=2 src/tests/resilience/degradedMode.spec.ts src/tests/resilience/slowApiAndStorage.spec.ts src/tests/property/storeResilience.spec.ts src/tests/composables/useInboxCounts.spec.ts src/tests/components/inbox src/tests/views/paper/inbox src/tests/composables/useCaptureQueueSync.spec.ts — 13 files, 257 passed.
  • npm run typecheck — clean.
  • npx eslint src/store/captureStore.ts src/tests/store/captureStore.spec.ts — clean.
  • npm run build — built.
  • git diff --check — clean.

Red-before-green, per defect:

NOT verified:

Boundaries and risks

  • Only two files change, both under frontend/taskdeck-web. No docs, locale catalogs, backend, view
    or composable files are touched. Paper views and useInboxOrchestrator were read, not edited.
  • trackLoading: false also applies to the refreshTerminalDetails call inside batchTriage, not
    just the poll. That is the same reconciliation for the same reason; it already ran with
    recordError: false and showToast: false.
  • The count sweep replaces the unconditional notification on the all-terminal stop path. Every
    completed batch still refreshes counts at least once, because completion implies each tracked id
    was observed terminal; the refresh now happens when the outcome is first seen rather than only at
    the end, and an unchanged snapshot no longer re-notifies.
  • latestSummaryGenerationById grows with the number of distinct capture ids seen in a session, the
    same shape as the existing latestDetailWriteGenerationById.
  • Scope guard for [Frontend][Inbox] Successful batch poll recovery leaves a stale list error hiding rows #2305 rests on the existing isCurrent() rule (not stopped, not aborted, still the
    active request, no newer explicit list load, no newer list write) plus the orchestrator cancelling
    polls on a board or archived-history scope change. Pre-existing and unchanged here: batchTriage
    refreshes with an unscoped fetchItems() while the poll reads the scoped query.
  • Detail-cache reads keep their existing guards (write generation plus shouldCache); this change
    adds monotonic protection for summaries only, which is what [Frontend][Inbox] Single and batch triage pollers can overwrite newer summary state #2301 specifies.

A batch whose immediate post-POST refresh exhausted its retries left
listError set, and both skins hide every row behind that message. The
later successful quiet poll replaced items but never cleared the error,
so the rows stayed invisible until the user pressed Retry.

Clear listError only on the accepted success path of the background
list read. Superseded, aborted, 401 and 403 responses never reach it.

Closes #2305
The single-item triage poll and an explicit detail load write a fresher
summary through upsertSummary without recording a capture write, so the
list write generation did not protect them. A slower batch-list response
landing afterwards put the row back to its pre-poll status.

Stamp every summary write with the shared monotonic generation and merge
a background snapshot per row: the list still owns membership, order and
scope, and only a row newer than the read keeps its local value.

Closes #2301
The bounded batch poll only refreshed the badge counts once every
tracked item was terminal, so one lagging item held the sidebar and Home
counts stale for up to a minute, and the 60-second deadline stop left
them stale for good.

Count each newly observed terminal outcome instead, once, and sweep
again at the deadline for an outcome whose tick was aborted before it
reconciled. A snapshot that observes nothing new notifies nobody.

Closes #2303
…anel

refreshTerminalDetails reads details for items the user may not have
open, and every one of those reads took the store-wide loadingDetail
flag. The Legacy detail panel renders from it, so an unrelated open
capture lost its body to a Refreshing spinner and had its Refresh Detail
button disabled by background work.

Add a trackLoading option, default true, and pass false from the
reconciliation. Foreground fetchDetail loading and error behavior is
unchanged.

Closes #2304
The count sweep reads the same post-enqueue observation rule isComplete
uses. Prove it: a poll whose every list read fails reaches the deadline
without refreshing the workload counts off pre-batch cached rows.

Refs #2303
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@Chris0Jeky

Copy link
Copy Markdown
Owner Author

Review gate (Codex credits exhausted, SC-9): one fresh-context adversarial reviewer on head 744f0cff0 (merge base 61e94f672). Verdict: SHIP. No CRITICAL or HIGH: the generation rename keeps #2517's equality-based guards intact, the per-row merge cannot pin a stale row, the counted-outcome sweep loses no refresh (isComplete implies isObservedTerminal), and trackLoading reaches no true foreground read.

Findings by bin:

Evidence at this head (from the implementation run): captureStore.spec.ts 76 passed (65 at base); six Inbox files 303 passed; thirteen neighbour files 257 passed; typecheck, scoped ESLint, build and diff check clean. Not verified: Playwright, manual browser pass, full vitest run. Round count: 1. Merge after ci-required green at this head.

@Chris0Jeky
Chris0Jeky merged commit 46fb41d into main Sep 4, 2026
37 checks passed
@github-project-automation github-project-automation Bot moved this from Pending to Done in Taskdeck Execution Sep 4, 2026
This was referenced Sep 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment