Skip to content

feat(orchestration): Control Panel — live coordinator DAG in Mission Control (O1 #6 + O2 #7) - #22

Merged
zaridan merged 4 commits into
mainfrom
zaridan/feat-orch-6-control-panel
Jun 24, 2026
Merged

feat(orchestration): Control Panel — live coordinator DAG in Mission Control (O1 #6 + O2 #7)#22
zaridan merged 4 commits into
mainfrom
zaridan/feat-orch-6-control-panel

Conversation

@zaridan

@zaridan zaridan commented Jun 24, 2026

Copy link
Copy Markdown
Owner

Part of #5. Implements O1 (#6) and O2 (#7): make the coordinator's live task DAG visible in Mission Control. Behind experimentalOrchestrators; additive; no regression to the existing aggregate-count path or the shipped-log section.

O1 — sync the per-run task DAG + dispatch detail to the renderer

Previously only three aggregate ints crossed to the renderer (OrchestrationActivity). Now the main→renderer graph sync also carries, per running coordinator, keyed by coordinator paneKey, run-scoped per F1 (#12):

  • Tasks{ id, status, deps, title (display_name ?? task_title ?? first spec line), targetKey } from listTasksWithDispatch({ coordinatorRunId }).
  • Dispatch per task{ assigneeHandle, assigneeAgent, status, lastHeartbeatAt, stale }. assigneeAgent is resolved in main from the worker PTY's launch agent; stale is computed in main (it owns the clock + hung threshold) so the renderer stays pure.
  • Latest worker signal per dispatch — most recent heartbeat phase or worker_done summary from the messages table (addressed to the run's coordinator handle, so it stays run-scoped without a run column on messages).

listTasksWithDispatch now also surfaces the active dispatch's status + last_heartbeat_at (aliased to avoid colliding with the task's own status); the taskList RPC strips them so its shape is unchanged. A pure assembler (run-dag-snapshot.ts) maps DB rows → OrchestrationRunDag, caps the payload (200 tasks, reports the truncated count, console.warn on truncation), and indexes the latest signal per dispatch. A new orchestration-run-dag store slice reuses the equality-check pattern from orchestration-activity to avoid re-render churn on high-frequency title ticks.

O2 — rewire Mission Control to DAG-backed + render the live Control Panel

OrchestratorMissionControl now renders the coordinator's DAG using Orca's existing visual vocabularyAgentStateDot for status and the shared agent glyphs (AgentIcon) for who's working. No new status visuals invented.

Field mapping:

UI Source
Coordinator dot deriveOrcastratorDotState (supervising/stalled)
Counts line OrchestrationActivity (N tasks · M workers · K stalled)
recipe line OrchestrationRunDag.recipe (absent for LLM directors)
Task row dot task status → idle (pending/ready) · working (dispatched) · stalled (stale heartbeat) · blocked · done (completed) · interrupted (failed)
Task row icon dispatch assigneeAgent → agent glyph
Task row message latest heartbeat phase / worker_done summary / "waiting on <dep>" for unmet deps
Shipped section existing .orcastrate/log.jsonl parse (unchanged)

Mode-agnostic + fallback

The same panel renders the current LLM director and a future recipe director — both emit the same CoordinatorRun + tasks + dispatches. When a director has no coordinator run yet (e.g. an LLM director that hasn't called orchestration.run), the existing lineage "Spawned work" + Shipped view renders unchanged, so nothing regresses.

Constraints

STYLEGUIDE tokens + shadcn + AgentStateDot; cross-platform; SSH/remote (sync already crosses that boundary). AGENTS.md: no max-lines disable (row/section/summary split into focused components), .ts over .d.ts, comments document the why, concrete file names. New UI strings synced across all locale catalogs.

Test evidence

  • Sync shape + per-run scoping: run-dag-snapshot.test.ts (stale derivation, signal latest-wins for heartbeat phase / worker_done summary, deps parsing, task cap + truncation, recipe passthrough); db.test.ts (new dispatch columns surfaced; listTasksWithDispatch({ coordinatorRunId }) scoping — two runs don't bleed); orchestration-run-dag.test.ts (store equality avoids churn, replaces on change); orchestrator-mission-control-data.test.ts (selector picks the director's run by paneKey tab id; concurrent runs don't bleed).
  • Render: orchestrator-task-row.test.ts (status→AgentStateDot mapping for each status incl. working vs stalled; message derivation); MissionControlTaskRow.test.tsx (correct dot/glyph/message per status); MissionControlTasksSection.test.tsx (row per task, empty state, truncation note); OrchestratorMissionControl.dag.test.tsx (renders DAG + counts when a run exists; falls back to lineage view when none; Shipped still renders).
  • Full vitest green (the only failures seen were pre-existing flaky native-PTY/git-relay suites that pass in isolation, plus pre-existing TuiAgent typecheck errors in orca-runtime.test.ts — both unrelated to this change). typecheck (node/web/cli), electron-vite build, and oxlint all green; localization catalog parity verified.

🤖 Generated with Claude Code

zaridan and others added 2 commits June 23, 2026 19:20
…enderer (O1, #6)

Today only three aggregate ints reach the renderer via OrchestrationActivity.
Extend the main→renderer graph sync to carry, per running coordinator and keyed
by coordinator paneKey, the run's live task DAG: each task's status + deps +
title + target_key, its active dispatch (assignee handle/agent, status, last
heartbeat, computed stale flag), and the latest worker signal (most recent
heartbeat phase or worker_done summary from the messages table).

- listTasksWithDispatch now also surfaces the active dispatch status +
  last_heartbeat_at (aliased to avoid colliding with the task's own status);
  the taskList RPC strips them so its shape is unchanged.
- New pure assembler (run-dag-snapshot.ts) maps DB rows → OrchestrationRunDag,
  bounds the payload (task cap + truncation count), and indexes the latest
  worker signal per dispatch. Run-scoped per #12 so concurrent runs don't bleed.
- buildOrchestrationRunDagByPaneKey() wires DB + per-handle agent resolution
  into the assembler alongside the existing activity builder.
- New orchestration-run-dag store slice reuses the equality-check pattern from
  orchestration-activity to avoid re-render churn on high-frequency title ticks.

Behind experimentalOrchestrators; additive to the sync; mode-agnostic (LLM and
future recipe directors both emit the same shape).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ol (O2, #7)

Rewire OrchestratorMissionControl from pure-lineage to DAG-backed. When the
director has a live coordinator run, render its task DAG using Orca's existing
visual vocabulary — AgentStateDot for status, the shared agent glyphs for who is
working — instead of inventing new status visuals:

- Director dot now uses deriveOrcastratorDotState so a director that handed
  control back reads as supervising/stalled while its run is live.
- Header shows the OrchestrationActivity counts (N tasks · M workers · K
  stalled) and a recipe line when present (absent for LLM directors).
- TASKS section renders one row per task: dot from task status (pending/ready→
  idle, dispatched→working, stale heartbeat→stalled, blocked→blocked,
  completed→done, failed→interrupted), icon from the dispatch's agent, message
  from the latest heartbeat phase / worker_done summary / "waiting on <dep>".
- Pure mappers (orchestrator-task-row.ts) + per-run selectors keep the logic
  testable; row/section/summary split into focused components (line cap).

Mode-agnostic: the same panel renders LLM and future recipe directors. When a
director has no coordinator run yet, the existing lineage "Spawned work" +
Shipped sections render unchanged, so nothing regresses. Behind
experimentalOrchestrators; STYLEGUIDE tokens; new strings synced across locales.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@zaridan

zaridan commented Jun 24, 2026

Copy link
Copy Markdown
Owner Author

Review panel — not merge-ready (top findings verified against source)

3-lens panel; the load-bearing items re-verified against source by the coordinator. No blockers, but a real perf regression + a feature gap.

🟠 Must-fix

  1. Hot-path perf regression. buildOrchestrationRunDagByPaneKey() runs at orca-runtime.ts:2746 inside syncWindowGraph — the 16ms-coalesced tick that fires on ANY terminal title/status/layout churn. Per running run it does listTasksWithDispatch (multi-table LEFT JOIN) + getAllMessagesForHandle(…,500,…), replacing 3 cheap COUNT(*)s. Fix: gate recompute on a change signal (max task/dispatch rowid + message-sequence high-water mark), cache otherwise — don't rebuild every coalesced tick.
  2. Builder ignores experimentalOrchestrators. The flag gates only the renderer panel; the main-side builder runs whenever a run is active, so users with a running coordinator pay feat(orchestrators): Orcastrator — director sidebar, Send to Orcastrator, and Mission Control #1 un-opted-in and can't see the panel. Fix: gate the builder on the flag / skip when no Mission Control window is mounted.
  3. worker_done summary unreachable on completed rows. listTasksWithDispatch's join is WHERE status IN ('pending','dispatched') (db.ts:691), so a completed task has dispatch_id = null; the signal is keyed on dispatch_idderiveTaskMessage returns ''. The advertised summary never lands. Tests pass only by injecting signalsByDispatchId against a hand-built dispatch_id, bypassing the real join. Fix: source the worker signal by task_id (or a 2nd LEFT JOIN to the latest dispatch regardless of status), and fix the test to drive the real join.

🟠 Should-fix

  1. Per-task stale omits the dispatched-at grace (run-dag-snapshot.ts:502) → freshly-dispatched workers flash amber stalled while the aggregate count shows 0. Add the same dispatched_at < threshold grace as getStaleDispatches.
  2. Two different numbers both labeled "tasks" — summary shows pendingTasks (outstanding), section shows dag.tasks.length (all). Relabel summary to "active"/"outstanding".
  3. Main buildOrchestrationRunDagByPaneKey untested — the "two runs don't bleed across panes" guarantee (pane resolution + signal scoping) has no test. Add one.
  4. Hardcoded English row labels (status words + "waiting on") bypass i18n. Route through t().

Nits

Dedup listCoordinatorRuns/staleThresholdIso across the two builders; wrap the per-run loop so a throw doesn't forfeit the whole sync tick; satisfies never exhaustiveness on the dot/label mappers; empty-title → fall back to task id; omit waiting on <opaque-uuid> past the 200-task cap.

No-merge until #1#3 land; #4#7 same pass.

zaridan and others added 2 commits June 23, 2026 19:56
…er_done on completed rows (#22 round 2)

Addresses the round-2 review must-fixes on the Control Panel DAG sync:

PERF (#1): buildOrchestrationRunDagByPaneKey ran the heavy
listTasksWithDispatch + getAllMessagesForHandle(500) per run on every
16ms-coalesced syncWindowGraph tick. Now a cheap per-run change token
(getRunDagChangeToken: MAX(rowid) of the run's tasks + dispatch_contexts, and
MAX(sequence) of the coordinator handle's messages) is computed each tick; the
heavy work runs only when the token moves, otherwise the cached snapshot is
reused. The cache is pruned to the running set so it can't grow unbounded.

FLAG (#2): the builder is gated on experimentalOrchestrators (read from the
runtime store), so un-opted-in users with a running coordinator no longer pay
the cost for a panel they can't see.

worker_done (#3): listTasksWithDispatch joins only pending/dispatched
dispatches, so a completed task's row has dispatch_id = null and a
dispatch-keyed signal never landed the worker_done summary. Worker signals are
now keyed by task_id (present on both heartbeat and worker_done payloads),
which reaches completed rows. New real-join test (in-memory DB → complete a
task with a worker_done → assert the summary lands) drives the actual join and
fails against the dispatch-keyed code.

stale grace (#4): per-task stale now mirrors getStaleDispatches exactly —
requires dispatched_at past the grace AND a stale/null heartbeat — so a
freshly-dispatched worker no longer flashes amber while the aggregate count
shows 0. listTasksWithDispatch surfaces dispatched_at for this.

Tests (#6) + nits: main-side buildOrchestrationRunDagByPaneKey tests for
cross-pane no-bleed (two runs → distinct panes, task + signal scoping) and the
change-token cache (heavy queries run once while the token is stable, again
when it moves), plus the flag-off omission. Dedup the running-runs +
hung-threshold across the activity and DAG builders; wrap the per-run loop so a
malformed run can't forfeit the whole sync tick; blank title → task id.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…unts line (#22 round 2)

Round-2 review should-fixes on the renderer:

i18n (#7): the task row status words (queued/working/stalled/blocked/done/
failed) and the "waiting on <dep>" hint were hardcoded English body text. The
mappers now return stable tokens (TaskStatusLabel) / a structured TaskMessage,
and the row component routes them through translate() with new locale keys
synced across all catalogs. Worker-authored content (heartbeat phase /
worker_done summary) still renders verbatim.

counts label (#5): the supervision summary shows pendingTasks (outstanding,
not-yet-terminal) but labeled it "N tasks", conflicting with the tasks-section
header's total task count. Relabeled to "N outstanding".

nit: a "waiting on <dep>" hint is omitted (rather than rendering a raw uuid)
when the blocking dep isn't in the synced set — e.g. truncated past the
200-task cap.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@zaridan

zaridan commented Jun 24, 2026

Copy link
Copy Markdown
Owner Author

Round 2 — must-fix #1#3 + should-fix #4#7 + nits landed

Pushed two commits (6bcdce6 main, 57af8a4 renderer).

Must-fix

  1. Hot-path perfbuildOrchestrationRunDagByPaneKey no longer rebuilds every coalesced tick. A cheap per-run change token (OrchestrationDb.getRunDagChangeToken: MAX(rowid) of the run's tasks + dispatch_contexts, MAX(sequence) of the coordinator handle's messages) is computed each tick; the heavy listTasksWithDispatch + getAllMessagesForHandle(…,500,…) run only when it moves, otherwise the cached snapshot is served. Cache pruned to the running set. Test: heavy queries called once across two unchanged ticks, again when the token moves.
  2. Flag gate — the builder is gated on experimentalOrchestrators (read from the runtime store); un-opted-in users with a running coordinator pay nothing. Test: flag-off → no DAG, listTasksWithDispatch never called.
  3. worker_done on completed rows — signals are now keyed by task_id (on both heartbeat and worker_done payloads), not dispatch_id, so the summary reaches completed rows (whose join dispatch_id is null). New real-join test seeds an in-memory DB, completes a task with a worker_done, and asserts the summary lands on the completed node — it asserts dispatch_id === null as the precondition, so it fails against the dispatch-keyed code.

Should-fix

  1. Stale grace — per-task stale now mirrors getStaleDispatches exactly (requires dispatched_at past the grace AND stale/null heartbeat); listTasksWithDispatch surfaces dispatched_at. Test: freshly-dispatched + null heartbeat within grace → stale === false.
  2. Counts label — supervision summary relabeled N tasksN outstanding (it's pendingTasks, distinct from the section header's total).
  3. Main builder tested — added cross-pane no-bleed (two runs → distinct panes; task + per-handle signal scoping stay run-local) and the change-token cache test.
  4. i18n — row status words + waiting on <dep> route through translate() with new keys synced across all locales; worker-authored content (phase/summary) still renders verbatim.

Nits

Deduped listCoordinatorRuns/threshold across the two builders (collectRunningOrchestration); per-run loop wrapped in try/catch so a malformed run can't forfeit the sync tick; blank title → task id; waiting on <dep> omitted (no raw uuid) when the dep is truncated past the 200-task cap. Skipped the satisfies never default — the mappers already get compile-time exhaustiveness from their return type, and an explicit default trips oxlint's switch-exhaustiveness-check.

Verification

typecheck (node/web/cli), electron-vite build, oxlint, and localization parity all green. Full vitest green for everything touched (orchestration DB/builder/runtime + renderer). The only full-suite failures were pre-existing flaky native-PTY / git-relay / subprocess / dev-startup suites that pass in isolation — unrelated to this change. Also fixed a pre-existing missing TuiAgent import in orca-runtime.test.ts so typecheck:node is now clean.

Not merging.

@zaridan
zaridan merged commit 9d3589e into main Jun 24, 2026
1 check passed
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