Skip to content

M4-39 Run graph and control room on the desktop

No Date Ruling Reason
D-M4-39-1 2026-10-25 The labels of the time axis are run times from the start of the axis in the form of the live box (0:00, 4:00, 1:00:00); the step is the smallest of a fixed list of round durations (1 s up to 24 h) that leaves at least DsSpace.s64 + DsSpace.s16 between two labels on the track, so seconds, minutes and hours follow the length of the run and the width of the window. Design note 2; the run time reads like the duration of the live box, and a width rule keeps labels apart without a count limit in code.
D-M4-39-2 2026-10-25 Focus order: the threads the app shows keep thread_ids (most recently shown win, as before); the rest of sync.max_focused_threads goes first to the run graphs focused in the background, then to the control room cards in on-screen order (top to bottom, then start to end). A card whose thread is already in thread_ids counts as live and is not repeated in background_thread_ids. The grid measures after every layout and scroll; leaving the view frees the places. idle is never touched by the background list. Design note 4, 04 6.2, R-SY-27; the person's open threads must not lose live data to cards, and the graph a person opened outranks a card.
D-M4-39-3 2026-10-25 The run graph always focuses its run with the thread as a background thread, also when it was opened from the live box: the route shows no messages of the thread, so it must not count as "Active in the thread". EngineFocus.unfocusRun drops only the kind of focus it names, so the live box and the graph of one run hand over in either order without losing the document. 01 6.2 "follows the same focus rule", 04 6.2, 13 4.4; found by the fake server test: dropping both kinds at once lost the graph's document when the live box left the screen.
D-M4-39-4 2026-10-25 Rule 7 of 09 5.6 is read as: among the children of a node, the finished ones that neither run nor have a running descendant are the candidates; above ui.livebox_visible_steps candidates the oldest go into one row "n earlier steps" in place of the first hidden child; a child on the running path is never hidden. The person may still close any row by hand (mouse or Left). The text protects the running path from the automatic collapse; closing a row is an explicit action of the person (01 D14 "rows can be collapsed").
D-M4-39-5 2026-10-25 No new mark in lib/src/marks/: the card reuses RunCardPainter; the bar of a row is RunGraphBarPainter in lib/src/run_graph/, because BarMark draws labeled rows of several segments without a tree, an indentation or the arrow of a running bar. Brief Scope "new marks only where none fits".
D-M4-39-6 2026-10-25 The route of the run graph is /running/{run_id}?t={thread_id}, with &from=thread from the live box (15 11 gains the row). The thread is part of the path because the focus needs it before the document arrives, and the target check of a link is the thread's. 15 11 "one path per target"; the graph lives under the view it belongs to.
D-M4-39-7 2026-10-25 Changes outside the scope list of the brief, each needed by it: sync_engine sends background_thread_ids (FocusManager, EngineFocus.setBackgroundRuns, focusRun(background:); the existing focus manager knew only thread_ids) and its fake gateway serves them (run documents and run.updated, no typing, a shared limit) and lists stall_warned_at in run summaries; DsLiveBox of design_system gets the optional onGraph button in the header of the full box; apps/client depends on bothy_viz (reachable through admin_ui in tools/arch-check/rules.yaml). Requirement 13 and Design note 4 of the brief name a focus manager that sends background_thread_ids; requirement 12 a button "Graph" in the live box.
D-M4-39-8 2026-10-25 The stall notice of a card without live details comes from an in-memory map of stall_warned_at filled by every complete listActiveRuns load; the local runs table and the domain RunSummary keep no stall mark. 09 5.6: "as of the last listActiveRuns load"; a schema change of the local store for a value that run.state_changed never carries would add a migration without a reader.
D-M4-39-9 2026-10-25 The root shows the run cost under billing_mode_used, else billing_mode_expected (the live box rule of 5.4); a child shows a usage cost only when billing_mode_used is organization, always as an estimate, and only with both cost_micros and currency; tokens come only from usage.tokens. 09 5.6 rules 1 and 6, D-RV-5; a reported child cost before the mode is known is not shown.
D-M4-39-10 2026-10-25 The run fixtures are Run.flat.json, Run.subagents.json, Run.summary.json, Run.stalled.json and Run.load.json (exactly the default of run.max_steps_persisted steps); they carry step names and titles of the test world and are left out of the statistics privacy test, which holds only for level a (R-VZ-14). Brief Scope "fixtures"; 25 1 boundary of 2026-10-24.
D-M4-39-11 2026-10-25 integration_test/run_graph_test.dart is listed in platform-tests.json for macOS and Windows; it also runs headless on flutter-tester against the fake server. The brief's integration test must run in the platform jobs; the scripted agent waits at gates, so it is deterministic.
D-M4-39-12 2026-10-25 On the phone "Running now" keeps the rows of M4-23 without the switch "Cards" and "List", and the path of the run graph belongs to the tab Running; the phone has no route of the run graph. Both change with M5-14 (M11, M12). The brief builds the desktop only; found by the delta red team after the rebase onto M5-02: a card on the phone opened a path the phone does not route ("Not found or no access").