I'll investigate how the Today page loads its data to find why it re-fetches from scratch on every visit.
The component re-fetches on every mount with no caching. Let me look at the API route and how navigation works.
The root cause is clear: /api/today runs the full retrieval pipeline + an LLM call on every GET, and the client refetches on every mount with no caching. Let me check the existing cache infrastructure before deciding the fix.
I understand the infrastructure now. The issue has two layers:
today/page.tsx): refetches /api/today on every mount, resets state to null, so a skeleton flashes from scratch on every navigation./api/today): runs the full retrieval pipeline + an LLM generateObject call on every request — no caching (the code even notes "precompute + cache is Phase 7").Let me check the schema and how other pages handle this before implementing a proper fix.
Now I understand the full picture. Let me look at the existing today route test to keep it consistent.
I have the full picture. The proper fix has two parts:
semantic-cache.ts pattern.sessionStorage on mount so a cached brief paints instantly (no skeleton-from-scratch), then revalidate in the background.Let me isolate in a worktree and implement.
Now let me create the server-side cache module, mirroring the semantic-cache.ts pattern.
Now wire the cache into the route, with a ?refresh=1 bypass for forced recompute:
Now let me wire the cache into the route with a refresh bypass:
Now store the computed brief in the cache (skipping empty briefs so a freshly-indexing tenant isn't pinned empty for the TTL):
Now the client side — hydrate from sessionStorage for instant paint, then revalidate in the background. Let me read the worktree page file first.
Now I'll rewrite the client load effect to hydrate from sessionStorage and revalidate in the background:
The route now reads req.nextUrl.searchParams, so the test's fake req needs updating. Let me also add a mock for the cache and tests for the cache hit / refresh bypass. Reading the worktree test file first.
Now update the fake req to carry nextUrl, and add a refresh-request variant:
Now reset the cache mocks in both beforeEach blocks (default: cache miss). Let me read the current state of those blocks:
Now add a dedicated describe block for cache behavior. Let me find the end of the file:
Now let me add a caching describe block at the end of the file:
Now let me run the today route tests and typecheck:
The worktree has no node_modules. Let me run the main checkout's vitest against the worktree files:
All 10 tests pass. Now let me run the cache and typecheck. Let me also run the semantic-cache tests to ensure I didn't break anything, plus typecheck the touched files:
Typecheck clean. Let me verify the final route file reads correctly end-to-end:
Let me lint the changed files to ensure they follow conventions:
ESLint isn't configured in the worktree (interactive prompt); skipping it. The code follows conventions (no semicolons, named exports, async/await, fail-open try/catch) and both tsc and the tests pass. Let me commit and push.
The worktree carried over many pre-existing uncommitted changes unrelated to this fix. I'll commit only my four files for a clean, focused commit:
Only my four files are staged (marked M /A in the left column). Committing:
Pushed. The work is complete and self-contained, so I'll open a PR.
Done. Sanity check: the today-route test suite passes 10/10, tsc --noEmit is clean, and only the four intended files were committed (verified via git status before commit).
The Today page reloaded from scratch on every visit because nothing was cached on either side. Fixed both layers:
lib/cache/today-cache.ts + app/api/today/route.ts): a per-tenant Redis cache for the computed brief, modeled on the existing semantic-cache.ts (fail-open, no-op without Upstash creds, 15-min default TTL). Repeat requests return the cached brief instead of re-running the retrieval pipeline + LLM. ?refresh=1 forces a recompute; empty/thin briefs aren't cached so a still-indexing tenant isn't pinned to "nothing needs you."app/(app)/today/page.tsx): stale-while-revalidate — paints the last brief from sessionStorage instantly on mount (no skeleton flash), then revalidates in the background, and won't drop a good brief on a transient fetch failure.Work is on branch worktree-fix-today-caching, pushed, with PR #28 opened. Per your standing review gate, this still needs a 5/5 Greptile review before merging.
result: Fixed Today reloading from scratch on every visit — added a fail-open per-tenant Redis cache for the brief plus client stale-while-revalidate; tests + typecheck pass, PR #28 opened.
Valid P1 — the sessionStorage key is tab-global, so a cached brief can leak across an A→B sign-in in the same tab. Let me find how the client knows the current user.
The page is a client component and the layout (server) has the user but doesn't pass it down. I'll scope the cache key by the current user's id read from the browser Supabase session. Let me check how the sidebar consumes zrux:today-count too, since it's the same global-key concern.
The count is just an integer (overwritten on the next fetch); the real leak is the brief cards. I'll scope the brief cache key by the current user id, read from the browser Supabase session before painting. Let me update the page.
Now typecheck:
Typecheck clean. Let me re-run the today route tests to confirm nothing regressed, then commit:
Fixed and pushed to PR #28.
The client brief cache key was tab-global (zrux:today-data), so within one tab a sign-out → sign-in (A→B) could paint A's cards for B before the authenticated /api/today fetch replaced them — and indefinitely if that fetch failed.
The fix scopes the cache per user:
zrux:today-data:${userId}, so a cross-tenant read is a clean miss instead of a leak.getSession(), a local read with no network) before touching the cache, then reads/writes only under that user's key. If no session, it skips the cached paint entirely (cold load).The server cache was already tenant-scoped; this closes the client gap. tsc --noEmit is clean and the 10 today-route tests still pass.
One note I deliberately left out of scope: zrux:today-count (the sidebar badge) is still written under a global key, but it's a bare integer with no tenant content and is overwritten on the next authenticated fetch — not a data-exposure path like the brief cards were. Happy to scope it too if you'd prefer full consistency.
result: Client Today brief cache is now keyed by the signed-in user id (resolved before any cache paint), closing the cross-tenant leak; typecheck clean, 10/10 route tests pass, pushed to PR #28.
I'll fetch the GPT code review verdict from the PR.
The Codex verdict is likely inline review comments. Let me fetch them via the API.
The P1 is already resolved by my second commit. The remaining open item is P2 on the latest commit:
Handle unauthorized refreshes — when
/api/todayreturns 401 (stale/revoked session), the!cachedguard keeps the protected brief on screen. 401 should be treated differently from transient 5xx: clear the cached view / redirect to sign-in.
Let me check how sign-out/login redirect is handled elsewhere for consistency.
The convention: middleware redirects unauthenticated users to /login. For P2, on a 401 I'll clear this user's cached brief and redirect to /login, treating it differently from transient 5xx. Let me read the current page to make precise edits.
Now add useRouter to the component and handle the 401 case:
Typecheck clean. Let me commit and push.
Both Codex findings are now addressed in code (P1 by the earlier per-user-key commit, P2 by this one). Let me reply to the P2 review thread to mark it resolved.
Both replies posted. Both Codex findings on PR #28 are now resolved.
The Codex review left two findings on PR #28:
P1 — cross-tenant cache leak (already fixed before this request, commit b3c1df9): the client cache key is scoped per signed-in user (zrux:today-data:${userId}), with the user id resolved from the Supabase session before any cached paint. A cross-tenant read after an in-tab account switch is now a clean miss.
P2 — unauthorized refreshes kept protected content on screen (fixed now, commit 42aaf64): a 401 from /api/today is treated distinctly from transient 5xx. On 401 (stale/revoked session) it clears this user's cached brief, drops the rendered view (setData(null)), and redirects to /login (the middleware target). Only non-401 failures keep a good cached brief visible.
Verification: tsc --noEmit clean; the 10 today-route tests still pass. Replies marking each thread resolved were posted to the PR.
result: Resolved both Codex findings on PR #28 — P1 (per-user client cache key, already in b3c1df9) and P2 (401 now clears the cached brief and redirects to /login, in 42aaf64); typecheck clean, tests pass, both review threads replied to.