Problem
/api/sessions and /api/sessions/[id] never send ETag/Last-Modified, and the list route also sends Cache-Control: no-store. A client with an unchanged session store therefore re-transfers the full payload on every page load, session switch, and app resume.
This is most visible on mobile. An installed PWA or a WebView shell does not keep the SPA alive across launches, so the app re-mounts on every open and re-fetches the session list plus the active session's context window each time. Measured on a local server with 82 stored sessions:
| request |
body |
after #731 (gzip) |
GET /api/sessions |
95,446 B |
27,625 B |
GET /api/sessions/{id}?deferThinking=1&deferMedia=1 (8,712-message session, tail=50 page) |
257,987 B |
100,591 B |
gzip (#731) removed ~71%, but nothing lets the client skip a request whose answer has not changed:
$ curl -D - -H 'If-None-Match: "anything"' .../api/sessions
HTTP/1.1 200 OK
cache-control: no-store
content-encoding: gzip
Why this should be cheap to fix
The server already computes exactly what a validator needs:
app/api/sessions/route.ts returns sessionListVersion (getSessionListVersion() → globalThis.__piSessionListGeneration), a monotonic counter the client already uses in components/SessionSidebar.tsx to decide whether to refresh.
app/api/sessions/[id]/route.ts already calls statSync(filePath).mtime to populate the modified field.
So a weak ETag is derivable on both routes without adding I/O.
A wrinkle: no-store blocks revalidation
The routes forbid storing the response, and the client requests the list with fetch(..., { cache: "no-store" }) in two places. A browser that may not store a response cannot send If-None-Match, so adding an ETag alone would change nothing. Making 304s reachable means relaxing these to no-cache (store, but always revalidate). That is a real API behavior change, so I am opening an issue rather than sending a PR.
Related: two call sites pull the whole list for one session
components/AppShell.tsx:570 (restoreWorkspaceContext) — finds the last-open session id.
components/AppShell.tsx:768 (hydrateSelectedSession) — hydrates one transient session.
Conditional requests would make both free when nothing changed; a targeted lookup (e.g. ?id=) would avoid the transfer entirely.
Proposal
- Add a weak
ETag to /api/sessions and /api/sessions/[id]; reply 304 to a matching If-None-Match. Keep ?force=1 bypassing validators.
- Relax those routes from
no-store to no-cache so browsers may store and revalidate.
- Optionally accept a single-session lookup on
/api/sessions for the two call sites above.
Happy to prepare a PR for (1) and (2) if the direction looks right. Point 3 and the no-store relaxation are separable if you would rather take them one at a time.
Problem
/api/sessionsand/api/sessions/[id]never sendETag/Last-Modified, and the list route also sendsCache-Control: no-store. A client with an unchanged session store therefore re-transfers the full payload on every page load, session switch, and app resume.This is most visible on mobile. An installed PWA or a WebView shell does not keep the SPA alive across launches, so the app re-mounts on every open and re-fetches the session list plus the active session's context window each time. Measured on a local server with 82 stored sessions:
GET /api/sessionsGET /api/sessions/{id}?deferThinking=1&deferMedia=1(8,712-message session,tail=50page)gzip (#731) removed ~71%, but nothing lets the client skip a request whose answer has not changed:
Why this should be cheap to fix
The server already computes exactly what a validator needs:
app/api/sessions/route.tsreturnssessionListVersion(getSessionListVersion()→globalThis.__piSessionListGeneration), a monotonic counter the client already uses incomponents/SessionSidebar.tsxto decide whether to refresh.app/api/sessions/[id]/route.tsalready callsstatSync(filePath).mtimeto populate themodifiedfield.So a weak
ETagis derivable on both routes without adding I/O.A wrinkle:
no-storeblocks revalidationThe routes forbid storing the response, and the client requests the list with
fetch(..., { cache: "no-store" })in two places. A browser that may not store a response cannot sendIf-None-Match, so adding anETagalone would change nothing. Making 304s reachable means relaxing these tono-cache(store, but always revalidate). That is a real API behavior change, so I am opening an issue rather than sending a PR.Related: two call sites pull the whole list for one session
components/AppShell.tsx:570(restoreWorkspaceContext) — finds the last-open session id.components/AppShell.tsx:768(hydrateSelectedSession) — hydrates one transient session.Conditional requests would make both free when nothing changed; a targeted lookup (e.g.
?id=) would avoid the transfer entirely.Proposal
ETagto/api/sessionsand/api/sessions/[id]; reply304to a matchingIf-None-Match. Keep?force=1bypassing validators.no-storetono-cacheso browsers may store and revalidate./api/sessionsfor the two call sites above.Happy to prepare a PR for (1) and (2) if the direction looks right. Point 3 and the
no-storerelaxation are separable if you would rather take them one at a time.