Skip to content

feat(shotload): shared shot-load state machine with a visible load chip - #63

Merged
obvious-autobuild[bot] merged 2 commits into
release/task-mode-ia-redesign-20261011-170103from
feat/tmia-shotload
Oct 11, 2026
Merged

obvious-autobuild[bot] merged 2 commits into
release/task-mode-ia-redesign-20261011-170103from
feat/tmia-shotload

Conversation

@obvious-autobuild

Copy link
Copy Markdown
Contributor

Why

A cold shot load is silent: pick an uncached shot and the panels just sit there while the server fetches the bundle from FAIR-MAST. The audit found the compare flow solved this years before single-shot did — per-shot chips with queued/fetching/ready/error states, AbortController cancellation, Retry/Drop — but that machinery was private to the compare fan-out. This PR generalizes it into one shared shot-load machine and wires every shot fetch through it, closing the single-shot gap without changing the compare flow's behavior.

What

  • web/shotload.js (new) — THE one shot-load state machine (spec §2, L4): per-shot queued → fetching → ready | error, AbortController supersede by generation, honest per-shot error reasons, listener notification. A superseded fetch lands back in queued — an abort is a supersede, not a failure.
  • Single-shot loads run through the machine (web/replay.js loadShot) and render a load chip in Replay's status cluster: loading shot #30192… while fetching; on error, the reason plus working Retry and Drop. The silent cold-load is gone.
  • Compare chips read the same machine — the hand-rolled cmp.st/cmp.err/cmp.ctrls maps are deleted; cmpFetch becomes a thin payload router over the machine. Cancel returns in-flight chips to queued instead of marking them "failed: fetch cancelled", matching the spec's state table.
  • getReplay caches each promise with the signal it was created under, so a new-signal fetch never adopts an in-flight promise an older signal is about to abort; a failed fetch evicts only its own cache entry.

Scope: web/ only (new shotload.js, edits to replay.js, index.html script order, style.css chip styles). No backend changes, no build step, ES5-compatible IIFEs, tokens extended (--chip colors reuse the existing ok/warn/bad semantic tokens).

How to Review

  • Read web/shotload.js first (~90 lines) — the whole state contract is there.
  • The compare diff in replay.js is a mechanical map→machine swap; payload ownership (cmp.data, replayCache) is unchanged, so re-entering compare refetches exactly as before.
  • data-guide anchors, window.FusionLab, and fusionlab:* events are untouched (compat contract L3).
  • Known pre-existing observation (not from this diff): after a failed single load, navigating to compare keeps the stale failure line in the shot-state header until the reference resolves; the ref's renderShot wiring is byte-identical to the base (git diff cdb157d..HEAD -- web/replay.js). Flagging for the compare-workspace lane rather than patching out of scope here.

Acceptance criteria

  • Machine states queued → fetching → ready | error with supersede and honest errors (web/shotload.js)
  • Visible load chip in Replay's status cluster for single-shot loads; error chip carries reason + Retry · Drop
  • Compare chips refactored onto the same machine (state maps removed)
  • V6 evidence recorded against the tested head SHA: throttled load shows fetching → ready; failed fetch shows the error chip with working Retry (QA records tc-1, tc-2 below)
  • Compare flow smoke-test unchanged (V2 slice — QA record tc-4)
  • uv run pytest -q green (352 passed, 1 skipped), uv run ruff check clean, uv run mypy fusionlab clean (V1)
  • Touches only web/ + no lessons.json copy changes needed; stored-energy units and naming untouched

Evidence (QA records against head 946687b8bbe6321b80a7e997868ded604c056369)

Recorded via obvious autobuild upload (15 s /replay throttle proxy; uncached-shot failure is shot #12017's real FAIR-MAST 502):

  • tc-1 — V6 fetching → ready (fl_xVRldRRr, fl_sS18tvvd): status cluster chip "loading shot #30192…" while panels keep last-valid data; then the chip is gone with #30192 rendered.
  • tc-2 — V6 error + Retry + Drop (fl_aCWLHiPh, fl_2wMHy7C2, fl_rE9smba0, fl_k4MKxGpp): error chip "couldn't load #12017 — shot 12017 could not be fetched from FAIR-MAST" with Retry · Drop; Retry restarts "loading shot #12017…" and honestly re-errors; Drop clears the chip and restores the picker.
  • tc-4 — V2 slice compare smoke (fl_IOhwFA7E, fl_rkTA1DqG): deep-linked compare boots both chips "fetching from the archive…", then both ready with per-shot traces and the ΔW strip.

CI is the final gate; pytest re-runs via the pre-push hook.

Sibling PRs (Direction C release lane)

Lane PR Status
Router (prereq) #58 ✅ merged (cdb157d)
Explore workspace #60 ✅ merged (1b752ec)
Lane A: shot-load machine (this PR) this PR 🔄 ready for review
Lane A: edit drawer TBD not opened
Lane A: compare workspace migration TBD not opened
Lane B: command palette TBD in progress by sibling worker
Promotion to main #50 (draft) after the parity audit

Human author: Tapan Ganatma Nakkina ([email protected])

🔗 Obvious Project · 🧵 Obvious Thread · 🔀 Obvious PR Artifact

ObviousApp and others added 2 commits October 11, 2026 20:05
Generalize the compare chip fetch machine into web/shotload.js as THE one
shot-load lifecycle (spec L4): queued -> fetching -> ready | error per shot,
AbortController supersede by generation, honest per-shot errors. A superseded
fetch lands back in 'queued' — an abort is not a failure.

Single-shot loading now runs through the machine and renders its states as a
load chip in Replay's status cluster ("loading shot #30192..."; on error the
reason plus Retry/Drop), closing the silent cold-load gap the audit found.
getReplay stores each cached promise with the signal it was created under so a
new-signal fetch never adopts an in-flight promise an older signal is about to
abort, and a failed fetch only evicts its own cache entry.

Co-authored-by: Tapan Ganatma Nakkina <[email protected]>
…achine

The compare fan-out's hand-rolled state Maps (cmp.st/cmp.err/cmp.ctrls) are
gone: per-shot fetch state now lives in web/shotload.js, the same machine a
single pick uses. cmpFetch becomes a thin payload router — supersede and honest
errors are the machine's to record — and Cancel returns in-flight chips to
'queued' instead of marking them 'failed: fetch cancelled', matching the spec's
state table (an abort is a supersede, not a failure). Selection changes drop
the previous batch's machine state, so re-entering compare refetches exactly
as before; payload ownership (cmp.data, replayCache) is unchanged.

Co-authored-by: Tapan Ganatma Nakkina <[email protected]>
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.

2 participants