docs: wave 11 accepted; SPEC records auth refresh + why the 404 rule needs it

HANDOFF: expired-token incident, local data loss, verification incl. live
sync against the public URL, phone recovery path, HAZARD #12 (slow storage).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Spriteandclaude committed 2026-09-17 02:57:47 +00:00
1 parent 009cb9a7cc
commit 4695731f91
4 files changed
+77 -1

No files matched your search

+61
View File
@@ -835,3 +835,64 @@ time with the monotonic clock (`/proc/uptime`), which does not advance while fro
Also seen 2026-09-16: Claude Code's background-task runner killed Gradle runs "because the system Also seen 2026-09-16: Claude Code's background-task runner killed Gradle runs "because the system
is running low on memory" even with ~6GB available (8GB box, no swap; Kotlin daemon ~2.1GB, is running low on memory" even with ~6GB available (8GB box, no swap; Kotlin daemon ~2.1GB,
Gradle daemon ~1.2GB). Foreground runs of the same command succeeded. Gradle daemon ~1.2GB). Foreground runs of the same command succeeded.
## Wave 11 — M-authexpiry: sync broken by an expired token — ACCEPTED 2026-09-17
**User report:** every sync on the phone said "Sync failed: HTTP 400". Server and public
URL were both fine. PocketBase's request log (`pb_data/auxiliary.db`, `_logs`) showed
every phone request with `auth: ""`: POST books -> 400 "create rule failure", PATCH
books -> 404. **Root cause:** user tokens lasted PocketBase's default 5 days, the app
logged in once and never refreshed, and PocketBase does NOT reject a bad token; it
silently serves the request anonymously. Last good sync was 09-13.
**The worse half: data loss on the phone.** `SyncEngine.update*` hard-deletes the local
row on 404 (SPEC rule). The anonymous PATCHes 404'd on five books that still exist on the
server, so they were deleted locally, and the pull cursor was already past them.
Server copies intact (kclh1e51t7hmj0z, insggwycbrd6o48, 9tp7rumid3rzsn9, 9plhmyj5s68f58e,
mle7o641pn7zzvt). Any unsynced local edits to those five on the phone are lost.
**Lesson: when you debug a server's 4xx, check the app's 4xx handling for side effects.**
**Fixes:**
- `ab1d294` (orchestrator): migration sets user token duration to 180 days (the user chose it).
Verified: fresh login token exp = 180.0 days; refresh also returns 180 days.
- `009cb9a` (worker + chain fix-worker, $4.89 + $1.34): refresh at the top of every sync;
401/403 -> `SyncResult.AuthExpired` (token cleared, URL/email kept, nothing pushed);
library sync bar "Signed out — sign in again to sync" (tap -> setup); settings "Sign in"
button; setup pre-fills URL + email; login clears pull cursors; one-time full re-pull
(`full_repull_2026_09_17_done` flag) to restore the five books; cancellation rethrown in
`sync()`. First gate RED: a pre-existing race in `LibraryViewModelTest` (cancelled
search job not joined). The fix worker made the test join it. That was a test fix, not a
logic change.
**Orchestrator verification:** read the full data-layer + VM diff; assembleDebug /
testDebugUnitTest (355, 2 skipped, 0 fail; was 337) / verifyPaparazziDebug all exit 0; no
"always 'false'"; eyeballed the 4 new PNGs (fine, light+dark). **LiveSyncTest run against
the PUBLIC URL** (`PB_URL=https://bookshelf-dev-b2jqx.sprites.app server/live-sync-test.sh`):
passed, not skipped; server log shows 4 auth-refresh calls, all 200, all authenticated.
Anonymous LIST on all four collections still 403.
**Recovery on the phone:** install the new build. The first sync gets 401 on refresh and
shows "Signed out". The user signs in (password only), which clears the cursors, and the
next sync re-pulls everything, which brings back the five books. The queued PENDING_CREATE
books from the failed syncs push normally. NOT verified on-device yet.
**Known soft spots (accepted):**
- The 401/403 "belt and braces" catch mid-sync would NOT fire for a token that dies
mid-sync. PocketBase would 404 anonymously, not 401. The window is one sync right after
a successful refresh that issued a 180-day token, so it's negligible in practice. It would
matter if tokens are ever revoked server-side (password change, tokenKey reset) during a sync.
- The library sync bar is tappable, but nothing visual marks it as tappable. The label
says what to do.
- `SetupViewModel` pre-fill is async and could overwrite text the user types in the first
few ms. Negligible.
- One unnecessary full pull on fresh installs (the flag starts false). Harmless.
- `SettingsStore.resetFullRepullDoneForTesting` is an internal test-only method.
### HAZARD #12 — sprite storage degrades; restart fixes it
2026-09-17 ~01:30Z: `testDebugUnitTest` ran more than 20 min (normally ~75s).
`ComponentGalleryPaparazziTest` took 763s and `BookDaoTest` 327s, and git was slow too.
Nothing was hung: result XML was still being written, just very slowly. The user restarted
the sprite and the same run took 74s. If builds or git crawl, suspect the box, not the
code: compare against the gate logs' "BUILD SUCCESSFUL in" times and ask the user to restart.
`tasks/wave-chain.sh` now reads its commit subject from `tasks/<task>.subject`
(it was hard-coded for waves 9/10).
+11
View File
@@ -82,6 +82,16 @@ Pull: GET /api/collections/{c}/records?filter=(updated>'{cursor}')&sort=updated
Push: records where syncState != SYNCED. PENDING_CREATE -> POST (with our id), Push: records where syncState != SYNCED. PENDING_CREATE -> POST (with our id),
PENDING_UPDATE -> PATCH, PENDING_DELETE -> PATCH {deleted:true}. PENDING_UPDATE -> PATCH, PENDING_DELETE -> PATCH {deleted:true}.
On 404 for update/delete: drop local record. On 409/duplicate id: switch to PATCH. On 404 for update/delete: drop local record. On 409/duplicate id: switch to PATCH.
Auth: EVERY sync starts with POST /api/collections/users/auth-refresh and stores the
new token, BEFORE any push or pull. 401/403 (from refresh or any later call)
-> clear the token only (keep URL + email), stop, report AuthExpired: the UI
says "sign in again" and routes to setup, pre-filled. Offline -> ordinary
failure, token kept. WHY: PocketBase does not reject an expired token, it
treats the request as anonymous, and the rules then 404 on records that
exist — so the 404 rule above is only safe on a freshly-proven token
(2026-09-17: five books hard-deleted locally this way). User tokens last
180 days server-side (migration 1789608448).
Sign-in clears all pull cursors, so every fresh session reconciles fully.
Order: push THEN pull (so our writes come back canonical). Order: push THEN pull (so our writes come back canonical).
Conflict: last-write-wins on `updated`. Document this in README; do not build Conflict: last-write-wins on `updated`. Document this in README; do not build
anything cleverer. anything cleverer.
@@ -147,6 +157,7 @@ scan Camera + reticle; on hit -> bottom sheet w/ fetched book + shelf picker
locations Bookcases -> shelves tree. CRUD + reorder. Book counts per shelf. locations Bookcases -> shelves tree. CRUD + reorder. Book counts per shelf.
Tap a shelf -> library filtered to it. "Move books" bulk action. Tap a shelf -> library filtered to it. "Move books" bulk action.
settings Server, account, sign out, manual sync + last-sync time, book/cover counts. settings Server, account, sign out, manual sync + last-sync time, book/cover counts.
Session expired -> "sign in again" + Sign in button instead of Sync now.
## Quality bar ## Quality bar
- No emulator on this box (no KVM). Verify via: `./gradlew assembleDebug`, - No emulator on this box (no KVM). Verify via: `./gradlew assembleDebug`,
+1 -1
View File
@@ -1 +1 @@
chain complete 2026-09-15T18:28:39+00:00 chain complete 2026-09-17T02:54:43+00:00
+4
View File
@@ -0,0 +1,4 @@
=== WAVE 11 (M-authexpiry) passed the chain gate 2026-09-17T02:54:43+00:00 ===
commit 009cb9a (pushed); tests 355; worker $4.886013000000001, 123 turns
Mechanical gate only. Orchestrator must still review: read logs/M-authexpiry.summary,
eyeball new Paparazzi PNGs, and read the diff (git show --stat 009cb9a).