Skip to content

fix: Make first-click project opens fast, honest, and recoverable - #427

Draft
Yona-Appletree wants to merge 7 commits into
mainfrom
claude/priceless-knuth-a57204
Draft

fix: Make first-click project opens fast, honest, and recoverable#427
Yona-Appletree wants to merge 7 commits into
mainfrom
claude/priceless-knuth-a57204

Conversation

@Yona-Appletree

@Yona-Appletree Yona-Appletree commented Aug 15, 2026

Copy link
Copy Markdown
Collaborator

Plan: lp2025/2026-08-14-1859-first-click-open-resilience
Path: ~/.photomancer/planning/lp2025/2026-08-14-1859-first-click-open-resilience/plan.md

Fixes the demo-day first-click open failures (diagnosed 2026-08-14): two
compounding root causes — (A) cloud sync holding the per-project Web Lock
across network I/O plus a post-acquire lock leak on failed opens
("project is open in another tab" with one tab), and (B) a flat 5 s
worker-boot timeout that includes the fw-browser wasm fetch, with no
recovery (terminal Dead preview pool, zombie workers, eternal skeleton).

Product principle: user clicks act as quickly as possible; every wait
state is visible, honest, and recoverable. Newest click wins.

Phases

  • P3 — boot hardening: Drop-terminate zombies, sim retry ladder, Dead-pool cooldown recovery (ac4dde1)
  • P5 — boot protocol v2: shared engine Module, phase statuses, inactivity timeout + ADR (888e3de)
  • P1 — lock hardening: OpenReceipt release-on-failure, retried acquire + 3 defect entries (7f60c29)
  • P4 — boot scheduling: engine preload→Module cache, staggered pool boots, user-open priority, hover deferral (f29640b)
  • P2 — sync publishes outside the project lock; ADR amended, defect closed (9d67896)
  • P6 — opening frame real states + Retry, second-click supersede, 6 stories (7a09441)
  • Gate G1 (visual/feel) — throttled cold-load states, forced-failure Retry, supersede feel, pool recovery
  • P7 — cleanup, docs sweep, full validation (after G1)

Stories note: six new project-opening-frame stories have no baselines yet
— expect a validate-stories auto-commit; merge the bot head after.

Resume notes: read plan.md, then phase files (each carries an
Implementation Result). ADRs: boot-protocol-v2 (new), per-project
locking (amended). Defects: 2 fixed-in-PR, 1 open-with-halves-cited.

🤖 Generated with Claude Code

Yona-Appletree and others added 7 commits August 14, 2026 19:48
…pool recovery

A failed browser-worker boot now terminates its Worker via Drop (the
forget()ed closures made abandonment invisible — the zombie kept
fetching the multi-MB wasm against every retry). The sim connect branch
gets a bounded retry ladder (3 × 400 ms backoff) mirroring the hardware
ladder. A Dead preview-pool worker revives lazily after cooldown with a
3-attempt exponential budget (2s/8s/30s) instead of poisoning the pool
for the page lifetime. Boot-loop magic numbers are now named constants
(same 5 s policy; the P5 inactivity rework lands on this seam).

Plan: lp2025/2026-08-14-1859-first-click-open-resilience (P3)

Co-Authored-By: Claude Fable 5 <[email protected]>
…inactivity timeout

The page now fetches + compiles the fw-browser wasm once (engine_cache:
streaming fetch with byte progress, WebAssembly.compile, thread-local
Module cache) and posts the compiled Module to every booting worker by
structured clone; workers only instantiate. The worker posts a status
per boot phase (booting → instantiating → gpu-init → runtime-create →
ready) and the host timeout is now inactivity-based (BootWaitClock,
pure + native-tested): 20 s of silence per phase, 120 s for the
path-fallback instantiate that contains the worker-side fetch. A slow
network shows progress instead of dying at a flat 5 s; a dead worker
still fails in seconds. engine_asset_phase() exposes fetch progress for
the opening-frame UI (P6); warm_engine_cache() is the page-load warm-up
seam (P4).

ADR: docs/adr/2026-08-14-browser-worker-boot-protocol-v2.md

Plan: lp2025/2026-08-14-1859-first-click-open-resilience (P5, protocol half)

Co-Authored-By: Claude Fable 5 <[email protected]>
…d acquire, sync-registration order

A failed open no longer leaks the project's Web Lock for the page
lifetime: OpenedProject now carries an RAII OpenReceipt, committed only
once the project is active — every earlier failure (load, migrate,
read, protocol/link error) routes to OpenRegistry::release_open, the
single named teardown close_project also delegates to. The open path
acquires with a bounded ladder (10 × 50 ms) via try_acquire_polling
instead of one ifAvailable shot, and mount_for_sync registers in
'syncing' before awaiting the lock (with insert-owner tracking so a
racing visitor pull cannot clear the winner's registration). uid parse
moved ahead of acquisition, removing the second leak site.

Three defect entries land with the fix: sync-holds-the-project-lock
(open; P2 owns the hold), post-acquire-open-failure-leaks-the-project-
lock (fixed here), worker-boot-timeout-races-the-wasm-fetch (open;
P3/P5 own the halves).

Plan: lp2025/2026-08-14-1859-first-click-open-resilience (P1)

Co-Authored-By: Claude Fable 5 <[email protected]>
… user-open priority

The page warms the engine assets at load: the wasm through the boot-v2
engine cache (one streaming fetch, one compile, Module shared with every
worker) and the glue JS into the HTTP cache. The preview pool now boots
one member at a time from a Pending state (pool_may_start_boot /
choose_boot_start in slot_policy, natively tested) instead of storming
every fetch at construction, and Dead-worker revival respects the same
gate. A user-initiated open raises app::open_priority (RAII counter
guard set beside pending_open): while it is up, the pool starts no new
boots and no new lease starts, and hover-to-play drops its lease
request instead of contending with the click. Everything resumes by
itself when the open settles.

Also formats device_controller.rs (committed unformatted in ac4dde1).

Plan: lp2025/2026-08-14-1859-first-click-open-resilience (P4 + P5 preload wiring)

Co-Authored-By: Claude Fable 5 <[email protected]>
The cloud sync trip now holds lp-project:<uid> only to snapshot the
package and history locally (SyncHold: lock + syncing registration as
one unit, register-before-acquire preserved, 20×50 ms acquire ladder),
releases it, and publishes from the snapshot. Post-trip bookkeeping
reacquires briefly for local writes; a project opened mid-publish gets
the snapshot's pending writes replayed into its live store instead
(LpFsOpfs::pending_writes). await_sync_handoff demotes 3 s → 500 ms
residual guard. The sync queue's request/take_due/finish already
modeled one-attempt-against-a-snapshot; its mid-flight test now names
D1 as its load-bearing reason.

Locks guard local OPFS consistency only, never network I/O — ADR
2026-07-08 amended; lpa-fs-opfs README's stale page-wide-lock story
rewritten; sync-holds-the-lock defect closed.

Plan: lp2025/2026-08-14-1859-first-click-open-resilience (P2)

Co-Authored-By: Claude Fable 5 <[email protected]>
…click supersede (D4)

The opening frame now narrates the actual pipeline: downloading engine
(byte progress from the P5 cache), starting engine (worker boot phase,
new worker_boot_phase read surface), preparing project, waiting for the
project lock (new projects_awaiting_lock read surface), and a Failed
state with a working Retry + back-to-Explore — no eternal skeleton
remains. Labels debounce ~150 ms so fast opens show only the calm
skeleton; example-open failures surface on Explore via the same notice.

Second click supersedes (D4): CommandSender::send bumps an open
generation the parked open observes at three await boundaries, the
newest click wins, and a superseded open abandons its OpenReceipt (P1)
— lock, registration, flushers all released. The booted sim worker is
reused, never torn down, so the newest click is also the fastest.
Same-batch double-clicks coalesce (pure opens only; create-and-open is
never dropped). Cards dim but stay clickable while an open runs.

Stories: six opening-frame states posed via explicit state prop
(baselines to follow from validate-stories). Also formats P2's
library_host_opfs drift.

Plan: lp2025/2026-08-14-1859-first-click-open-resilience (P6)

Co-Authored-By: Claude Fable 5 <[email protected]>
@github-actions

Copy link
Copy Markdown
Contributor

CI refreshed the story baselines on this branch

The validate-stories job detected drift and committed the fresh set: 3 changed, 15 added
in c3ff03f.

Review every PNG in the PR's Files changed view (swipe / onion-skin).
Your local branch is now behind — git pull before pushing again.

Story Before After
studio__home__home-gallery__opening-a-project__lg.png studio__home__home-gallery__opening-a-project__lg.png@7a094418bb studio__home__home-gallery__opening-a-project__lg.png@c3ff03f21e
studio__home__home-gallery__opening-a-project__md.png studio__home__home-gallery__opening-a-project__md.png@7a094418bb studio__home__home-gallery__opening-a-project__md.png@c3ff03f21e
studio__home__home-gallery__opening-a-project__sm.png studio__home__home-gallery__opening-a-project__sm.png@7a094418bb studio__home__home-gallery__opening-a-project__sm.png@c3ff03f21e
studio__home__project-opening-frame__downloading-engine__lg.png studio__home__project-opening-frame__downloading-engine__lg.png@c3ff03f21e
studio__home__project-opening-frame__downloading-engine__md.png studio__home__project-opening-frame__downloading-engine__md.png@c3ff03f21e
studio__home__project-opening-frame__downloading-engine__sm.png studio__home__project-opening-frame__downloading-engine__sm.png@c3ff03f21e
studio__home__project-opening-frame__failed__lg.png studio__home__project-opening-frame__failed__lg.png@c3ff03f21e
studio__home__project-opening-frame__failed__md.png studio__home__project-opening-frame__failed__md.png@c3ff03f21e
10 more file(s)
  • studio__home__project-opening-frame__failed__sm.png (added)
  • studio__home__project-opening-frame__preparing-project__lg.png (added)
  • studio__home__project-opening-frame__preparing-project__md.png (added)
  • studio__home__project-opening-frame__preparing-project__sm.png (added)
  • studio__home__project-opening-frame__starting-engine__lg.png (added)
  • studio__home__project-opening-frame__starting-engine__md.png (added)
  • studio__home__project-opening-frame__starting-engine__sm.png (added)
  • studio__home__project-opening-frame__waiting-for-sync__lg.png (added)
  • studio__home__project-opening-frame__waiting-for-sync__md.png (added)
  • studio__home__project-opening-frame__waiting-for-sync__sm.png (added)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant