Skip to content

payload: add-only stale marker for read-only stale menubar serves - #1100

Merged
iamtoruk merged 2 commits into
mainfrom
fix/menubar-freshness-marker
Aug 22, 2026
Merged

payload: add-only stale marker for read-only stale menubar serves#1100
iamtoruk merged 2 commits into
mainfrom
fix/menubar-freshness-marker

Conversation

@iamtoruk

Copy link
Copy Markdown
Member

Summary

Closes #771. The warm-refresh gate (#743) serves the prior complete snapshot when the refresh lock times out or is unavailable — correct and fail-closed, but the payload's only time field was the render timestamp, so a stale serve was indistinguishable from a fresh parse.

Since #771 was filed, src/parser.ts gained isSessionHydrationComplete() (PR #937) which already returns false exactly when the last completed parse in-process was a read-only serve that had to skip or stale real on-disk changes. This PR threads that signal through to the payload:

  • src/menubar-json.ts: new optional top-level stale?: boolean on MenubarPayload; buildMenubarPayload gains a trailing optional stale param and sets payload.stale = true only when passed true — never emitted as false.
  • src/usage-aggregator.ts: buildMenubarPayloadForRange passes isSessionHydrationComplete() ? undefined : true.
  • app/renderer/lib/types.ts: mirrors the field (add-only, per this repo's CLI-app payload contract).
  • mac/Sources/CodeBurnMenubar/Data/MenubarPayload.swift: mirrors the field in the Codable data model (let stale: Bool?, decoded via decodeIfPresent so older CLI payloads that never emit it decode fine as nil).

Absence of the field always means "assume fresh," including for payloads from a CLI version that predates it — the fail-safe direction for a marker like this.

Follow-up (not in this PR)

The macOS menubar UI has no visible staleness indicator yet — this PR is data-model only for the Swift side. A subtle indicator (small dot/tooltip near the header, matching the pattern of #1096's cold-state signaling) is a reasonable small follow-up once there's a view spot picked for it; scoped out here to keep this change minimal and reviewable.

Test plan

  • tests/menubar-json.test.ts: new cases assert stale: true when passed, and stale is undefined (not false) on a normal build. Verified revert-proof — reverting the src/menubar-json.ts + src/usage-aggregator.ts changes fails the new stale:true test.
  • tests/usage-aggregator.test.ts: new assertion that a normal (env-isolated, always-complete) parse leaves payload.stale undefined.
  • npm run test (full suite, excluding the separately-gated lock tests): 225 files / 3141 tests passed, 2 files / 5 tests skipped (pre-existing skips, unrelated).
  • tsc --noEmit clean on both the CLI (tsconfig.json) and the desktop renderer (app/tsconfig.json).
  • swift build and swift test --filter MenubarPayloadCombinedTests clean in mac/.

isSessionHydrationComplete() (parser.ts, PR #937) already reports when a
read-only serve skipped or staled real on-disk changes; thread it through
buildMenubarPayload as an optional stale field, present and true only on a
stale serve, always absent otherwise, so older/newer CLI-app pairs stay
compatible. Mirrors the field into the desktop renderer types and the macOS
menubar's Codable payload model (data layer only, no view change).

Drafted with minimax/MiniMax-M3 via local gateway.
isSessionHydrationComplete() (parser.ts) reads a module-level global that is
only safe immediately after the parse it describes, with no intervening
awaits. The stale-marker wiring landed in the prior commit read it at the
bottom of buildMenubarPayloadForRange, after several awaits -- including,
on the claude-config-scoped branch, a second parseAllSessions call for the
365-day history block that runs after the parse producing the headline
data. That let the history backfill's hydration outcome silently overwrite
the headline's.

Capture the flag into a local right after each branch's primary parse
resolves (the claude-config-scoped parse, or buildDurablePeriod) and thread
that captured value to buildMenubarPayload instead of re-reading the global
at the end. This also closes a cross-request race: once captured
synchronously, no other in-flight request's parse can flip it under us.

Regression test simulates a complete primary parse followed by an
incomplete bystander parse and asserts the payload reflects the primary
outcome, not the bystander's.

Drafted with minimax/MiniMax-M3 via local gateway.
@iamtoruk

Copy link
Copy Markdown
Member Author

Pushed a fix for the review's stale-global-read bug (commit e19c099).

Bug: isSessionHydrationComplete() reads a module-level global in parser.ts that's only safe to read immediately after the parse it describes. The prior commit read it at the very end of buildMenubarPayloadForRange, after several awaits — including, on the claude-config-scoped branch, a second parseAllSessions call (365-day history backfill) that runs after the parse producing the headline data. That let the history parse's hydration outcome silently overwrite the headline parse's.

Fix: capture isSessionHydrationComplete() into a local immediately after each branch's primary parse resolves (the scoped branch's Claude parse, or buildDurablePeriod on the non-scoped path), and thread that captured value to buildMenubarPayload instead of re-reading the global at the bottom. Also closes the cross-request race the review flagged (web-dashboard SWR / parallel MCP calls), since a synchronously-captured local can't be overwritten by another request's later parse.

New test (tests/usage-aggregator-freshness.test.ts): mocks parseAllSessions/isSessionHydrationComplete so the primary parse reports complete and a second (bystander) parse reports incomplete, then asserts the payload reflects the primary outcome. Verified it fails against the pre-fix code (expected true to be undefined, i.e. the bug reproduces) and passes against the fix.

Verified: full suite (226 files / 3142 tests passed, 2 files / 5 skipped — pre-existing), tsc --noEmit clean, swift build + swift test --filter MenubarPayloadCombinedTests clean.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

menubar payload: data-freshness marker for stale readOnly serves

1 participant