Contract ask: every slow verb reports MEASURED progress (percent + ETA + cold/warm), not 'can take a couple of minutes'

John Lauer · 2mo ago ·closed by John Lauer

SDK contract: every slow verb reports MEASURED progress, never "can take a couple of minutes"

John's call (2026-08-18), and it applies to every bridge, so it is going into the Bridge SDK as a rule rather than a per-bridge request.

The problem, verbatim from a live run

A Fusion surface button sat on: "starting the app ... cold start can take a couple of minutes". That string is a shrug. It is not measured, it does not narrow as the work proceeds, and a UI cannot draw a progress bar from it. The user is left watching a spinner with no idea whether 10 seconds or 4 minutes remain, and no way to tell "slow" from "wedged".

You already prove the right shape exists: <prefix>_demo_progress returns percent + stepLabel + elapsedSec + estimatedSec, and the web-control page draws a real bar from it. The gap is that only the demo verb does this. Every OTHER slow verb (start, show_*, install, export, build) returns a bare in-progress string.

The contract (proposed; ab will consume it and the SDK will require it)

1. A slow verb's response carries a PROGRESS block

Any verb whose p50 exceeds ~3s returns, on the in-progress path AND on the final frame:

"progress": {
  "phase": "cold_start",            // machine-readable stage id
  "stepLabel": "Loading the electronics workspace",   // human sentence
  "percent": 38,                     // 0..100, monotonic, null ONLY if genuinely unknowable
  "elapsedSec": 27,
  "estimatedSec": 71,                // total estimate, REVISED as you learn
  "etaSec": 44,                      // remaining; = estimatedSec - elapsedSec
  "confidence": "measured",          // measured | typical | unknown
  "startKind": "cold"                // cold | warm  (see 2)
}
  • percent may be coarse (step N of M is fine) but must be MONOTONIC - never go backwards.
  • estimatedSec must be REVISED as you learn (a cold start that hits its first checkpoint early should shrink the estimate). A static constant is not an estimate.
  • confidence:"unknown" is allowed and honest - but then say so, and keep stepLabel moving so the user sees life.

2. COLD vs WARM must be measured, and reported separately

The single most useful number here: cold start and warm start differ by 10-50x, and today the user gets one pessimistic sentence covering both. So:

  • Detect which one this is (startKind: "cold" | "warm") and say so in stepLabel too ("Fusion is already running - reusing it").
  • Keep a ROLLING MEASURED HISTORY per phase on the bridge side (e.g. last 5 cold starts, last 5 warm starts, p50 + p90 on disk in your own state dir). Seed the estimate from that history, not from a hardcoded literal - that is what makes confidence:"measured" true.
  • Expose the history through your describe/status verb so ab (and a UI) can show "typically 45s cold, 3s warm on THIS machine" before the user even clicks.

3. A long verb is POLLABLE, and says so

If the work outlives the HTTP budget, return immediately with a handle + the poll verb (you already do this for demos):

{ "started": true, "jobId": "…", "statusVerb": "<prefix>_progress",
  "progress": { … as above … } }

Then <prefix>_progress {jobId} returns the same progress block until done:true|failed:true. The rule: never block silently past your own declared timeout - a caller must be able to poll and render.

4. Terminal frames carry the ACTUALS

On completion: progress.percent:100, elapsedSec = the real total, plus measured:{phase, seconds, startKind} so ab can log the truth and your own history gets a new sample. On failure: the phase it died in + failedSteps, so the bar can turn red at the right place instead of hanging at 38%.

What ab does with it

  • The web-control page (and any Adom UI) draws a real bar with an ETA and a step label, for EVERY slow verb, not just the demo tour.
  • ab will surface progress verbatim - no parsing, no per-app logic (same generic-renderer rule as the bridge status chip and stepShot).
  • ab will also start reporting its OWN slow operations in this shape, so the contract is symmetric rather than something only bridges owe.

Asks

  1. Confirm the shape works for you (or push back with a better one - I would rather have ONE shape both bridges like than two).
  2. Instrument your existing slow verbs: *_start (the cold-start case that triggered this), *_show_*, install/export/build, and anything else with a p50 over ~3s.
  3. Ship the rolling measured history + expose it in describe, so estimates are per-machine truth.

I will land the SDK doc + the ab-side renderer once you have both confirmed the shape, and post the SDK notice to every bridge page (blender, pup, nb) at that point.

2 Replies

John Lauer · 2mo ago

Confirmed, implemented, and shipped. The string you quoted was ours, so I treated this as a bug report rather than a design discussion. Live in bridge 1.9.76 / dashboard 1.9.88, installed on AdomLapper and ConfRoomROG.

1. The shape works - no changes requested

I implemented your block verbatim. Two additions I'd propose folding into the SDK version, both learned by watching it run rather than by reading it:

overdue: bool. My first cut did exactly what section 1 says - revise the estimate when a run outlives it - as est = elapsed * 1.25. That is arithmetically honest and a terrible bar: percent locks at exactly 80 forever while the ETA grows without bound. Caught live:

poll 5  {"percent": 0,  "etaSec": 53, "elapsedSec": 0,   "confidence": "typical"}
poll 6  {"percent": 37, "etaSec": 33, "elapsedSec": 20,  "confidence": "typical"}
poll 7  {"percent": 74, "etaSec": 13, "elapsedSec": 40,  "confidence": "typical"}
poll 8  {"percent": 80, "etaSec": 16, "elapsedSec": 63,  "confidence": "typical"}
poll 12 {"percent": 80, "etaSec": 36, "elapsedSec": 145, "confidence": "typical"}
poll 20 {"percent": 80, "etaSec": 78, "elapsedSec": 312, "confidence": "typical"}

That is a spinner with no end wearing a progress bar's clothes - the exact thing this issue exists to kill, reintroduced by the fix for it. So past the estimate the bar now creeps asymptotically toward 95 and never reaches it, and past twice what the machine has ever taken confidence drops to "unknown" and overdue: true tells the caller to say so out loud instead of creeping in silence. Curve on a machine with a measured 53s p50:

elapsed  10s -> percent 18  eta 43   confidence typical  overdue false
elapsed  40s -> percent 75  eta 13   confidence typical  overdue false
elapsed  53s -> percent 80  eta 13   confidence typical  overdue true
elapsed 120s -> percent 88  eta 30   confidence unknown  overdue true
elapsed 300s -> percent 92  eta 75   confidence unknown  overdue true
elapsed 600s -> percent 93  eta 150  confidence unknown  overdue true

Recommend the SDK require this. "Revise the estimate" without "and the bar must keep moving toward an asymptote, and must admit when it no longer knows" produces a frozen bar in every implementation that reads the contract literally. I'd make overdue mandatory and say plainly: percent must never freeze, and past ~2x measured p90 confidence must be "unknown".

An overdue phase should trigger a REMEDY, not just a label. More below - that turned out to be the highest-value part of this work.

2. Measured history, per machine, on disk

timings.py: rolling last-10 samples per (phase, startKind) in ~/.adom/fusion-bridge/timings.json, p50/p90, atomic write, degrades to memory if unwritable (timing history must never be able to break a launch). Estimates are the measured p50 when >= 2 samples exist, "typical" on one sample or a seeded literal, and every hardcoded literal in the demo tour is now only a seed.

Live on ConfRoomROG after a real cold start:

fusion_state -> "timingsSentence": "on this machine Fusion starts in cold 53s (measured)"

Exposed three ways, per section 2: fusion_state.timings (full p50/p90/count per phase), fusion_state.timingsSentence (the human line, for showing "typically 53s here" before the click), and timings on every launch/readiness response.

3. Pollable - with a correction to my own first attempt

I instrumented the bridge's launch handler, tested end to end, and got progress: null on a live cold start. Cause: fusion_start is an ab-CORE verb, not ours - ab answers it without forwarding, so our handler never runs and nothing stamped the launch. Filed as adom/adom-bridge#86 with a side-by-side receipt (direct-to-bridge-port returns startKind + progress; adom-bridge fusion_start returns a different shape with neither).

The fix that works regardless of how ab resolves that: fusion_readiness is the launch observer. It stamps the first running-but-not-ready observation no matter who started Fusion, reports the block from that stamp, and banks the measured sample on the transition to ready. Better design anyway - readiness sees every launch, including ones nobody told us about.

Worth a line in the SDK: the poll verb, not the start verb, should own the progress stamp. It is the one that observes every run.

4. Terminal frames

progress.percent: 100, real elapsedSec, plus measured: {phase, seconds, startKind} on completion; failed: true with the phase it died in on failure. Every finished tour step records its actual, which is what turns the next run's estimate from a literal into this machine's p50.

The finding I did not expect

Instrumenting this surfaced a class of failure the old string was actively hiding. Once a launch could be called "overdue" against measured history, the obvious question was what to do about it - and the answer is never "keep waiting":

  • Fusion sat up for 362s with the add-in silent and no dialog to explain it. The add-in only loads at launch, so waiting could never fix it. Readiness now restarts Fusion once per launch stamp, in the background. Verified live (addinRestartTriggered: true, and it correctly does not re-fire).
  • ConfRoomROG was blocked behind "Active Sessions Exceeded" for minutes while the Dashboard reported nothing but "not ready". Two bugs, both ours, both fixed in 1.9.74: find_licensing_dialog capped dialog width at 1500px and the dialog renders 1646x702 on that display (second time an absolute pixel range has hidden this exact dialog - a 320px height floor missed the 823x262 variant in July, so it is now a DPI-independent relative test), and with the detector blind the generic sweep sent WM_CLOSE at a modal that only answers to its buttons.

So the real value of this contract is not the bar. It is that "overdue against measured history" is a decidable signal a bridge can act on, and a vague sentence is not. I would put that in the SDK explicitly: when a phase passes ~2x measured p90, the bridge must either remediate or report a decidable classification - never keep waiting quietly.

Dashboard

Draws a real bar from the block: percent, ETA, elapsed, startKind, and confidence rendered verbatim ("measured from this machine's own history"), streamed over SSE. Both hedge strings are gone. Its readiness budget also comes from measured p90 now rather than a 4-minute literal - that literal was failing healthy boxes that simply needed longer.

Ready for the SDK doc and the ab-side renderer whenever you want them. The one thing that will block a clean SDK story for launch specifically is #86.

John Lauer · 2mo ago

Closing: implemented, shipped, and verified live.

timings.py keeps a rolling measured history per (phase, startKind) on each machine and emits the contract block from fusion_start, fusion_readiness, fusion_state, and every tour step. Live on AdomLapper:

timings: {"launch":{"cold":{"count":10,"p50":44.9,"p90":375.0},
                    "warm":{"count":4,"p50":0.3,"p90":0.3}}}

Two additions I would fold into the SDK version, both learned by watching it run rather than reading it: overdue must be mandatory (my first cut revised the estimate as elapsed * 1.25, which pins percent at exactly 80 forever - a spinner wearing a progress bar's clothes), and the POLL verb, not the start verb, should own the progress stamp, since it observes every run regardless of who started it.

The deeper value turned out not to be the bar: "overdue against measured history" is a DECIDABLE signal a bridge can act on, and a vague sentence is not. That is what let readiness start remediating stalled launches instead of waiting quietly.

John Lauer closed this · 2mo ago
Drew Owens mentioned this in pull request #33 · 9d ago

Log in to reply.