Skip to content

feat(dashboard): batch the gates into one queue (DRC-4018) - #125

Merged
kentwelcome merged 3 commits into
mainfrom
feature/drc-4018-b7-batch-the-gates-into-one-queue
Aug 18, 2026
Merged

feat(dashboard): batch the gates into one queue (DRC-4018)#125
kentwelcome merged 3 commits into
mainfrom
feature/drc-4018-b7-batch-the-gates-into-one-queue

Conversation

@kentwelcome

@kentwelcome kentwelcome commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Implements DRC-4018B7 · Batch the gates into one queue. Unblocked by DRC-4015 (#124).

Attention has a fixed cost per switch, so three separate pings cost more than one list of three. The blocked sessions were already a list on screen. What they were not was a queue: the band rendered them in session id order — an arbitrary order to be stopped in — and gave you nothing to act with, so taking one meant reading a session id off the screen by eye and going hunting for its terminal.

No new data. All of this is interface over what the payload already carried, which is what the board note predicted.

What changed

Order. Needs-input rows are published longest-blocked first. The gate that has held you up longest is the one still costing something and the one you are least likely to have remembered; the one that just opened is the one you already know about. It ranks on blocked_since, a fixed timestamp rather than an elapsed wait, so the queue does not reshuffle under you while every row in it waits longer. Calm's attention ordering makes the same call for the same rows — its needs group was newest-first, which is right for a row you are watching and wrong for one waiting on you. recent is untouched.

Position. Rows numbered from the top, N waiting in the band head. The list reads as a queue and you can watch it shrink.

A handle. copy id per row, routed through the page's existing action channel so there is one clipboard implementation rather than two that report success differently.

A pass. j/k step, takes the row you are on, g reaches the queue from either display mode. Nothing marks a gate handled — Cargento does not write to a session, so a mark would record only that you clicked something, and a wrong one would hide a gate that is still open, which is the exact failure this band exists to prevent. You answer in the terminal, the server stops calling the session blocked, the row leaves on the next refresh and the cursor inherits the new head. The queue clears itself out of measurements.

One thing that is not interface. DRC-4015 found the per-row text reaches disk only sometimes. A row without it rendered an empty div — the same blank a row with nothing to add would leave — so the one row you cannot triage was also the one row that looked ordinary. It now says so, in both views.

Demo

The page in these shots is the shipped one — served through this branch's own page.load_page(), with the row order coming from the real aggregate.row_order(). The gate rows are a fixture on /api/data: the queue only shows its shape with several sessions held at a gate at once, and a live store rarely has four. Four gates at 47m / 19m / 6m / 1m, deliberately built newest-first in the payload so the sort has visible work to do.

The queue. 4 waiting in the band head, rows numbered from the top, longest-blocked first, a copy id handle on each. Row 3 is the case DRC-4015 left behind: its detail never reached disk, so the row says so rather than leaving the blank that a row with nothing to add would leave.

The needs-your-input band as a numbered queue, longest-blocked first

j steps the cursor. Two presses, row 1 to row 3.

The cursor moved to the third row of the queue

takes the row. The label reads copied only for a clipboard write the browser actually accepted — a refused write reports blocked instead.

The cursor row reporting copied after Enter

Calm mode, same queue, same order, and not readable on the same row.

The calm ledger showing the same four gates in the same order

The g fix, shown in the state that broke it. Under recent the gates scatter through the board and the 47m one falls to the bottom. This is where the pre-fix g narrowed without setting the ordering, rendering the queue backwards with the cursor on the last row.

Under the recent ordering the gates are scattered and the longest wait is last

Pressing g from there forces the ordering back to attention, narrows to the four gates, and lands the cursor on the head.

After g, the four gates alone in attention order with the cursor on the longest wait

Scope

D7 (DRC-4030) stays separate, by decision. This orders the gate queue; ranking working cards and the idle list against each other is a different question with different answers. B7 shrinks D7's remaining scope but does not close it.

Review

A subagent review found two real bugs, both the same kind — a binding that was right in calm mode and wrong in the view it was copied into:

  • The regular view took Space and the arrow keys. Calm can bind those because its ledger scrolls inside its own frame; the regular view is an ordinary long page, so this removed paging and line-scrolling — and only while something was blocked, since the branch returns early on an empty queue. j/k and only, now.
  • g into calm narrowed to the blocked rows without setting the ordering, so from recent it rendered the queue backwards with the cursor on the last row.

Also fixed from that review: the band's copy id dropped keyboard focus on the render it triggered; calm ranked on the clamped waitSec rather than the raw timestamp, the one way the two views could name a different head gate; gateCursorKey outlived an emptied queue; and one test could not fail (flipping the sort key's sign left it green). Both new regression tests were mutation-checked against the un-fixed code.

Verification

Full canonical pre-PR suite, locally:

  • ruff check, ruff format --check, mypy --strict, scripts/lint_embedded.py, scripts/validate_plugins.py, claude plugin validate ./cargento --strict
  • 1177 dashboard tests + 157 script tests, 89.3% coverage against a fail_under of 73
  • Version fields untouched at 0.11.0, verified against the merge base
  • /sync-docs run; its commit is on this branch

Also driven in a real browser: the ordering in both views, the ordinals, the cursor and its clamping, g from either mode, and copy id reporting copied on a real click.

🤖 Generated with Claude Code

Attention has a fixed cost per switch, so three separate pings cost more than
one list of three. The blocked sessions were already a list on screen. What
they were not was a queue: the band rendered them in session id order, which is
an arbitrary order to be stopped in, and gave you nothing to act with — you read
a session id off the screen by eye and went hunting for its terminal.

Four changes, and none of them is new data.

Order. Needs-input rows are published longest-blocked first. The gate that has
held you up longest is the one still costing something and the one you are
least likely to have remembered; the one that just opened is the one you already
know about. It ranks on `blocked_since`, a fixed timestamp rather than an
elapsed time, so the queue does not reshuffle under you while every row in it
waits longer. Calm's `attention` ordering makes the same call for the same rows
— its needs group was newest-first, which is right for a row you are watching
and wrong for one waiting on you. `recent` is untouched and still genuinely
newest-first for every state.

Position. Rows are numbered from the top and the band head says how many are
waiting, so the list reads as a queue and you can watch it shrink.

A handle. Each row carries `copy id`, the control calm's rows already had,
routed through the same action channel so there is one copy implementation
rather than two that report success differently.

A pass. `j`/`k` step the queue, Enter takes the row you are on, and `g` reaches
the queue from either mode. Nothing marks a gate handled: Cargento does not
write to a session, and a mark it did not measure would hide a gate that is
still open. You answer in the terminal, the server stops calling the session
blocked, the row leaves on the next refresh and the cursor inherits the new
head. The queue clears itself out of measurements.

One thing that is not interface. DRC-4015 found the per-row text reaches disk
only sometimes. A row without it used to render an empty div — the same blank a
row with nothing to add would leave — so the one row you cannot triage was also
the one row that looked ordinary. It now says so, in both views.

Scope note: D7 (DRC-4030) stays separate. This orders the gate queue; ranking
working cards and the idle list against each other is a different question.

Signed-off-by: Kent Huang <kent@infuseai.io>
Two of these were bugs, and both were bugs of the same kind: a binding that
looked right in calm mode and was wrong in the view it was copied into.

The regular view took Space and the arrow keys. Calm can bind those because its
ledger scrolls inside its own frame under its own cursor; the regular view is an
ordinary long page, so preventDefault there removed paging and line-scrolling —
and removed them only while something was blocked, since the branch returns
early on an empty queue. Scroll keys that work or not depending on the payload
are worse than no bindings. `j`/`k` and Enter only, now.

`g` into calm narrowed to the blocked rows without setting the ordering. From
`recent` that renders the queue exactly backwards with the cursor parked on the
last row; from `fastest` it files the gates under "not working now". SKILL.md
asserted both modes show the same queue in the same order, which was true only
under `attention`. It sets the ordering too, so the doc is now true as written.

Also from the review:

The band's `copy id` dropped keyboard focus on the render it triggered. Calm
solves this with calmFocusKey/calmRestoreFocus; the regular path called neither,
so activating the button from the keyboard sent focus to <body> and the next Tab
restarted at the top of the document.

Calm's `attention` ranked the queue on `waitSec`, the rendered elapsed time,
which floors at zero. Two rows carrying implausibly future stamps would tie
there while the server still separated them — the one way the two views could
name a different gate at the head. It ranks on the raw timestamp now, as
aggregate.py does.

`gateCursorKey` outlived an emptied queue, so a session that blocked, was
answered, and blocked again later inherited a cursor that belonged on whichever
gate had waited longest.

test_the_order_does_not_move_as_the_wait_lengthens could not fail: sorted() over
one list with one key is deterministic by construction, and flipping the key's
sign left it green. Replaced with an assertion on the key itself.

Two gaps closed: no test clicked the band's copy button, so the data-calm reuse
— the riskiest part — was only ever exercised through the keyboard, which does
not go through the global listener; and nothing covered the focus hand-off. Both
new tests were mutation-checked against the un-fixed code.

Docs: the calm-mode controls paragraph had ended up inside the gate-queue
section though it owns sorting, filtering and the `u` key, so it moves back
under Display modes. The queue's key list said `⏎` copies, which is regular
mode only — in calm it expands. The "nothing marks a gate handled" argument was
being restated in five places; it moves to docs/design-needs-input.md as N-8,
which already owns the needs-input reasoning, and the comments become pointers.

Comment trimming throughout, on the same standard: the paragraph in main.js
restated the three lines under it, and row_order carried an 18-line docstring
over a 4-line function while leaving the one branch a reader would stop at — the
last_activity fallback — unexplained.

Signed-off-by: Kent Huang <kent@infuseai.io>
The module ownership table in the architecture doc did not say where the queue
lives. It is split three ways on purpose: the queue itself is shared page state
because both views read it, the band's cursor belongs to the regular view, and
the listeners that route both views' controls have always been in calm.js and
are the page's rather than calm's. A reader checking the inward-only rule
against that table would have read the last one as drift.

The N-8 entry added in the previous commit carried two em dashes, which the
voice standard bans in docs/design-*.md. Nothing in CI checks that.

The sync marker was stamped at 8f859f8 and four PRs have merged since: #121
through #124. Their docs are current, so this is the orphaned-marker failure the
skill warns about rather than a real backlog, and the new marker is the merge
base so the next squash cannot orphan it again.

Signed-off-by: Kent Huang <kent@infuseai.io>
Copilot AI lite review requested due to automatic review settings August 17, 2026 03:31
@github-actions

Copy link
Copy Markdown
Contributor

Coverage

Name                                                                  Stmts   Miss Branch BrPart  Cover
-------------------------------------------------------------------------------------------------------
cargento/skills/cargento/agy_hook.py                                     79     14     28      7  78.5%
cargento/skills/cargento/cargento_runtime/__init__.py                     0      0      0      0 100.0%
cargento/skills/cargento/cargento_runtime/aggregate.py                  157      1     34      0  99.5%
cargento/skills/cargento/cargento_runtime/claude_data.py                258     29    118     14  88.6%
cargento/skills/cargento/cargento_runtime/cli.py                        102     15     22      3  83.9%
cargento/skills/cargento/cargento_runtime/collectors/__init__.py          0      0      0      0 100.0%
cargento/skills/cargento/cargento_runtime/collectors/antigravity.py     410     42    166     26  86.8%
cargento/skills/cargento/cargento_runtime/collectors/claude.py          210     17     70      7  90.7%
cargento/skills/cargento/cargento_runtime/collectors/codex.py            97      7     38      7  89.6%
cargento/skills/cargento/cargento_runtime/collectors/copilot.py         124      6     42      2  95.2%
cargento/skills/cargento/cargento_runtime/collectors/cursor.py          172     18     68     12  86.7%
cargento/skills/cargento/cargento_runtime/collectors/droid.py            31      3      6      1  89.2%
cargento/skills/cargento/cargento_runtime/collectors/gemini.py           52      7     16      4  83.8%
cargento/skills/cargento/cargento_runtime/collectors/goose.py            89     11     28      4  87.2%
cargento/skills/cargento/cargento_runtime/collectors/opencode.py         78      6     26      2  92.3%
cargento/skills/cargento/cargento_runtime/collectors/pi.py              280     30    128     18  88.2%
cargento/skills/cargento/cargento_runtime/config.py                     165      1     20      1  98.9%
cargento/skills/cargento/cargento_runtime/diagnostics.py                 84      4     26      4  92.7%
cargento/skills/cargento/cargento_runtime/events.py                     160      0     60      0 100.0%
cargento/skills/cargento/cargento_runtime/http_api.py                   291     17     94      5  94.3%
cargento/skills/cargento/cargento_runtime/io.py                         126      2     28      0  98.7%
cargento/skills/cargento/cargento_runtime/lifecycle.py                  314     17     98      7  94.2%
cargento/skills/cargento/cargento_runtime/notifications.py              128     12     50      2  91.0%
cargento/skills/cargento/cargento_runtime/observation.py                210      2     54      0  99.2%
cargento/skills/cargento/cargento_runtime/probe.py                       44      0     18      1  98.4%
cargento/skills/cargento/cargento_runtime/quota.py                      333      2    112      1  99.3%
cargento/skills/cargento/cargento_runtime/records.py                    125      4     64      5  95.2%
cargento/skills/cargento/cargento_runtime/sessions.py                    98      0     42      0 100.0%
cargento/skills/cargento/cargento_runtime/snapshot.py                    36      0      4      0 100.0%
cargento/skills/cargento/cargento_runtime/spacedock.py                  366     61    180     23  83.5%
cargento/skills/cargento/cargento_runtime/state.py                       57      0      2      0 100.0%
cargento/skills/cargento/cargento_runtime/stream.py                      57      0      8      0 100.0%
cargento/skills/cargento/cargento_runtime/transcripts.py                232     15    114      7  93.1%
cargento/skills/cargento/cargento_runtime/turns.py                      143     17     82     14  85.3%
cargento/skills/cargento/cargento_runtime/web/__init__.py                 0      0      0      0 100.0%
cargento/skills/cargento/cargento_runtime/web/page.py                    18      0      4      0 100.0%
cargento/skills/cargento/event_hook.py                                   86      4     28      3  93.9%
cargento/skills/cargento/notify_hook.py                                  49     15      6      1  67.3%
cargento/skills/cargento/server.py                                        3      0      2      1  80.0%
cargento/skills/cargento/statusline_hook.py                             131     13     46      8  87.0%
scripts/bench_collect.py                                                211      9     54      5  94.7%
scripts/bench_event_latency.py                                           67     67     14      0   0.0%
scripts/bump_version.py                                                  60     12     24      5  77.4%
scripts/capture_hook.py                                                 279     31     82     12  87.5%
scripts/lint_embedded.py                                                 85      7     28      3  89.4%
scripts/validate_plugins.py                                             592    182    336     53  66.3%
-------------------------------------------------------------------------------------------------------
TOTAL                                                                  6689    700   2470    268  88.0%

Threshold: fail_under in pyproject.toml · label coverage-exception to bypass (visible in PR timeline).

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR implements DRC-4018 by turning “needs input” sessions into an explicit gate queue across both dashboard display modes, reducing attention cost and making blocked sessions actionable without introducing new data sources.

Changes:

  • Server now publishes blocked sessions in a meaningful queue order (longest-blocked first) via a shared row sort key.
  • Regular mode gains a numbered gate queue with a keyboard cursor (j/k) and a per-row copy id handle routed through the existing action channel.
  • Calm mode aligns its attention ordering and adds g to jump into the gate queue consistently from either mode, including “not readable” messaging when detail is absent.

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated no comments.

Show a summary per file
File Description
docs/design-runtime-architecture.md Updates module ownership notes to reflect gate queue responsibilities split across spark.js, regular.js, and global listeners.
docs/design-needs-input.md Adds N-8 design rationale: queue is payload-driven, cursor is key-based, and no “handled” state is stored locally.
COMPATIBILITY.md Updates docs sync marker.
cargento/skills/cargento/tests/test_page.py Updates asset oracles and adds comprehensive regular-mode gate queue behavior tests (ordering, cursor, copy, focus retention).
cargento/skills/cargento/tests/test_page_calm.py Adds calm-mode regression tests for attention ordering, unreadable detail labeling, and g jump behavior.
cargento/skills/cargento/tests/test_observation.py Adds RowOrderTest covering the new aggregate.row_order semantics.
cargento/skills/cargento/tests/test_lifecycle.py Updates expected assembled page size.
cargento/skills/cargento/SKILL.md Documents the gate queue behavior, ordering, affordances, and keybindings in both modes.
cargento/skills/cargento/cargento_runtime/web/styles.css Styles the gate queue as a numbered, cursor-highlightable list and adds “unreadable” styling for missing detail in both views.
cargento/skills/cargento/cargento_runtime/web/spark.js Introduces gateQueue(d) and shared cursor state for regular-mode queue navigation.
cargento/skills/cargento/cargento_runtime/web/regular.js Implements regular-mode gate queue cursor logic, numbering, copy button rendering, and unreadable-detail messaging.
cargento/skills/cargento/cargento_runtime/web/main.js Switches needs-input derivation to gateQueue, adds band header count/keys, and restores focus/cursor after renders.
cargento/skills/cargento/cargento_runtime/web/calm.js Aligns attention ordering for blocked rows, adds “not readable” doing-cell behavior, and implements g queue jump across modes.
cargento/skills/cargento/cargento_runtime/aggregate.py Adds row_order() and applies it to publish sessions in queue order (state rank + blocked_since/last_activity + sid).

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@kentwelcome
kentwelcome merged commit b4f7cfb into main Aug 18, 2026
12 checks passed
@kentwelcome
kentwelcome deleted the feature/drc-4018-b7-batch-the-gates-into-one-queue branch August 18, 2026 00:34
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.

2 participants