Repository navigation
cockpit v3: blocker paths and status lines, roll-ups and the base env, alarms closed with a cause (S5, S6, S7) - #210
Merged
Conversation
…, alarms closed with a cause (S5, S6, S7) S5. node_status.py computes each node's blocker path over the built tree (pure, memoized): own waits from waits.py first (the snapshot's waiting_on for a project, task_waits for a task, a ready PR's merge), ranked stall > decision > proposal > hold > merge > dependency > capacity; an ask ends the path, a dependency, another project's merge or a hold on another node hops there; otherwise it steps into the child that blocks the most (person-ending paths first, by the weight they hold, then dependents, then column); a node's own decision (or one on a proposed child) ends it when no child's does; 6 shown segments, cycle guard. Ready PRs a person merges into one base are one ask (merge:<base>). The header's line carries its label (Not started for 7 h, Held on purpose, Top blocker, Not moving), also_holds, a numbered why and a note on met links with no reason; status_compact is the end of the path. S6. Roll-ups add burn from turn spans, budget sums, an ETA only from observed task durations (graph.critical_path; a feature's slowest project per column), Try it from passing acceptance steps, and the base env on the root. The base watcher records the commit the base env runs (omnigent.factory.base_env.<key>, served as factory.base.deployed), placed in the base's history; in_base_env per change is by ancestry (base.contains). S7. A stall that ran again because its task is now held on purpose is answered by the server with the cause and the real ask; server closings record cause, real_ask, about and was on their event. Health lists today's closed alarms (closed_alarms), the node each was about gets a chip, and the root roll-up counts them (alarms_closed). An open alarm on a held task is no ask on the node pages. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…closed only with a real cause Review fixes for S5-S7: - Dependencies in merged mode are met only when the target has no open task (gating, waits, snapshot, start, guards, graph): the panel really waits on assistant-core while T-16 is open; the test fixture builds waiting_on with waits.project_waits like the snapshot does. - A path that ends at a person wins over own waits that end elsewhere; a not-started project nothing holds ends at who starts it (view.start); a long path keeps its first steps, an elision and the person; cycle paths are not memoized; waits that end elsewhere keep who must act. - base.contains counts only an exact sha until the deployed head is placed; a merge into another repo is never in the base env; smoke only from a probe. - Expiries count as closed alarms only with an explicit cause; a refuted claim's Keep waiting is superseded, not closed; settle records a concrete cause; only idle stalls of held tasks are hidden; the real ask shows its verb; the alarm read pages until it covers the window. - Burn counts open turns and reads spans newest first; a parent's budget is its children's caps only when each has one; auto-merge targets are read only on a tree rebuild. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.
Related issue
Closes # (cockpit v3 plan items S5, S6, S7;
notes/factory-v3/final/plan.md)Summary
Every node of the v3 node API (
GET …/n/{ref}) now says in one line why it is or isn't moving, ending at the person who must act. Roll-ups carry money, time and what is in the base env. Alarms the server closed show with their cause. v3 still ships behind?cockpit=v3; v1 and v2 are untouched.ELI5: each card used to show only its state ("Needs you"). Now it follows the chain of waits down and across ("T-16 → held for C-16 → needs you: Apply C-16 (D-45)"), so you can see who has to act.
Plan bullet → what this PR does
S5. Blocker path, status lines, why (
node_status.py, new)blocker_pathperapi.md§3.3, reusingwaits.py. A node's own waits come first: the snapshot'swaiting_onfor a project (waits.project_waits, already cut to the viewer),waits.task_waitsfor a task, and a ready PR's merge for a change. They are ranked stall > decision > proposal > hold > merge > dependency > capacity.→).›) into the child that blocks most. Person-ending paths rank first, by held weight (Σ estimate of the tasks the ask holds, plus the weight of projects that depend on where it sits), then by dependents, then by earliest column.start: "the server starts it within a minute" (ends: server), "waits for Atlas to start it", "starting: pulling the image", or the capacity wait "(all 12 machines in use, 3 ahead)". Without astart, the charter's spawn owner decides.…) and its end, soends: personand thedecisionstay on the line. A path that comes back to a node still being computed endscycle(cycle results are not memoized, so the answer does not depend on which node was asked first).merge:<base>(grouped,external_url, "Review and merge").status(full line):decisionis a fullDecisionRef: verb, actions from the decision's options, asker, age,expiresfrom the 72 h proposal expiry, subject, title, text.also_holds, numberedwhyandwhy_title.notenames met links that had no reason.status_compactshows only the end of the path ("Waits on X · held for C-16", "Needs you: … · via a › b", "Held for C-16: …"). A moving node showsmoving {ref, activity}. With no path, the line falls back to the state line, which now has done notes ("fix(sessions): refuse a fork that needs a copy of a missing agent bundle omnigent-ai/omnigent#9881 merged · in base env") and "queued 7 h".Dependencies are met only when the work is finished (
gating.py, used by the waits, the gate and every view of it)merged-mode dependency was met as soon as the target's PRs were merged with none open. A project that merged its first task's PR and still had open tasks (assistant-core after [Bug] A message cancelled while queued blocks every later message in its session omnigent-ai/omnigent#9876, T-16 still open) released its dependents, so the panel's "waits on assistant-core" came only from a hand-written fixture and the real snapshot had no wait for it.mergeddependency until those close or it is done.readymode is unchanged. "Start now" (S8) is how a person relaxes a dependency to the change it needs (PR-9876), perapi.md§4.1.project:<slug>dependency andwaits.py.S6. Roll-ups, results, base env fact
turnspans (cost at open and close, prorated; read newest first). A turn still open counts up to its session's cost now (the snapshot's member). A project shows its own burn; a feature or need shows the sum over its projects. The root shows the snapshot's factory rate, which also pays for leads and agents that work for no project, so it is more than the features' sum; for a viewer from whom part of the factory is hidden, it is the sum of what they may read.tasks.observed_weights→graph.critical_path). A feature's ETA is the slowest project per column, summed. Otherwise there is no ETA field at all.try_itlists passing acceptance steps on the feature and on the project that owns each step, marked "API only: not built yet" while aplan.user_facingproject is unfinished.try_readyandrollup.in_base_env.try_itare set from it.base.pyrecords the commit the base env runs on the root (omnigent.factory.base_env.<key>: sha, first-report time, and when the watcher saw it become the head).env.reported(integration project, ready).base.movedhistory, and placed later when the move lands.base.movednow carriesat.factory.base.deployedin the snapshot, and asnode.envon the root ({name, branch, sha, deployed_at, state, smoke?, app_url, api_url, note, ref}).smokeshows only when the server's probe ran.in_base_envper change by ancestry (base.contains): the deployed commit is the merge, or the watcher saw it become the head after the merge landed. A deployed commit the watcher never saw become the head (first sighting, two moves between polls, a report that raced the watcher) holds only itself until its next move or report places it; the env's first-report time is no longer used as a bound. A PR into another base, or into the same branch of another repo, is never in the base env.S7. Alarms closed with a cause
server_decisions.close_by_server(…, real_ask, real_ask_verb, about)records{cause, real_ask?, real_ask_verb?, about?, was}on the closing event (closed_data).expire_decisionrecords a cause only when given one (why); the generic "the situation cleared" is now only the summary's wording and no cause.expire_decision(…, superseded_by=D-n)records that a newer alarm replaced it.loop_escalation.settle: a stall that ran again because its task is now held on purpose is answered by the server with "T-1 is held on purpose for C-n; a held task is not stalled. The real ask is D-m (Apply C-n)." The PL gets no RESUMED notice because the hold stands. A loop that runs again expires its alarm with "T-1 runs again: progress resumed" (settled_cause). Loops that ended (merged, closed) record the node they were about. A refuted claim's "Keep waiting?" is expired withsuperseded_by: it was escalated, not closed.alarms.py(new) reads today's server-closedloopanddone_checkalarms back from the events: answers by the server, and expiries that carry a cause and nosuperseded_by. The read grows (500 → 2000 → 8000) until it covers the 24 h window, so other decisions' events cannot push today's alarms out.fx:healthlists them asD-nitems withclosed_alarmson its page.alarms_closed {count, ref: "fx:health"}.loops.resumes_when_held: an idle stall within its wall budget, every loop riding on the decision): a wall stall, an exhausted or a thrashing loop stays an ask, since the server will not close it. The snapshot's stall wait for such an alarm is dropped too.Web (types and contract only; rendering comes in W8/W9)
nodeTypes.tsgains optionalNodeFull.envandclosed_alarms(withreal_ask_verb).factoryApi.tsgainsFactoryBaseView.deployed.nodeWire.server.test.tsxnow reads the server's lines throughstatusTextinstead of expecting empty segments.fx:health.Deviations
waiting_on(the server's ownwaits.project_waits, viewer-filtered). They are recomputed only for a project the snapshot has no view of. The test fixture now builds every view'swaiting_onwithwaits.project_waitsthe way the snapshot does (node_fixtures.snapshot_waits), so the headline case is what production computes.test_reconcile_expires_*contract is unchanged.api.mdNodeFull gainedenvandclosed_alarmslines to match.Test Plan
tests/server/factory/test_node_status.py(31 tests) runs the prototype's cases on the 13:40 fixture. Every view'swaiting_onis now built bywaits.project_waitsas the snapshot does (node_fixtures.snapshot_waits), not written by hand:dependency_met(assistant-core)is false while T-16 is open and would be true without it.ends: other), "… the server starts it within a minute" (ends: server), "… starting: pulling the image", and "… waits for a free machine (all 12 machines in use, 3 ahead)".…, the person),ends: person,decisionD-45, label "Not started for 7 h:"; the tile keeps D-45.weight()forced to 0).ends: server.cyclefrom both sides.superseded_bydoes not.test_dependency_gating.py: a project with an open task does not meet amergeddependency; a done one does;readymode unchanged.test_loops.py: a refuted claim's "Keep waiting?" is expired withsuperseded_byand no cause, and Health does not list it.test_loop_stall.py: a stall then held on purpose is answered with its cause, the real ask and its verb ("Apply C-n"), and Health reads it back.test_base_watcher.py: an unplaced deployed head contains only itself; once placed on its move, the earlier merge is in it.uv run --frozen pytest -q -n 8 tests/server/factory tests/examples/test_factory_sample_tokens.py: 4321 passed, 2 failed.test_discovery_beats::test_the_budget_line_then_the_stop_and_the_queued_askfails the same way on a cleanorigin/stagingcheckout (checked).test_nodes::test_every_page_of_a_13_45_360_factory_is_fasthit 0.76 s while pre-commit and the web tests ran alongside; alone it passes, and the first build takes 0.17 s with and without this change.test_sessions_factory_labels, runner factory tools, bundles): 336 passed.vitest run src/components/factory/v3(114 passed),tsc -b,oxlint,prettier --check.pre-commiton the changed files: ruff and web hooks pass; pyrefly reports only the 28 errors already on staging (telemetry, gensee, reconcile), none in touched files.OMNIGENT_FACTORY_NODE_API=1, open/factory/<id>/n/assistant-panel-ux?cockpit=v3and read the header line, then "why? ▾". Openn/productto see the top blocker and "3 alarms closed as noise". Openn/fx:healthto see the closed alarms with their real asks.Demo
N/A here: this PR is server data plus types. The web already renders these lines (W2), and the server's pages go through the live source in
nodeWire.server.test.tsx.Type of change
Test coverage
Coverage notes
The contract cases run on the shared 13:40 fixture. The same JSON drives the web's live-source contract test, so server and web cannot drift.
Changelog
Cockpit v3 node pages say why each node is or isn't moving, ending at the person who must act, and show burn, ETA, Try it, what is in the base env and the alarms the server closed. A project that depends on another now starts when that project's work is finished, not after its first merge.
🤖 Generated with Claude Code