Skip to content

Join provider refreshes and prevent scoped false zeros - #1235

Merged
iamtoruk merged 20 commits into
getagentseal:mainfrom
avs-io:codex/provider-reconciliation-races
Sep 2, 2026
Merged

Join provider refreshes and prevent scoped false zeros#1235
iamtoruk merged 20 commits into
getagentseal:mainfrom
avs-io:codex/provider-reconciliation-races

Conversation

@avs-io

@avs-io avs-io commented Sep 2, 2026

Copy link
Copy Markdown
Member

Dependency

Stacked on #1234. Review the final three commits for this slice; once #1234 lands, this PR reduces to four macOS files.

Summary

  • validate all-provider evidence while preserving the explicit one-shot bypass
  • reconcile cost and call mismatches independently instead of accepting partially stale payloads
  • respect authoritative scoped hasUsage signals
  • snapshot cache keys atomically and join an existing all-provider refresh rather than launching competing work
  • keep loading, stale, and error recovery transitions consistent

Verification

  • 31 focused AppStoreRefreshRecoveryTests
  • 21 focused ServeConnectionTests
  • exact aggregate acceptance package built universal and passed strict code-sign verification
  • installed build acceptance-a1ec918e verified live: Hermes $0.48, 7 calls, 7 sessions; Cursor $0.28, 7 calls, 7 sessions
  • installed UI exposes only All, Codex, Claude, Hermes, and Cursor; closing restores the retracted Codex dock

This remains draft until #1234 is accepted and the stacked diff is reviewed in that state.

@avs-io
avs-io force-pushed the codex/provider-reconciliation-races branch from 699db51 to 735b20d Compare September 2, 2026 11:15
@avs-io
avs-io force-pushed the codex/provider-reconciliation-races branch from 735b20d to c9c5bc0 Compare September 2, 2026 13:46
…#1132

Two open branches claim daily-cache version 30, so a v30 file on disk carries
unknown accounting. Bump this branch to 31 and move the Hermes
pending-rederive contract version with it, so a v30 file written under getagentseal#1132's
accounting is treated as a foreign prior version: carried forward per slice,
never adopted as the finalized base.

The getagentseal#946 copilot fixture seeded itself at DAILY_CACHE_VERSION - 5, a moving
target that crossed the copilot contract boundary (26) at this bump and
silently stopped granting the entitlement those tests exist to check. Pinned
to 25, which is the fixed historical version the fixture actually means.

Disclosure: the bump re-derives every day whose sources still survive, so
finalized history shifts. On the reviewer's machine Claude moved -0.78
percent. That is a corrected figure replacing a stale cached one, not a loss,
but it is a visible change to numbers users have already seen.
…s with shrunken parses

The provider-scoped overlay blind-set a fresh slice for every date it could
produce one. A settled day's sources age off disk continuously, so that fresh
slice is a lower bound, not a correction, and the overlay replaced finalized
cache days with whatever the surviving files still explained. It ran over the
whole period and over the whole history, so the scoped view could report a
fraction of the same day the all-provider view served off the same cache.

The overlay now routes through mergeDayEntries with guardPartialSurvival:
fresh may FILL a (date, provider) the cache lacks and still wins inside the
settle window, but it can no longer SHRINK a settled day the cache already
holds. Filling has to keep working, because on a cold cache the scoped parse
is the only source a historical day has.

Measured on this machine over 2026-08-03..2026-09-01, comparing scoped
--provider claude against the claude slice of the unscoped run:

  before  $16649.42 / 81812 calls   (short by $291.70 and 2507 calls)
  after   $16941.12 / 84319 calls   (exact match, both fields)
…pend

hasUsage is a new payload field, so every RELEASED CLI omits it. Both decoders
treated the absent case as "derive from cost", which is the strict reading of
a field that is not there: a provider with $0 spend in the period vanished
from the picker even though the user has it and is actively using it on an
included subscription.

Absent now means visible on both surfaces. The strict signal still applies
wherever the CLI actually emits the field, so an explicit false still hides an
idle discovery row. Settings.tsx carried the same fallback and is fixed with
it.
The loader reads only the v3 path, so a hermes-session-ledger.v1.json from an
earlier install sat in the cache dir unread and undeleted. It is dead weight
that can only mislead: the parse-version bump forces a cold re-parse that
rebuilds every cursor from source, so nothing in it is recoverable. Unlink it
when the v3 file is absent.

The v2 test this replaces was vacuous. It wrote a v2 file and asserted the
ledger was empty, but the loader never looks at a v2 path, so it passed
whatever the code did. The replacement uses a real v1 fixture and asserts both
halves: the ledger loads empty AND the v1 file is gone afterwards.
…-flight fetch

The interactive refresh path bailed with false whenever the key was already
being fetched. recoverFromStuckLoading() reads that return value, so a healthy
fetch that was simply still running was announced as a failed recovery, and a
second interactive refresh threw away a result that was moments from landing.

Join the in-flight fetch and report on its outcome, which is what the quiet
path already did.

Adds a DataClient fetch hook under DEBUG: slot ownership and acceptance
ordering are only observable when two REAL fetches overlap, which needs a
fetch whose completion the test controls rather than a spawned CLI.
inFlightKeys held only a timestamp, so the slot had no notion of WHICH fetch
owned it. The watchdog and the display-sleep reset both evict an occupant, and
a later fetch then legitimately claims the same key. When the evicted fetch
finally returned, its defer cleared the new owner's slot and resumed every
task parked on it, so a waiter woke on an empty cache and reported failure
while the real fetch was still running.

The slot now carries a token. A fetch releases it only if it still owns it;
the watchdog and the resets take an explicit force path, because they are
declaring the slot dead rather than reporting a fetch that ended.
…every tab click

To guarantee the all-provider slice was cached before a scoped result could be
accepted, the code simply awaited the all-provider fetch BEFORE starting the
scoped one. That is two full parses back to back on every tab click, and the
ordering it bought is only needed at acceptance time, not at start time.

Both fetches now start together. fetchPayload awaits the all-provider task
after its own fetch has already returned and before the consistency check
reads it, so the evidence is still present when the scoped result is judged.

The resident-bypassing cold fetch was already conditional on a contradiction
actually being detected; it is unchanged.
…eing served

When a scoped payload disagreed with its all-provider slice, the code re-ran
the fetch bypassing the resident child and, if it STILL disagreed, threw. That
verified payload is what the CLI actually reports for the provider, so
throwing traded a real number for an error banner and left the tab with
nothing at all.

The payload is now cached with a contradictsAll flag on CachedPayload
(add-only) and served, and the popover carries one subdued secondary line
saying the total may be incomplete. The refresh paths treat an
already-verified contradiction as usable, so a poll cannot re-run the one-shot
parse forever.
… under the selected tab

Switching providers stashed the outgoing payload and rendered it under the new
tab until that tab's own fetch landed. That is not continuity, it is a wrong
number: opening the Hermes tab printed the all-provider total, and the test
suite pinned that behaviour by asserting $12 under Hermes and $12.34 under
Cursor.

The fallback is gone. With no cached data for the selection, `payload` is
empty, `hasCachedData` is false, and the popover renders the loading state it
already has for a cold key, so no dollar figure is shown for a provider whose
data has not arrived. AgentTabStrip and MenuBarContent take the same
hasCachedData gate for the same reason.
ProviderFilter.cliArg is both the --provider argument and the id the
payload-consistency guard matches against providerDetails. A case whose cliArg
is not a registry name silently disables that guard for its tab: the lookup
finds no detail row, so no contradiction is ever detected. Nothing in either
language can see both sides, so the contract is checked from the TypeScript
side, where the registry is importable and the Swift source is readable.

Table-driven over all 30 enum cases. No mapping needed fixing: .kimiCode
already yields `kimicode`, which is the registry name (`kimi-code` is not a
provider the CLI knows), and every other case resolves too. Setting .kimiCode
to `kimi-code` makes this suite fail, which is the check working.
@iamtoruk
iamtoruk marked this pull request as ready for review September 2, 2026 18:51
@iamtoruk
iamtoruk merged commit 500412a into getagentseal:main Sep 2, 2026
17 checks passed
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.

2 participants