live-parity-plan.md
Live Activity: parity with qcontrol main's `/live`
Written: 2026-11-12, from Mark's prompt (live-parity-prompt.md) and his screenshot of the live qcontrol app (live-parity-reference-main.webp).
The reference: qdash's /live on qcontrol origin/main 8677b44ed (2026-11-12): crates/qdash/ui/app/pages/live.vue (849 lines) and what it imports. qdash's UI and backend are unchanged since 9e6cd55a0, the commit c138's survey read for the backend.
Ours: qnative main 880451a, Dev › Live Activity: crates/devmode/src/live.rs (1,058 lines) on the stream core in crates/stream.
Standing: a plan for Mark's review. Nothing is built, and nothing in qcontrol changes.
In short
The native page matches the wrong page.
- Where the vectors came from. Its parity vectors run qdash's TypeScript from the local qcontrol checkout,
scripts/live-vectors.shwithQDASH_UI=../qcontrol/crates/qdash/ui. That checkout is onfeat/new-pages, the Fable app, which branched from main on 2026-10-04 (8e98cdd8e). review-3 records the source aslib/live/*.ts at 5c9175fa6, the Fable move. - What main added since. Main has moved on without the Fable branch:
- the Audit log (#757, 2026-10-08) and its order fix (#765);
- the audit-logger renamed Sentinel (#772);
- Sentinel on MCP calls and host operations (#787);
- Sentinel's inference categories (#835);
- plugin events that carry
datawhere they carriedpayload.
- Why nobody saw it. c138's survey read main's backend but the Fable branch's UI (research/live-viewer.md, lines 4 and 10). The native port is held to the Fable app by every vector review-3 lists, and none of them knows the Audit log exists.
- What the screenshot shows. It is the Audit log, the one region the native page lacks entirely.
Under that sits drift in the regions both pages share:
- a side rail where the web has a filter card;
- no column header;
- a 5k/20k/100k picker where the web has buffer inputs;
- the type as coloured text where the web has a chip;
- a fixed detail pane with a code block where the web has a draggable pane with a tree;
- a bare tally.
Some of these departures are c138's seven improvements, and those stay. The rest are drift.
The plan has four phases, with a review stop after each:
- Re-anchor the vectors on main.
- Port the audit projection and give it its own buffer.
- Build the Audit log tab.
- Bring the shared regions back to main's layout, keeping the improvements.
A wipe against main ends it. Seven calls for Mark come first; they decide what Phase 3 builds.
Why parity broke
- The source.
scripts/live-vectors.shdefaultsQDASH_UIto../qcontrol/crates/qdash/ui. On this machine that isfeat/new-pages(merge-base with main8e98cdd8e, 2026-10-04). The twolib/live/trees have since diverged:- only main has
audit-events.ts(146 lines) and its tests (9 cases, 285 lines), andlive-config.ts; - only the Fable app has
event-context.ts,event-rows.tsandrun-log.ts, because it split them out of the page.
- only main has
- The plugin quirk. Main's
events.tsreads a plugin event's fields fromdata, falling back topayload. The Fableevents.tsreadspayload.payload, and the native port keeps that "web quirk, kept for parity" (crates/stream/src/preview.rs:480). On a sensor running Sentinel, every audit record in the native page therefore readsplugin=sentinel │ event=sentinel.inferenceand nothing more: no target, no model, no decision. - The corpus. 614 lines, one
plugin.event, no Sentinel record. No vector could have caught it. auto_tap. Main dropped the family. The Fable app and the native port still know it.
What main's /live is (read 2026-11-12)
Regions, top to bottom (pages/live.vue unless named):
- Header.
- Title "Live Activity" and subtitle "Live events and audit summaries — filter activity, inspect details, or open a captured JSONL log."
- At the right: a status pill (
livegreen,connecting,reconnectingamber,disconnectedred,file · <name>), Open JSONL, and Return to live in file mode.
- Drop zone. Drop a file anywhere. The overlay reads "Drop JSONL to load a run log / The file stays local in this browser." If the view is not empty, the page confirms before replacing it.
- Presence.
- A right-aligned "Show agents / Hide agents" link, shown only when cards exist. Hidden by default and remembered.
LiveMiniAgentCards(the layer's agent MiniCard): status pawn, folder, logo and label, requests, files (read/write/edit), and tools and skills.- View details toggles the agent filter; the card shows a selected ring.
- Tabs and buffer, one row.
- Tabs: Events (kept), Tally (never evicts), Audit log (audit rows), each with its count.
RetentionControlsbeside them, keyed to the open tab: "Event buffer" or "Audit buffer", N kept, X MiB, N evicted (amber, only when above zero), a number input for events (step 500), one for MiB (step 5), and Reset.- Saved separately per view (
qdash.live.retention.v1,qdash.audit.retention.v1); the defaults are 5,000 events and 25 MiB.
- Audit note (Audit log only): "Audit summaries retained in this live browser session." In file mode, "Audit summaries from the loaded log." Both then say: "The decision records whether an action was allowed, not whether it succeeded."
- Filter card.
- Search: "Search visible fields", or "Search paths, commands, tools, or decisions" on the Audit log.
- The Filter popover: agent checkboxes, then a tri-state family → type tree with counts and Clear all. On the Audit log it lists categories only, with no types.
- Removable chips for each agent, family and type, and Reset.
- Under a rule, two chip rails with counts: Agents (unselected chips dim) and Events (family tones), which reads Activity on the Audit log.
- Follow. Scrolling away from the live edge freezes the follow. An amber banner reads "N new events" (or "N new audit entries", or "Viewing older entries") with "Jump to latest".
- Table (
components/live/EventTable.vue): 44 px rows, virtualised, zebra, a selected ring.- Events: Time (the header toggles newest on top or bottom) · Seq · Event (a mono chip in the family tone) · Agent (click to filter, with
(run pid/agent pid)or(pid)) · Summary (key=subtle, value muted). - Audit log: Time · Activity (a chip with the readable label; a capture gap is amber) · Agent · Summary (two lines: the target, then "Rules: … | Tags: …") · Decision (a pill:
allowedgreen,deniedred,flaggedamber,—). The pid comes from the entity index. - When the table is narrower than 640 px (the detail open), the Agent column hides.
- Empty states: "No events loaded …"; "No matching audit summaries — Run
qcontrol run --plugins sentinel -- …while this page is connected, or open a captured JSONL log. Check your filters if records are hidden."
- Events: Time (the header toggles newest on top or bottom) · Seq · Event (a mono chip in the family tone) · Agent (click to filter, with
- Detail (
components/live/EventDetail.vue).- Header:
[seq] typein the family hue, Copy JSON, and close. - Context chips in order: Agent, Entity, Install, Run, Run PID, Agent PID or PID, Family, Type. Each filters or searches. On the Audit log, Type is evidence only.
- Preview as a key/value table.
- Payload as a JSON tree expanded to depth 2.
- Width draggable from 360 to 920 px. Below 1280 px wide the detail becomes an overlay.
- The selection is deep-linked as
#event=<id>.
- Header:
- Switching to or from the Audit log clears the family and type filters, closes the detail and jumps to the latest row.
- Audit projection (
lib/live/audit-events.ts).- It takes
plugin.eventwithplugin_name: sentinelandevent: sentinel.*, readingdata(orpayloadfor older logs). - Activity labels: File read, File write, File open (a denied or flagged open), File access error, Command, HTTP request, Inference, Background inference, Subagent inference, Tool call, MCP tool call, Skill read, Skill request, Capture gap, Connection.
- It reads the decision from
decision.result, falling back to a baredecision, and the reasons fromdecision.reasons, falling back torule. - The summary is the detail or path. For inference it is
authority · model; for a tool it isname: target [query], capped at 512 characters. - It keeps the record and its id, and puts every scalar field into the preview.
- It takes
- Store (
composables/useLiveEvents.ts): two histories, Events and Audit, bounded independently and sharing one entity index. File traffic cannot evict audit history; a late process snapshot attributes audit rows the Events buffer has already evicted; file mode fills both, untrimmed. - Lifetime. The stream is a
globalThissingleton that survives navigating away; a reload loses it.
Side by side
Missing means main has it and we do not. Drift means both have it, differently, for no reason that was ruled. Ours means an improvement from c138 to keep.
| Region | main /live | native Live Activity | Gap |
|---|---|---|---|
| Audit log tab | Third tab, own buffer, projection, Decision column | none | Missing |
| Sentinel records | Projected; data read | payload.payload read; fields lost | Missing (a kept quirk main fixed) |
| Header | Title, subtitle, status pill at right, Open JSONL | Title; a sentence state line (rate, last event, reasons) | Drift (pill, subtitle); Ours (the honest line) |
| Tab counts | Events N · Tally N · Audit log N | Events · Tally, no counts | Drift |
| Buffer control | "Event/Audit buffer · kept · MiB · evicted", two inputs, Reset, per view, saved | Keep 5k/20k/100k; kept and MiB in the state line | Drift (review-3 call 3 asks about the defaults) |
| Presence toggle | "Show agents", right, hidden by default | "Show harnesses (N)" button, shown by default | Drift (review-3 call 5) |
| Presence cards | The layer MiniCard: pawn, logo, folder; View details filters; focus and reveal | Text cards, 8 at most; no actions | Drift, actions missing |
| Filters | Card: search, popover with type tree, chips, Reset, Agents and Events chip rails | Left rail: severity, harnesses, families, "Filtered to", Clear filters | Drift; Ours (severity, process, session, payload search) |
| Type filter | Popover tree and chips | Only through the detail's type chip | Missing |
| Follow and pause | Scroll away = follow frozen; amber banner, Jump to latest | Pause button and Space; the view really freezes | Drift; Ours (a real pause) |
| Table header | TIME ▲ (toggles direction), SEQ, EVENT, AGENT, SUMMARY | none; newest always on top | Missing |
| Row | 44 px, zebra, type as a chip, agent clickable, key= styled | 28 px, rules, type as coloured text, severity dot, ms | Drift; Ours (ms, t for ages, severity) |
| Agent cell | Adapter id claude-cli + (pid); click filters | Display name Claude Code (4312); no click | Drift (the word: c92's rename) |
| Detail | [seq] type, Copy JSON, close; 8 context chips; tree to depth 2; drag 360–920; overlay when narrow | #line type; time · severity · entity; 3 chips (process, session, type); code block; fixed 480 px | Drift; Ours (masking, Reveal, session chip) |
| Tally | Pivot: expand a family into types, sort, hide zero rows and columns, shading, totals, Copy view and Copy full | Process, session, type or run keys; plain table; Copy CSV | Drift; Ours (the keys) |
| File mode | Drop anywhere, Open JSONL, confirm replace, both buffers filled | Open JSONL (path prompt); no drop, no confirm | Drift |
| Empty and status | One message per table | Seven honest states | Ours |
| Keys and export | Enter/Space select | ↑↓ j k, Space, t, Esc; Export JSONL; Clear | Ours |
| Lifetime | Stream survives navigating away | Feed ends with the page (the gate) | Ours (call 7) |
The plan
Every phase is built in ../qnative, in c3 or a cycle of its own at kick-off. qcontrol is only read, through a worktree of main. Each phase ends at a review stop with captures.
Phase 0: re-anchor on main
- A read-only checkout of main. Run
git -C ../qcontrol worktree add --detach ../qcontrol-main origin/main. Leave the Fable checkout alone, since other sessions use it. - The script.
- Point
scripts/live-vectors.shat../qcontrol-main/crates/qdash/ui. - Add an
ESBUILDoverride, so the worktree needs nonpm ci. It borrows the Fable checkout's esbuild, which only bundles. - Refuse to run when the checkout is not main, and print the commit the vectors came from (it already records it).
- Point
- The harnesses. They import three modules main does not have:
event-context,event-rowsandrun-log.- Main does that work inline: the detail chips in
EventDetail.vue'scontextParts, the rows inEventTable.vuewithevent-display.ts, the run log inlive.vue'sloadFile. - Copy each piece into its harness as a marked copy, as the tally harness does with
tallyView.ts. - Then retarget the ports to main's behaviour (the agent chip's word, the context order, the file sort).
- Main does that work inline: the detail chips in
- A Sentinel stream script in
crates/fixtures, joining the corpus. It covers:- every activity kind:
file_open,file_read,file_write,exec,http,inference,tool,observation_gap,connection; - each decision (
allowed,denied,flagged, none) with reasons and tags; inference_kindthree ways, and the legacyrequest_class;- tool
kindastool,mcpandskill, plusskill_evidence: file_read; - both the
dataand the flat legacypayloadenvelopes; - the screenshot's row: an Anthropic inference denied by "Secret in prompt", attributed through a
process.startedforclaude-cli.
- every activity kind:
- Regenerate and fix the ports.
- Expected: the plugin preview reads
data,auto_tapgoes, and every Fable-only difference turns up. - Remove the quirks main fixed, and update review-3's quirk list.
- Expected: the plugin preview reads
- Correct the survey. A section 8 in research/live-viewer.md, "main's
/live", saying what the survey missed and pointing here, so the next reader starts from main.
Done when: the vectors regenerate byte-identical from main twice, and cargo test -p stream passes against them.
Phase 1: the Audit log's data (crates/stream)
stream::audit. A port ofaudit-events.ts:is_audit_event,project_audit_event,audit_field,audit_metadata,audit_summary, and the inference and tool labels. The 512-character cap counts code points (Array.from), not bytes.- Vectors:
scripts/vectors/audit.tsover the corpus, plus the nine cases ofaudit-events.test.tsas fixed inputs.
- Vectors:
- Two histories.
- A second bounded buffer fed only audit frames, with its own limits and its own protected selection.
- It shares the entity index, so a late process snapshot attributes audit rows already gone from the Events buffer.
- File mode fills both, untrimmed.
stream::bufferkeeps raw frames, so audit rows are projected on demand, like any other row.
- The pid for audit rows comes from the entity index (main's
pidForEvent).
Done when: the audit vectors agree, and a buffer test shows that 100,000 file.* records leave the audit history whole.
Phase 2: the Audit log tab (crates/devmode)
- A third tab, "Audit log N", with counts on all three: Events is the number kept, Tally the never-evicting total, Audit log the audit rows.
- Main's audit note, in both its live and file versions.
- The table in audit mode.
- Columns: Time · Activity chip (a capture gap in the warning tone) · Agent
(pid)· a two-line Summary (the target, then "Rules: … | Tags: …") · Decision pill (allowedSUCCESS,deniedERROR,flaggedWARNING,—neutral). - No Seq column.
- When the detail leaves the table under 640 px, the Agent column hides.
- Columns: Time · Activity chip (a capture gap in the warning tone) · Agent
- Filters in audit mode.
- The rail reads Activity and lists categories only; the popover shows no types; the detail's Type chip is not a filter.
- The search placeholder is main's, and the search reaches the decision, the rules and the tags, which are in the projected summary.
- Switching tabs clears the family and type filters, closes the detail and jumps to the latest row, as main does.
- An Audit buffer of its own, with limits saved per view.
- Main's empty state, with
qcontrol run --plugins sentinel -- …. The honest states still take precedence while the stream is not live. - Stories:
app-dev-live-audit;-audit-denied(the screenshot's state, filtered toactivity=Inference);-audit-narrow(the detail open);-audit-empty;-audit-file.
Done when: the five stories render and their region-text tests pass. The wipe against main comes in Phase 4.
Phase 3: the shared regions, back to main's layout
What changes comes first in each item; what stays, because it is one of c138's improvements, is marked keep.
- Header.
- Main's subtitle, with the status pill at the right beside Open JSONL and Return to live, using main's words: live, connecting, reconnecting, disconnected, file · name.
- Keep the honest state line under the title, as the subtitle's second line: the pill gives the word, the line the detail.
- Presence.
- The toggle as main's right-aligned link above the tabs (its word is call 1; hidden or shown by default is review-3 call 5).
- Cards in the MiniCard's arrangement: status, folder, logo and label, Requests, Files R/W/E, and Tools & Skills (three, then "+N more").
- View details toggles the agent filter and rings the card. Folder and focus actions are call 6.
- Tabs and buffer on one row.
- The tabs at the left.
- At the right, main's buffer cluster: "Event buffer · N kept · X MiB · N evicted", an events input, a MiB input and Reset, per view, saved in the Store. This replaces the 5k/20k/100k picker (call 4).
- The filter card replaces the left rail (call 2).
- Search, then the Filter popover: agent checkboxes and a tri-state family → type tree with counts, using
gpui-componentpopover,checkboxandtree. - Removable chips for each agent, family and type, and Reset.
- The chip rails under a rule: Agents, then Events (or Activity).
- Keep severity as a third rail (all, info+, warn+, error), process and session as chips, and the search over the whole record.
- Add Esc to close the popover. Main lacks it; it was survey item 14.
- Search, then the Filter popover: agent checkboxes and a tri-state family → type tree with counts, using
- Follow and pause (call 3).
- Scrolling away from the live edge pauses: keep the real freeze at a line.
- The pause shows main's amber banner, "N new events" with "Jump to latest", and Jump to latest resumes.
- Keep Space and the Pause button.
- Table.
- A header row: TIME ▲ toggles newest on top or bottom, then SEQ, EVENT, AGENT, SUMMARY.
- The type as a mono chip in the family tone. The agent cell is clickable to filter, with the pid in parentheses.
- The summary styled as
key=subtle and the value muted. Zebra rows and a selected ring. - Row height is call 5.
- Keep milliseconds,
tfor ages, ↑↓ j k, and the severity dot (moved into the Time cell, so the columns match main's).
- Detail.
- The header
[seq] typein the family hue, with Copy JSON and a close ✕. - Main's context chips in main's order (Agent, Entity, Install, Run, Run PID, Agent PID or PID, Family, Type), each filtering or searching as main's does. Keep Session after them.
- The preview as a key/value table.
- The payload as a tree expanded to depth 2 (
tree). Above 200,000 characters it falls back to the code block, as main falls back to<pre>. - Width draggable from 360 to 920 px (
resizable). - Keep masking, Reveal and "Copy JSON (masked)".
- The header
- Tally.
- Main's pivot on the Run key: expand a family into types, sort by run, total, family or type, Hide zero rows (on by default), Hide zero columns, expand or collapse all, Reset, log-scaled shading relative to the column, a totals footer, Copy view and Copy full.
- Keep process, session and type as the key picker, with Process the default, since the Run key is one "Unattributed" row on today's stream.
- File mode.
- Drop a file anywhere on the window, with main's overlay sentence and a confirmation before replacing a view that is not empty.
- Keep Export JSONL and Clear.
- Stories. Re-capture the eight existing
app-dev-live*stories and add one for the filter popover and one for a frozen follow.
Phase 4: the wipe, and the readings
- Main, captured.
- Run main's UI from the
../qcontrol-mainworktree on its own port, never a secondnuxt devin a busy tree. - Load the corpus through Open JSONL (a file input in a shot program), so both pages show the same records.
- Shot program:
scripts/shot-programs/c138-live-parity-main.json, with one state each for Events, Tally, Audit log, Audit log filtered to Inference, detail open, filtered to nothing, and the popover.
- Run main's UI from the
- Ours, captured from the same file with
npm run shots -- --native, state for state. - The wipe. Pairs in
UxImageCompare, on the c138 cycle page: c132 D7's re-check, applied to a page we replicate. - The readings. Two buffers and the projection under 100 and 1,000 events a second (
scripts/footprint/live-activity.sh). They fold into the readings review-3 call 12 already owes. - The checklist. Every row of the side-by-side table above, marked matched, kept ours, or ruled away.
Calls for Mark (before Phase 3; Phases 0–2 need none of them)
- The word. Main says agents: "Show agents", the AGENT column, and the adapter id
claude-cli. Ours says harnesses, with the display name "Claude Code", per c92's rename (2026-09-22), which qdash's Fable app also follows.- Recommend: keep Harness and the display name, and put the adapter id in the chip's title.
- If faithful means the words as well, say so, and Phase 3 uses main's.
- The filter card or the rail.
- Recommend: main's card, with severity, process and session as extra rows and chips.
- The rail costs 200 px of table width, and main has no rail.
- Follow and pause.
- Recommend: one mechanism. Scrolling away is a real pause, frozen at a line, under main's banner, and Jump to latest resumes.
- The alternative is main's soft freeze, where rows keep evicting under you, plus our Pause button as a second, separate thing.
- The buffer control.
- Recommend: main's two inputs per view with our defaults, 20,000 events and 32 MiB (review-3 call 3).
- Fully faithful would be main's 5,000 events and 25 MiB.
- Row height. Main uses 44 px; ours is 28.
- Recommend: 44, because the Audit log's two-line summary does not fit in 28, and one table should keep one height.
- The cost is about a third as many rows on screen.
- Presence actions and default. The native app can reveal a folder (
open -R) and focus an app itself, without qdash's/api/agent/*routes.- Recommend: reveal now and focus later, with the row hidden by default as main has it (review-3 call 5 asked the opposite question).
- Lifetime. Main keeps streaming after you leave the page; ours ends the feed with the page, which is what the gate measures.
- With an Audit log, leaving the page loses its history.
- Recommend: keep ours while it is a developer tool. If the Audit log is ever shown outside developer mode (Findings is its natural home), it needs the sensor's cursor, not a page that stays open.
Out of this plan
- qdash's own
/liveonfeat/new-pageshas the same gap, because the Fable app replaced the files main has since changed. Both sides of PR qpoint-io/qcontrol#713 have changedlive.vue,EventTable.vue,EventDetail.vue,FilterPopover.vue,RetentionControls.vue,useLiveEvents.ts,retention-settings.tsandevents.tssince the merge-base, so when it merges with main the Audit log is lost unless someone carries it across. qcontrol is read-only here; it is a task of its own. Carried across 2026-11-12 in../qcontrol(feat/new-pages,d274b9306, pushed 2026-11-13): main's #757 fitted into the Fable app's split files. The record is fable-audit/README.md. - The sensor asks (a cursor, a gap marker, a filter on the server, a snapshot for the new subscriber only) are unchanged.
- c138 Phase 4, Events, still waits on Mark's brief and mock.
2026-11-13: the firehose in main's grammar (done: qnative f19e679, pushed)
Mark: "the live view, ie the firehose. It seems like the existing version is cleaner, better color coding, filtering". I compared main's /live with the native page on the same corpus. Main ran on :3031 from a read-only checkout of origin/main at ../qcontrol-main, against the seed backend. I then brought the native Events view to main's grammar in crates/devmode/src/live.rs. Captures are in live-firehose/.
- Colour: each type is a chip in its family's colour, from main's
familyTonemapped to QGUIkit's tokens (family_chip). The detail header and the tally take the same colours. - Rows:
- 44 px, zebra-striped, with the selected row tinted in primary;
- time, line and summary in 12 px mono;
- the summary as
key=valueparts, the key subtle and the value muted, with no pipes; - only lines that failed to parse in red.
- A column header: TIME ▲, SEQ, EVENT, HARNESS, SUMMARY.
- Filtering: main's card above the table replaces the 200 px rail. It holds the search, the severity control (kept from c138), the active filters as removable chips, Reset, and the Harnesses and Events rails as chips with counts. While one chip is on, the others dim.
- Kept from c138's improvements: the real pause, the honest states, the buffer, severity and payload search, the keys, export and masking.
This takes calls 2 (the filter card) and 5 (44 px rows) the way Mark's direction points. Still open from Phase 3:
- main's status pill and subtitle;
- the presence cards;
- main's Event buffer cluster (call 4);
- scroll-away as the pause, under main's banner (call 3);
- the time-direction toggle;
- the detail's JSON tree and resizable pane;
- the tally's pivot features;
- drag and drop of a JSONL file;
- the Audit log tab (Phases 1 and 2).