Skip to content

cockpit v3: level views and lenses, Needs you and Find, last-look marks (S8, S9, S10) - #213

Merged
LittleChimera merged 2 commits into
stagingfrom
factory/v3-m2-views
Nov 4, 2026
Merged

LittleChimera merged 2 commits into
stagingfrom
factory/v3-m2-views

Conversation

@LittleChimera

@LittleChimera LittleChimera commented Nov 4, 2026 •

Copy link
Copy Markdown

Related issue

Closes # (none: cockpit v3 milestone 2, plan.md S8, S9 and S10)

Summary

This PR adds the server side of three steering aids for cockpit v3: level views and lenses (S8), Needs you and Find (S9), and "since your last look" marks (S10). Everything lives under /v1/factory with the usual factory-reader auth. v1 and v2 are not touched. Each route reads the same 5-second, per-viewer tree as the node page, so a viewer sees only the projects, decisions and cost they are allowed to read.

ELI5: the cockpit can now answer four questions about any node. Where did this come from (lineage)? What happened under it today (timeline)? Where did the money go (money)? What changed since I last looked? Two factory-wide questions get answers too: what do I owe, most important first (Needs you), and where is T-16 (Find).

node page ──┬─ /level/{level}?state=&q=   one level, grouped under parents (sections, no lines)
            ├─ /lineage                    product › need › feature › project › here, then counts and results
            ├─ /timeline?from=&to=         one lane per child: work ▇  wait ▒  you ▒ (amber)
            ├─ /money                      children's cost against budget, plus what no child holds
            └─ PUT /seen, ?seen=1          last look → rollup.since + children's `changed`
factory ────┬─ /needs-you?tree=            asks ranked by held weight, merges grouped, block-nothing apart
            └─ /find?q=                    T-16, #9876, C-16, D-45, titles → node + breadcrumb

Plan bullet → what was done

plan.md Done
S8 GET …/n/{ref}/level/{level}: grouped under parents, all-done groups folded, cursors per group Already served since #211. This PR adds the state and q filters that api.md §3.6 listed as "not read yet" (400 bad_state). A group the filters leave empty is dropped. A cursor pages the filtered group.
S8 GET …/lineage node_lenses.lineage. Lists the ancestors root first. Folded implicit ancestors are flagged as on the breadcrumb. Every row has its compact status line. The root also has its why (repo → base), a need its evidence summary, a feature its approved by C-n link (from C-n for an implicit feature), and any row with progress shows it. Then come the node itself (here), the levels below it (levels_below) and its results (product tree only).
S8 GET …/timeline: spans become bars, waits become hatched gaps, amber when a person is the cause node_lenses.timeline gives one lane per shown child; runs and Later children are left out. Work comes from stage spans for projects and task spans for task sessions. Waits come from queued and ready stages, ready_wait spans (amber unless the server merges into an auto-merge target), open or blocking decision spans, and agent wait spans parked on a person. The wait a lane is in now is its compact status line, starting when it began. It is amber only when the lane's own path ends at a person; a path that hops to another node is grey. Gaps between bars are grey waiting. Spans of server-closed alarms (loop and done_check) never count. Done lanes draw their work as done. The window comes from from/to (epoch seconds or ISO, at most 7 days; otherwise 400 bad_window). By default it is the last day, starting at the hour of the first bar. last_look is this viewer's mark.
S8 GET …/money node_lenses.money. Children are sorted by cost (the same numbers as their roll-ups), with budget and burn. The response also has the total, the node's own budget, and rest: what the node spent that no child holds, such as leads and agents.
S8 Tests: tasks under the product grouped by project; the lineage of T-16 reads product → need → feature → project; the timeline of Agent assistant shows the panel waiting all day test_node_lenses.py: test_tasks_under_the_product_are_grouped_by_project, test_the_lineage_of_t16_reads_product_need_feature_project, test_the_timeline_of_agent_assistant_shows_the_panel_waiting_all_day. The panel lane is 06:01–13:40 wait "Waits on assistant-core · held for C-16". assistant-core is planning → T-15 → T-16, then amber from 07:46. There are also tests for filters, a closed alarm never being a person's wait, auto-merge targets waiting on the server, windows, no lane joining another, money, and timing at scale.
S9 GET /factories/{fid}/needs-you: ranked by held weight, merges on one human-merged base grouped, block-nothing asks grouped, closed alarms with causes, the other tree's count node_sheets.needs_you. Items are ranked by Paths.weight, the same held weight the blocker paths use, then by age. Each item has its breadcrumb (root first, ending at the node the ask sits on), holds by name (new Paths.holds: the tasks whose own wait ends at the ask, then the projects waiting on where it sits) and its weight. Ready PRs into one human-merged base are one merge:<base> item. blocks_nothing is listed apart, oldest first. closed_alarms (product tree) carry their causes and real asks. other_tree is a count (400 bad_tree).
S9 GET /factories/{fid}/find over refs and titles, with breadcrumbs node_sheets.find matches, in this order: refs (T-16, t-16, #9876, PR-9876, a slug, a closed alarm's D-n), what a decision or proposal is about (D-45 and C-16 go to assistant-core; C-11 goes to I-1 and then its projects, via new Tree.proposal_nodes), titles that start with the words, then titles that contain them (higher levels first). An empty query lists the work that holds an ask. Results are capped at limit (default 20, max 50). Done work never leaves the tree, so it is still found after 30 days.
S9 Tests: order D-45, merges, D-49, then C-14/C-15; find "T-16", "omnigent-ai#9876", "C-16" test_node_sheets.py: test_needs_you_ranks_by_what_each_ask_holds (D-45 holds T-16 and assistant-panel-ux), test_find_reads_task_pr_and_proposal_refs. Further tests cover merges as one item, closed alarms and the other tree, needs-you after D-45 is answered (4 left, as in api.md §8), viewer cuts, titles and breadcrumbs, done work still found, and the cap at scale.
S10 factory_node_reads (user_id, node_ref, seen_at) with a Postgres-verified migration Migration fz8c9d0e1f2a. The primary key is (workspace_id, factory_id, user_id, node_ref); seen_at is epoch seconds and only moves forward. The table is created only when missing, so CockroachDB can replay it. Also added: SqlFactoryNodeRead, FactoryNodeRead, and mark_node_seen/list_node_reads on the abstract, in-memory and SQL stores (racing first marks replay on IntegrityError).
S10 PUT …/n/{ref}/seen Marks the shown (folded) node. Uses HUMAN_ORIGIN; READ level is enough; 404 for an unknown node. GET …/n/{ref}?seen=1, which the page's seen param promised, now marks after building the response, and only for a person's request (a runner-origin GET does not mark).
S10 since items in roll-ups and changed on children last_look.py. The mark used is the node's own, or else that of its nearest looked-at ancestor. The events since then (500 newest of 10 kinds) are placed on the node each is about. Ranking: a new ask that holds work > a merge or failure > work added > a change applied > an answer > a task done > a stage moved. Three merges or more read as one ("6 merged"). rollup.since = {at, items[:3], more}. Each child gets changed. Not counted as changes: server-closed alarms, asks that block nothing, a builder going idle on an unfinished task, and work the viewer cannot read.
S10 Tests: marks are per person; since-items are the three most significant changes in the subtree test_nodes_api.py::test_last_looks_are_per_person_and_name_what_changed (alice gets since; carol has no mark, then seen=1 marks hers). test_last_look.py::test_the_root_names_the_three_most_significant_changes gives ["D-49 new", "6 merged", "T-17 done"] with more: 1. Further tests cover subtree scoping, noise, viewer cuts, ancestor marks and events before the mark. Store tests: tests/stores/test_factory_store_node_reads.py. Migration test: tests/db/test_migration_factory_node_reads.py.

Response models are in node_views.py: LineageView, TimelineView, MoneyView, NeedsYouView, FindView and SeenView (extra keys forbidden). nodeLenses.server.json (new, beside nodePages.server.json) is the server's output for the web's W7, W8 and W10, kept in sync by test_node_lens_wire.py. docs/FACTORY.md describes the lenses, sheets and last looks.

Deviations from api.md

  • needs-you.blocks_nothing items have the same shape as items ({decision, breadcrumb, holds: [], weight: 0}) rather than bare DecisionRefs, so the sheet can say where each one sits ("Open assistant-core"). closed_alarms is listed for the product tree only (in the factory tree they are Health's items already), and every sheet carries version.
  • Lineage rows are the node's Crumb plus line (its compact StatusLine), with the optional why_html (root), evidence (need), links (feature: approved by/from C-n) and progress. There is no "informed by" study link yet, because studies (R-n) are S14.
  • Money comes from the same snapshot costs as the roll-ups (session usage), not from factory_cost_daily, so the lens and the roll-up never disagree. It adds state, tag, the node's budget and rest.
  • Timeline lenses on the node page: the page does not say whether a timeline has content, because that would mean reading spans for every page. The web hides the lens when lanes is empty.
  • Level view reads state and q. group and lens stay client-side choices.
  • Migration chain: fz8c9d0e1f2a revises fz7b8c9d0e1f. If another M2 migration lands first, this one's down_revision needs a rebase (test_single_alembic_head will say so).

Test Plan

  • uv run --frozen pytest -q -n 8 tests/server/factory tests/stores tests/db tests/examples/test_factory_sample_tokens.py -p no:cacheprovider (SQLite, final commit): 5890 passed, 15 skipped. The only failures were the 11 test_databricks_volumes_artifact_store.py cases, which need the optional databricks extra that is not installed here; they are deselected on the standard lanes.
  • PostgreSQL 16 (throwaway container on port 55526, stopped afterwards):
    • alembic upgrade head → downgrade -1 → upgrade head: the table and its primary key appear, are dropped, and reappear. compare_metadata shows no diff for factory_node_reads.
    • With OMNIGENT_TEST_DB_URI set, tests/stores/test_factory_store_node_reads.py, tests/stores/test_factory_store_round3.py, tests/server/factory/test_nodes_api.py and tests/server/factory/test_cockpit_read.py all passed: 132 passed.
  • The touched suites, run after the final refactor: 375 passed. These include test_nodes.py, test_node_status.py (the held-weight change keeps every existing ranking), test_nodes_api.py (p95 < 300 ms with the extra mark read), the new lens, sheet, last-look and wire tests, test_factory_sample_tokens.py (FORBIDDEN_SAMPLE_TOKENS green), and the single-head and replay migration tests.
  • pre-commit run --files <changed>: ruff format and ruff check pass. pyrefly reports only the 28 errors already on staging (runtime/telemetry.py, onboarding/sandboxes/gensee.py, background/reconcile.py), none in the files this PR changes.
  • Runner-side code (omnigent/runner, omnigent/tools, omnigent/policies, omnigent/factory.py) is untouched.

Demo

N/A (server routes only). For a quick look, run a local server with a factory and try curl …/v1/factory/factories/<fid>/needs-you, …/find?q=T-1, …/n/<feature ref>/timeline, …/n/<ref>/lineage, …/n/<ref>/money. Then PUT …/n/product/seen, let something merge, and GET …/n/product: rollup.since names the merge.

Type of change

  • Bug fix
  • Feature
  • UI / frontend change
  • Refactor / chore
  • Docs
  • Test / CI
  • Breaking change

Test coverage

  • Unit tests added / updated
  • Integration tests added / updated
  • E2E tests added / updated
  • Manual verification completed
  • Existing tests cover this change
  • Not applicable

Coverage notes

The unit tests run on the 13:40 sample factory plus spans_1340(), a new fixture with the day's stages, task sessions, asks and ready PRs, and on the synthetic 13/45/360 factory. The integration tests drive the routes over a real store on SQLite and PostgreSQL. The web pieces (W7 lenses, W8 sheets, W10 since) are separate PRs and render against nodeLenses.server.json.

Review fixes

  • Answered asks follow the tree's rule. A closed proposal ask is amber only when its proposal holds work by Tree.proposal_holds_work (shared with blocks). D-43 on connectors-core, which is done, no longer cuts the done bar short.
  • A wait runs from when it began. A lane's current wait, and its recorded waits that are still open, start no earlier than the lane's first bar or the end of its last recorded work. If D-45 is asked at 06:31 while T-15 runs, the lane still shows T-15 and turns amber at 06:55. A merge is amber from when its PRs became ready: the sample's ready PRs now have ready_at 12:00, so the merge ask reads 1h 40m. In a lane that is not a roll-up, a person counts as the cause only when the open wait is theirs, never a queue.
  • Past queues say what they waited on. For example, waits on assistant-core: the first project the lane depends on that had not finished when the queue began.
  • Timelines are fast at the span cap. Each lane is flattened in one sweep with a heap. The default window reads and builds the last day instead of 7 days. A roll-up lane's unbroken work is one bar, named by its longest part (+n). 5000 spans in one lane now take milliseconds instead of 2.1 s and give one bar instead of 5001 (test_a_timeline_at_the_span_cap_is_fast_and_small).
  • from/to are range-checked. Values before 2000 or more than a week from now return 400 bad_window instead of a 500 from an int64 overflow.
  • Level views have no empty groups. state=later and state=ghost list those nodes as the items. A group with nothing to show is left out, filtered or not.
  • Needs you names what a merge holds: #9856, #9857.
  • Runner refusals on last-look marks are tested. PUT …/n/{ref}/seen is in the human-origin route table (runner bearer and tunnel header get 403 human_only). GET ?seen=1 from a runner leaves no mark. The write-route walk now covers the node routes too.

Rebased on staging after #214 (web only; the fixtures it reads were regenerated and pass). The migration is still the single head fz8c9d0e1f2a, checked again on PostgreSQL 16 (upgrade, downgrade -1, upgrade, no model diff).

🤖 Generated with Claude Code

LittleChimera and others added 2 commits November 4, 2026 04:35
…ks (S8, S9, S10)

S8. GET …/n/{ref}/level/{level} (grouped under parents, all-done groups
folded, a cursor per group) now filters by `state` and `q`. node_lenses.py
adds the lenses the node page does not carry: GET …/n/{ref}/lineage
(ancestors top to bottom with their one line, a need's evidence, the
proposal that approved a feature, the levels below and the results),
GET …/n/{ref}/timeline?from=&to= (one lane per child from the recorded
spans: stages and task sessions as work bars; queued and ready stages,
merges, open asks and agents parked on a person as waits, amber only when a
person is the cause; the wait a lane is in now is its status line; gaps are
waits; server-closed alarms never count; lanes never join) and
GET …/n/{ref}/money (children's cost against budget, most first, the total
and what no child holds).

S9. node_sheets.py: GET /factories/{fid}/needs-you?tree= ranks every ask by
the work it holds (the blocker paths' weight), with its breadcrumb and what
it holds by name; merges on one human-merged base are one item; asks that
block nothing come apart; closed alarms with their causes; the other tree's
count. GET /factories/{fid}/find?q= finds nodes by ref (T-16, omnigent-ai#9876, a
closed alarm), by what a decision or proposal is about (D-45, C-16) and by
title, each with its breadcrumb.

S10. factory_node_reads (workspace_id, factory_id, user_id, node_ref,
seen_at; migration fz8c9d0e1f2a, verified on PostgreSQL up/down/up) holds
one last look per person per node, only moving forward. PUT …/n/{ref}/seen
(people only) and GET …/n/{ref}?seen=1 (never from a runner) mark it; the
node page then carries rollup.since (the three most significant changes in
the subtree since that look, or the nearest ancestor's) and `changed` on
each child, from the event log placed on the nodes events are about.

The server writes nodeLenses.server.json for the web's lenses, sheets and
since lines (W7, W8, W10).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… tree's rule, no empty level groups (review)

- An answered proposal's ask is amber only when the proposal holds work by
  the tree's rule (Tree.proposal_holds_work, shared with blocks): D-43 on
  connectors-core, done, no longer cuts its done bar short.
- A lane's current wait, and its recorded waits still open, begin no earlier
  than the lane's first bar and its last recorded work: an ask made while
  T-15 ran leaves T-15 visible, and a merge is amber from when its PRs
  became ready (the sample's ready PRs now carry ready_at 12:00). A person
  is the cause of a leaf lane's wait only from their own recorded waits.
- A past queue names the project it waited on ("waits on assistant-core").
- Timelines sweep each lane once (a heap, not every bar per point), read the
  last day by default instead of 7 days, and a roll-up lane's unbroken work
  is one bar; 5000 spans in one lane take milliseconds.
- from/to outside 2000 .. a week from now answer 400 bad_window (no int64
  overflow in SQL).
- Level views: state=later and state=ghost list those nodes as the items; a
  group with nothing to show is left out.
- Needs you names the PRs a merge holds (omnigent-ai#9856, omnigent-ai#9857).
- Tests pin that PUT …/seen refuses runners (403 human_only) and that GET
  ?seen=1 from a runner marks nothing; the write-route walk covers the node
  routes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@LittleChimera
LittleChimera merged commit 523a713 into staging Nov 4, 2026
48 of 53 checks passed
@LittleChimera
LittleChimera deleted the factory/v3-m2-views branch November 4, 2026 03:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/XL Pull request size: XL

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant