oficina / docs Public

app: fix round 2 on Task 6 — retry the resync itself, fix its own advice

BLOCKING (fixtures/editor-entry.js): round 1's resync() ran once and
gave up silently on failure, with a comment claiming the editor would
get "another chance the next time a batch is dropped" — false, and it
inverted the fix. After a failed resync the local doc stays diverged,
so subsequent edits get a real 204 (accepted, never integrated) with
no 4xx to ever trigger another attempt. One transient resync failure
therefore recreated F1's silent-204 loss for the rest of the session.

Reproduced live before fixing: aborted the resync's own GET at the
drop moment (Playwright route interception, round 1's build), typed
a follow-up edit, and confirmed it vanished on reload with a real 204
along the way — then typed a SECOND edit after "recovery," and it
vanished too, because round 1 never actually recovers.

Fixed by extracting the retry itself into fixtures/resync-controller.
mjs (attemptResync/timing/the 'online' listener injected, the same
pattern save-queue.mjs already used): exponential backoff (1s, 2s,
4s, ... capped at 30s) plus an immediate attempt on the browser's own
'online' event. `diverged` is exposed and gates every local edit in
editor-entry.js — a 204 taken while diverged proves nothing, so
nothing is sent while diverged, which also stops burning
maxRetainedUpdates rows on edits that could never have been
integrated anyway (one of the two LOWs the reviewer flagged, fixed as
a natural consequence rather than left open).
fixtures/resync-controller.test.mjs drives the full state machine —
drop, attempt, fail, retry, succeed, and the 'online' fast path — with
a fake clock; confirmed it fails for the right reason (2 !== 3)
against round 1's no-retry logic before confirming it passes against
the fix. A caught-along-the-way bug in the new module itself: run()
was clearing its own timer *reference* without cancelling the
underlying scheduled timeout, so an eager reportDivergence()-triggered
attempt could leave a stale timer alive to fire a redundant extra
resync later — fixed by always cancelling before proceeding.

Re-verified live against the fixed build with the same aborted-GET
reproduction: the during-divergence edit is now never even sent (no
204, nothing to lose), the retry recovers a few seconds later, and an
edit typed after recovery survives a reload.

MEDIUM (render.go): docs.editor.status.failed said "Copy anything
important before leaving this page" — by the time anyone reads it,
resync() has already replaced the view, so the content it points at
is gone from the screen, and it steered a person to stay put when
reloading (the same recovery resync itself performs, available
immediately rather than after resync-controller.mjs's own backoff) is
what actually helps. Rewritten to the past tense ("...were lost")
plus the one thing that helps ("Reload the page if this keeps
happening") — accurate whether read while still diverged or after an
automatic recovery already happened.

MEDIUM (beforeunload's keepalive fallback): verified against a real
browser that fetch's own keepalive:true shares materially the same
quota as sendBeacon (100KB refused by both, 30KB through both), so it
was never actually able to carry the paste-sized payload its own
comment cited as the reason for existing. Corrected the comment
instead of claiming coverage that was never real: the fallback still
helps when sendBeacon's refusal was about the page's shared quota
being used by something else, not this payload's own size, but a
paste-sized edit that has not cleared one debounce window by the time
of an immediate tab close is a real, unclosed, platform-level gap —
stated as such rather than papered over.

Confirmed, not touched: the server still returns 204 for updates it
cannot integrate — open server-side, belongs to the snapshot plan.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011F5CivVm4CmL9ByfSSAaXA
Paul Campbell pushed by paul@keymail.dev bc2dc4ed661c9b3483d42a5919f44644c4265b48 parent e6aa03b
6 files changed, +430 −121
  • .amadan/ci.d/10-gate +8 −0
  • fixtures/editor-entry.js +124 −101
  • fixtures/resync-controller.mjs +111 −0
  • fixtures/resync-controller.test.mjs +155 −0
  • internal/docs/render.go +21 −9
  • internal/docs/static/{editor.40cce03a9468a798.js => editor.15280b7eedd1868f.js} +11 −11

CI passed — run details

Intent

Human

<task-notification> <task-id>a7260d609c3b53ad8</task-id> <tool-use-id>toolu_01AYFcCNSGw6gF4JaPotoJ5j</tool-use-id> <output-file>/home/paulca/.cache/tmp/claude-1001/-home-paulca-amadan-net-carlos-docs/70f302d1-9d5d-4eb2-8b38-e09a15fdd1c3/tasks/a7260d609c3b53ad8.output</output-file> <status>completed</status> <summary>Agent "Editing Task 6 the editor" finished</summary> <note>A task-notification fires each time this agent stops with no live background children of its own. The user can send it another message and resume it, so the same task-id may notify more than once.</note> <result>That note is just confirming my own append via `cat &gt;&gt;` — expected, no issue. Fix round 1 is complete. ## Summary **Status:** done. `.amadan/ci.d/10-gate` exits 0. **Commits:** `cec952b`, `f11e68b`, `e6aa03b` on `editing`. **Bundle:** `internal/docs/static/editor.40cce03a9468a798.js`, 306378 bytes. **Debounce:** 400ms → 2000ms. `maxRetainedUpdates`=4096 rows; at 400ms that's ~27min of continuous typing to the cap, at 2000ms it's `4096×2s ≈ 136min ≈ 2h16m` — headroom, not a fix (comment at `core.go`'s cap names snapshots as the real one). **F1 testing:** factored retry/drop logic into `fixtures/save-queue.mjs`, tested under plain Node (`node --test`, wired into the gate) — confirmed the regression test fails against the pre-fix logic (2nd edit merged into the dead batch) before confirming it passes against the fix. **Save failure now shows:** a fixed-position indicator (`docs.editor.status.*` catalog keys) — quiet/hidden while retrying, sticky red "Some changes couldn't be saved. Copy anything important before leaving this page." once a batch is permanently dropped. Also found and fixed, via the coordinator's own required "&gt;1MB paste, reload" check (not in the reviewer's list): dropping a batch left the local Y.Doc diverged, silently poisoning *every* later edit (204 returned, but never actually integrated server-side — confirmed in isolation with real yjs). Fixed with `resync()`: on permanent rejection, discard the diverged doc/view and rebuild from a fresh server fetch. Live-verified: 1.4MB paste → 413 → failure shown → follow-up edit → 204 → reload → paste gone, follow-up survives.</result> <usage><subagent_tokens>331175</subagent_tokens><tool_uses>96</tool_uses><duration_ms>1113868</duration_ms></usage> </task-notification>

Human

<task-notification> <task-id>a15ae8491f693db57</task-id> <tool-use-id>toolu_01LhLCZrNxAAeGwAoCHYDq8J</tool-use-id> <output-file>/home/paulca/.cache/tmp/claude-1001/-home-paulca-amadan-net-carlos-docs/70f302d1-9d5d-4eb2-8b38-e09a15fdd1c3/tasks/a15ae8491f693db57.output</output-file> <status>completed</status> <summary>Agent "Re-review editor fix round" finished</summary> <note>A task-notification fires each time this agent stops with no live background children of its own. The user can send it another message and resume it, so the same task-id may notify more than once.</note> <result>Review written to /home/paulca/amadan.net/carlos/docs/.superpowers/sdd/2026-08-31-editing/task-6-rereview.md. Verdict: **F1 HOLDS** — gate ok, 3/3 node tests, `go test ./...` ok; live: 1.2MB paste got one 413, was never re-POSTed, next edit POSTed alone (204) and survived reload. **F2 HOLDS** — indicator appeared with exact catalog copy for both states (failed sticky across later saves; retrying cleared after a network blip recovered). **F3 HOLDS** — 4096×2s = 2h16m and 4096×400ms ≈ 27min both check out; the unload tradeoff was considered (pendingBytes covers in-flight; beacon-refusal fallback added). **Resync HOLDS on the happy path, BROKEN on its own error path** (below). In the browser (real Chromium via playwright-core against `run-dev.sh`, signed in by magic link): typed, pasted 1.2MB, saw the failed indicator, typed after, reloaded — later text survived, paste gone. Then aborted the resync GET at the drop moment: resync failed once, and everything typed afterwards got **real 204s (315B, 259B) and vanished on reload**. Findings: 1. **BLOCKING** — fixtures/editor-entry.js:175 (resync catch): "another chance the next time a batch is dropped" is false — poisoned edits 204, never 4xx, so resync is never retried; one transient failure recreates the silent-204 loss for the whole session, and the failed copy ("before leaving this page") steers the user to stay on the broken page when reload is the cure. Fix is small: retry resync from the catch / on `online`. 2. MEDIUM — render.go:130 failed copy tells the user to copy text resync already wiped from the screen (verified: paste gone from DOM before the indicator is readable). 3. MEDIUM — editor-entry.js beforeunload: keepalive fetch shares sendBeacon's 64KiB quota (verified: 100KB rejects, 30KB resolves) — the fallback cannot carry the paste-sized case its comment cites. 4. MEDIUM — core.go Apply/handlers.go updatesPost still 204 unintegratable updates; the class stays open server-side — name it in the §4.1 snapshot plan. 5. LOW — poisoned 204 rows permanently burn the 4096-row/8MB caps; LOW — resync loses cursor/focus; LOW — a full log becomes a drop→full-GET cycle every 2s. 6. NOTE — save-queue.test.mjs is real (stub matches fetch semantics), but nothing automated covers resync/sessionFailed/DOM wiring — tests pass with resync deleted. Gate, dev server, and scratch files cleaned up; tree is clean.</result> <usage><subagent_tokens>98877</subagent_tokens><tool_uses>34</tool_uses><duration_ms>550288</duration_ms></usage> </task-notification>

.amadan/ci.d/10-gate

Loading diff…

fixtures/editor-entry.js

Loading diff…

fixtures/resync-controller.mjs

Loading diff…

fixtures/resync-controller.test.mjs

Loading diff…

internal/docs/render.go

Loading diff…

internal/docs/static/{editor.40cce03a9468a798.js => editor.15280b7eedd1868f.js}

Loading diff…