fix(tasks): source flips serve each side's own work-item cache instead of stale shared entries#8738
Draft
moseoh wants to merge 3 commits into
Draft
fix(tasks): source flips serve each side's own work-item cache instead of stale shared entries#8738moseoh wants to merge 3 commits into
moseoh wants to merge 3 commits into
Conversation
moseoh
force-pushed
the
fix/work-items-cache-eviction-scope
branch
from
July 14, 2026 12:31
fdadced to
3cadfe3
Compare
7 tasks
…sues On a fork, 'auto' routed the Tasks PR list/count to origin — the fork's own PR list, which is almost always empty — while highlighting the Upstream pill (issues resolve upstream-first). The PRs tab showed 'No matching GitHub work' until the user explicitly clicked Upstream, and the highlighted pill contradicted where the data actually came from. stablyai#5176 routed explicit picks through the preference for both sides but deferred the 'auto' side. This aligns it: 'auto' resolves PRs upstream-first exactly like issues; an explicit 'origin' pick still pins PRs to the fork.
Toggling the Tasks Upstream/Origin selector without a manual refresh kept
showing the previous source's rows (or an empty list) until the refresh
button was pressed. The preference-flip eviction and in-flight clearing
matched only bare 'repoId::'/'repoPath::' key prefixes, but TaskPage keys
its work-item cache with a task-source scope prefix
('github:local:...::repoId::limit::query') even for plain local repos —
so the flip evicted nothing, and the post-flip effect kept serving the old
source's still-fresh cache while the forced refetch's correct result only
reached the store cache, never the visible pages.
Broaden the shared repo-cache-key matcher to also match scoped keys
(':: + prefix' segment boundary) and route setIssueSourcePreference's
inline eviction through it. This also covers execution-host-scoped keys
('hostId::repoId::...') that escaped repo evictions the same way.
…ides coexist
Scope the work-item cache key with the repo's effective source side
('repoId::<upstream|origin>::limit::query'). Upstream and origin results
now live in separate entries, so a source flip reads the other side's key —
served instantly while still fresh — instead of evicting shared state and
refetching from scratch. Pre-flip in-flight requests also get their own
dedupe slot, removing the collision class the eviction dance guarded
against; setIssueSourcePreference keeps only the defensive in-flight clear
and the nonce bump that re-runs the Tasks fetch effect.
The key segment is the effective side, not the raw preference: 'auto'
routes upstream-first exactly like an explicit 'upstream' pick (previous
commit), so both share one entry and pinning the already-highlighted pill
is a cache hit rather than a spurious refetch. Legacy-format persisted keys
are never selected again and age out through the bounded cache; repo
evictions still match them via the repoId segment.
moseoh
force-pushed
the
fix/work-items-cache-eviction-scope
branch
from
July 19, 2026 03:50
3cadfe3 to
ec1f718
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Related to #8726 (item B). Stacked on #8727 — the first commit here is that PR; review the last two commits. Draft until #8727 lands, then this rebases to its own two commits.
Toggling the Tasks Upstream/Origin selector without pressing the manual refresh button showed stale or empty lists: after one flip the rows could stay on the previous source while the pager shrank to the new source's count; after flipping again the list could go empty and stay empty. Only the refresh button recovered. Reproduces on
mainfor both the Issues and PRs tabs.Two defects compound:
github:local:…::repoId::limit::query) even for plain local repos, butsetIssueSourcePreferenceevicted only keys starting with barerepoId::/repoPath::— a complete no-op for every Tasks-page entry, and the same mismatch silently skipped scoped entries inevictGitHubRepoCachesand the in-flight dedupe clearing. The post-flip effect then kept serving the old source's still-fresh (60s TTL) cache, while the forced refetch's correct result reached the store cache but never the visible rows (the page/cache reconciliation only patches rows present in both).This PR fixes both layers:
matchesRepoCacheKeynow also matches the::repoId::segment boundary, covering task-source-scoped and execution-host-scoped keys. This independently fixesevictGitHubRepoCaches(repo removal) skipping every scoped entry.repoId::<upstream|origin>::limit::query. Upstream and origin entries coexist, so a flip just reads the other side's key — served instantly while still fresh, refreshed by the existing forced fetch. Pre-flip in-flight requests get their own dedupe slot, removing the collision class the eviction dance guarded against.setIssueSourcePreferencedrops its eviction and keeps the defensive in-flight clear plus the nonce bump that re-runs the Tasks fetch effect.The key segment is the effective side, not the raw preference:
autoroutes upstream-first exactly like an explicit'upstream'pick (per #8727), so both share one entry and pinning the already-highlighted Upstream pill is a cache hit rather than a spurious refetch of identical data.autoevictGitHubRepoCaches)Screenshots
before.mov
after.mov
Testing
pnpm lintpnpm typecheckpnpm test— 5 new regression tests (written first, red on the old behavior): scoped-key repo eviction; flip keeps the other side's entries + bumps the invalidation nonce; post-flip fetch does not dedupe onto a pre-flip in-flight request; toggling back within TTL is a cache hit (2 network calls for 3 states);autoand explicit'upstream'share one entry. Store, TaskPage, github-component, andsrc/main/githubsuites: 2,325 tests pass.pnpm buildAI Review Report
Adversarial race-trace of the toggle path (Codex, plus the authoring agent's verification of each load-bearing claim against the code):
getTaskSourceCacheScope→workItemsCacheKey) vs the bare-prefix eviction was proven by construction, then reproduced as a red unit test; no flaky manual repro required.repos:changedecho re-running the fetch effect around the forced refetch, whose.thenis cancelled; with per-side keys the surviving non-forced run reads the correct side's entry, so the trace is benign now.::repoId::containment requires the repo id (a UUID) at a::segment boundary; cross-repo collision would need one repo's UUID embedded in another's scope, which the scope construction cannot produce.hostId::repoId::…) had the same eviction escape and are covered by the same matcher fix; key construction threads through the existing host/source-scope plumbing unchanged.Security Audit
No new inputs, IPC, command execution, or dependencies — renderer cache-key construction, eviction matching, and tests only.
Notes
auto≡upstream) is only correct onceautoroutes PRs upstream-first. Kept as draft until that lands.totalItemCountat dispatch, so the pager runs degraded until the count lands), and count queries always usegh api --cache 120swith no bypass. The row-side fix here doesn't cover it; the natural follow-up is a per-side count cache mirroring this PR's key scheme, plus (optionally) a cache bypass on flips.sourcesmetadata, so it unmounts during the first fetch of a side (per Route task PR queries by upstream source #5176's "render nothing until metadata arrives" rule) and remounts on response. With per-side entries this brief flicker can happen once per side instead of once per repo. Not addressed here — persisting the raw candidates per repo (or falling back to any cached entry's sources) would keep the chip mounted; happy to follow up.