Skip to content

fix(orchestration): validate state.json before it reaches the renderer; add error boundary - #93

Merged
amirlehmam merged 2 commits into
amirlehmam:masterfrom
tawman:feature/wmux-orch-state-crash
Jul 13, 2026
Merged

fix(orchestration): validate state.json before it reaches the renderer; add error boundary#93
amirlehmam merged 2 commits into
amirlehmam:masterfrom
tawman:feature/wmux-orch-state-crash

Conversation

@tawman

@tawman tawman commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

Fixes #92 — a malformed wmux-orch-*/state.json blanks the entire window.

The bug

orchestration-watcher.ts reads state.json with a cast, not a check:

const parsed = JSON.parse(raw) as OrchestrationState;   // no validation
return parsed;                                          // → broadcast to the renderer every 1s

state.json is written by a separate process (the plugin, or an agent hand-rolling it), so it is untrusted input. A run written with "orchestrationId" instead of "id" reaches OrchestrationPanel, which does orch.id.replace(/^orch-/, "") and throws. With no error boundary anywhere in the renderer, React unmounts the whole tree and the window is left painted in its #1a1a1a background — looks like a hang, cleared only by a restart. And since the watcher prefers status: "running", one stale file reproduces it on every launch.

Repro and full analysis in #92.

The fix

  • orchestration-watcher — shape-check id / status / waves before broadcasting; ignore anything that fails, warning once per directory rather than once per 1s poll tick.
  • ErrorBoundary (new) — wraps the root as a last-resort net, and the orchestration panel specifically (silent, so a bad run just hides the panel). This kills the bug class: no single data-driven panel can blank the window again. Reusable for the other panels that render data wmux does not own (session snapshots, agent metadata).
  • OrchestrationPanel — bail out on a run with no id/waves instead of throwing.

Verification

  • New tests/unit/orchestration-watcher.test.ts pins the exact orchestrationId-instead-of-id shape, plus missing/empty/non-string id, missing status, non-array waves, and malformed waves.
  • npm test → 197 passing (26 files). tsc clean. No new lint problems (35 on master, 35 on this branch).
  • Verified live in a packaged build: planting the malformed state.json above with status: "running" no longer produces a console error and the window stays up; a well-formed run still renders in the sidebar as before.

tawman added 2 commits July 12, 2026 01:52
…r; add error boundary

A malformed wmux-orch-*/state.json blanked the entire window. The watcher
read the file with `JSON.parse(raw) as OrchestrationState` — a cast, not a
check — and broadcast it to the renderer every second. A run written with
`orchestrationId` instead of `id` therefore reached OrchestrationPanel, which
does `orch.id.replace(/^orch-/, '')` and threw. With no error boundary
anywhere in the tree, React unmounted the whole app, leaving a window painted
in the background colour that only a restart cleared. The watcher prefers
runs with status "running", so a single stale file reproduced it on every
launch.

state.json is written by a separate process, so treat it as untrusted:

- orchestration-watcher: shape-check id/status/waves before broadcasting;
  ignore (and warn once per dir) anything that fails.
- ErrorBoundary: new component; wraps the root as a last-resort net and the
  orchestration panel so a bad payload degrades to a broken panel, not a
  dead window.
- OrchestrationPanel: bail out on a run with no id/waves rather than throw.
…ot 495520:02:09

parseIso() returns 0 for a missing or unparseable startedAt, and the panel then
rendered `now - 0` — the entire Unix epoch — as the elapsed time. A run ten
minutes old displayed "495520:02:09" (~56 years).

An unknown start time is not a very old run. Render an em dash instead.

The plugin's schema names the field `startedAt` (skills/orchestrate/SKILL.md,
spawn-agents.sh, json-tool.js); a run that writes some other key for its start
time lands in exactly this path.
@tawman tawman closed this Jul 12, 2026
@tawman
tawman deleted the feature/wmux-orch-state-crash branch July 12, 2026 16:36
@tawman
tawman restored the feature/wmux-orch-state-crash branch July 12, 2026 17:04
@tawman tawman reopened this Jul 12, 2026
@amirlehmam
amirlehmam merged commit e39e7ae into amirlehmam:master Jul 13, 2026
@amirlehmam

Copy link
Copy Markdown
Owner

Merged — thanks for another surgical fix. The analysis in #92 was spot on: the watcher trusting a cross-process file and the renderer having no error boundary were two separate holes, and this closes both. The silent boundary around the orchestration panel + the root-level net is exactly the right layering, and pinning the orchestrationId-instead-of-id shape in the regression test means this exact failure can't come back. Also appreciated the drive-by fix on the epoch-elapsed rendering ("495520:02:09"). Ships in the next release.

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.

Malformed wmux-orch-*/state.json blanks the entire window (uncaught TypeError unmounts the React tree)

2 participants