Live-run synthesis fix + Console backend robustness - #8
Merged
Conversation
…teardown
`make console` already ran both localhosts (backend :8742 + Vite dev server) and
kept them alive — confirmed that's the "build both + keep running" command. Three
fixes found while verifying it:
- Auto-open the app once Vite is serving, reading the REAL url from Vite's own
"Local:" banner (Vite bumps off a busy 5173, so a hardcoded port was wrong).
- Self-sufficient: if v1/out has no discovery-*.json, render the offline golden
suite first (no key, no cost) — a first `make console` no longer dies on the
explorer's sync-data step ("cannot read v1/out") with a confusing Node error.
- Robust teardown: start the backend + dev server each in their OWN process group
and kill the group on Ctrl-C, so grandchildren (npm -> vite -> esbuild) are
reaped too instead of leaking. Cross-platform (setsid/killpg on POSIX,
CREATE_NEW_PROCESS_GROUP on Windows).
README: give the Console its own paragraph (no key needed; opens the browser;
Ctrl-C stops both). Verified end-to-end: both ports serve, opens the correct
(bumped) port, SIGINT leaves 0 stray procs; empty-out auto-generates then starts.
pyrefly clean on tasks.py; suite 248 pass.
…nd robustness
Validated by a genuinely-fresh p2p E2E run (out/ renamed aside so nothing replays
from prior output): the live agent ran, but synthesis aborted the whole suite with
"'str' object is not a mapping". Root cause: a live section emits strategy_profile
as a bare string; build.py's `{**strategy_profile}` spread chokes on a str. Golden/
cached runs never hit it because their emits are well-formed — so it only bites
real live runs (exactly the path under test).
- build.py: coerce strategy_profile to a dict at both the spread site and in
_from_payload, so a stray string is discarded (the typed StrategyProfile still
wins) instead of crashing. +2 regression tests (direct mapper + full fan-out).
Suite 250 pass, 100% branch coverage held.
Also fixes the UI "⚠ Failed to fetch": its root cause is a stale Console backend
still holding :8742, which made a new server.py crash silently (OSError: address
in use) — Vite then started, so the UI loaded with no backend to call.
- server.py: catch EADDRINUSE and exit 1 with a clear "port in use — stop the other
backend or set DISCOVERY_UI_PORT" message (no traceback); allow_reuse_address so a
just-stopped backend's TIME_WAIT socket doesn't block restart.
- tasks.py (console): poll /healthz until the backend answers BEFORE starting Vite;
if it never comes up, abort with the stop-the-stale-one hint instead of opening a
UI that can only say "Failed to fetch".
Verified end-to-end: fresh p2p run.py --fresh → full 6-report suite (4 findings);
and the Console path (POST /api/run mode=live → SSE) streams real live agent
activity. pyrefly clean on product code + tasks.py.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
Follow-up to #7 (merged). Two commits landed on the branch after that PR merged, so they're not on
mainyet. They came out of a genuinely-live p2p end-to-end test (renameout/aside so nothing replays, run fresh):TypeError: 'str' object is not a mapping.Bug 1 — live synthesis crash (the important one)
A fresh p2p run did its live discovery fine (4 findings), then synthesis crashed:
Root cause: the live agent sometimes emits
strategy_profileas a bare string instead of an object;build.pydid{**strategy_profile}, and spreading a string raises that exact error. Golden/cached runs never hit it because their emits are well-formed — so it only bites real live runs (the path under test).Fix (
build.py): coercestrategy_profileto a dict at both the spread site and in_from_payload— a stray string is discarded (the typedStrategyProfilestill wins) instead of crashing the suite. +2 regression tests (direct mapper + full fan-out).Bug 2 — the UI "⚠ Failed to fetch"
Root cause: a stale Console backend still holding port 8742 made a new
server.pycrash silently (OSError: Address already in use); Vite then started anyway, so the UI loaded with no backend to call.Fix:
server.py— catchEADDRINUSEand exit 1 with a clear "port in use — stop the other backend or setDISCOVERY_UI_PORT" message (no traceback);allow_reuse_addressso a just-stopped backend's TIME_WAIT socket doesn't block restart.tasks.py(console) — poll/healthzuntil the backend answers before starting Vite; if it never comes up, abort with the stop-the-stale-one hint instead of opening a UI that can only say "Failed to fetch". Also (frome3d7646): the console command is self-sufficient (renders the offline golden suite first ifout/is empty), opens the real Vite URL (handles a bumped port), and tears down both servers' process groups cleanly on Ctrl-C.Verified
run.py --freshp2p (without/renamed aside) → full 6-report suite, exit 0, 4 findings,strategy_profilea dict. No cache, no fixture.POST /api/run {mode:live}→ SSE) streams real live agent activity end-to-end.server.pysecond-instance on a busy port → graceful exit + clear message (no traceback).tasks.py).If "Failed to fetch" ever recurs, the one-liner is
lsof -ti tcp:8742 | xargs kill, then re-run.