fix(agent-status): count only agent work in stats, and read a Grok background subagent as working - #22474
Conversation
There was a problem hiding this comment.
Important
One correctness gap in the new edge clock: a stop can be dated before the event that triggered it, dropping time from "Time agents worked" in a narrow Codex sequence. Details inline.
Reviewed changes
- Shared execution derivation.
isAgentExecutionOwedanswers "was an agent executing" fromstate,workingMode, andlead, with a deliberate old-host fallback tostate === 'working'. - Recorder reads
executing. The per-pane mirror stores the derived boolean instead of the combined state, andagentExecutionEdgeAtchooses the clock for each edge. - Plugin event gains
lead. A new pure projection adds the lead fact besidestate; the payload schema admits it as an optional field and restored rows still project to nothing. - Renderer readers pinned, not migrated. Smart sort and the unread badge keep reading the combined state, with tests that record the choice.
- Codex verdict carry-forward. A
Stopkeeps the inferred-interrupt verdict on the Codex root record; any other root event drops it, on both the hook and relay paths.
ℹ️ Nitpicks
docs/reference/agent-status-store.mdis the canonical description of the store and its readers, but it says nothing aboutisAgentExecutionOwedor the recorder moving off the combinedstate. A sentence in "The lead fact" would keep the doc honest about which question each reader asks.
DeepSeek Flash (free via Pullfrog for OSS) | 𝕏
There was a problem hiding this comment.
ℹ️ No new issues in this delta — the commit is tests-only and the added tests are sound. The edge-clock thread from the previous review remains open: the new recorder test covers the approval-wait ordering, not the lead-settles-while-the-row-is-already-non-working ordering that thread describes, and
agentExecutionEdgeAtis unchanged by this commit.
Reviewed changes
- Added a recorder test pinning that a Codex child's approval wait (combined
waiting) does not split a span while the lead's own turn runs. Verified meaningful by ablating the lead read inisAgentExecutionOwed: the test then reports 2 spawns / 45s instead of 1 / 60s. - Added an
isAgentExecutionOwedassertion that awaitingrow with aworkinglead still owes execution.
DeepSeek Flash (free via Pullfrog for OSS) | 𝕏
9fdfbfb to
093ca10
Compare
3798c11 to
2b24918
Compare
093ca10 to
3901004
Compare
3901004 to
94739e4
Compare
|
Navigate logical layers of code changes, visualize relationships, and explore their blast radius. Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (3)
Included review availability: Your plan provides up to 10 included reviews per hour; 7 remain after this review. 📝 WalkthroughWalkthroughThe change adds a shared predicate that determines whether an agent status accrues time. Grok background subagents now produce a working row, while shell tasks and active stop hooks produce monitoring status. Session transition recording uses the predicate and selects execution-edge timestamps. The plugin status event schema and projection support an optional main-agent fact. The main process emits the projected event only when a payload is produced. Tests cover these changes, activity unread counts, and sidebar attention. Priority: ➖ Normal Merge Risk: 🔵 Low · up to The change is mergeable with a narrow test-coverage follow-up: the OSC-repaint test does not protect its pinned-row-clock case. No current failure was established. 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Advanced
Run ID: 5c20594d-000d-466d-9d2f-5e5c1fd0173b
📒 Files selected for processing (10)
src/main/plugins/plugin-agent-status-event.test.tssrc/main/plugins/plugin-agent-status-event.tssrc/main/startup/main-process-plugins.tssrc/main/stats/agent-session-transition-recorder.test.tssrc/main/stats/agent-session-transition-recorder.tssrc/renderer/src/components/activity/useActivityUnreadCount.test.tssrc/renderer/src/components/sidebar/smart-attention.test.tssrc/shared/agent-lead-status-fold.test.tssrc/shared/agent-lead-status-fold.tssrc/shared/plugins/plugin-events.ts
Included review availability: Your plan provides up to 10 included reviews per hour; 2 remain after this review.
| const local = new AgentSessionTransitionRecorder(osc) | ||
| local.onStatus(mainAgentHook({ state: 'working', mainAgent: MAIN_AGENT_WORKING }, T)) | ||
| local.onStatus(hook('done', T + 10_000)) | ||
| local.onStatus(mainAgentHook({ state: 'working', mainAgent: MAIN_AGENT_WORKING }, T + 12_000)) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Make the OSC-repaint reopen test use the unchanged row clock.
The test comment says the next hook row "restores the fact with its unchanged clock, which predates that close". Line 394 passes stateStartedAt: T + 12_000 instead. hook sets receivedAt to the same value. Both clocks equal T+12000, so the reopen edge is T+12000 whichever clock agentExecutionEdgeAt picks. A regression back to event.stateStartedAt would still produce 18000, so the test cannot catch it. To test the stated case, keep the row clock at T and put the observation time in receivedAt.
💚 Proposed fix
- local.onStatus(mainAgentHook({ state: 'working', mainAgent: MAIN_AGENT_WORKING }, T + 12_000))
+ local.onStatus(
+ mainAgentHook({ state: 'working', mainAgent: MAIN_AGENT_WORKING }, T, {
+ receivedAt: T + 12_000
+ })
+ )📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| local.onStatus(mainAgentHook({ state: 'working', mainAgent: MAIN_AGENT_WORKING }, T + 12_000)) | |
| local.onStatus( | |
| mainAgentHook({ state: 'working', mainAgent: MAIN_AGENT_WORKING }, T, { | |
| receivedAt: T + 12_000 | |
| }) | |
| ) |
| export function agentExecutionEdgeAt(event: AgentSessionStatusEvent): number { | ||
| const { mainAgent, state } = event.payload | ||
| if (!mainAgent || (mainAgent.state !== 'working' && state !== 'working')) { | ||
| return event.stateStartedAt | ||
| } | ||
| return event.evidenceObservedAt ?? event.receivedAt | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
The stop edge is still dated too early when the main agent settles under a row that is not working.
The issue from the earlier review is still present in the new code. Sequence: (waiting, mainAgent working) at T+1000, then the root Stops while a child still waits, which produces (waiting, mainAgent done). At that point isAgentTimeAccruing becomes false, so the recorder emits stop. agentExecutionEdgeAt then takes the first branch, because mainAgent.state is 'done' and state is 'waiting'. It returns event.stateStartedAt, which is the child's prompt time (T+1000), not the Stop time. The main agent's time between those two points is lost from totalAgentTimeMs.
This PR does not use the remote mainAgent.stateStartedAt. When mainAgent is present, date the edge with the local evidence clock, and keep the later of the two clocks:
🐛 Proposed fix
export function agentExecutionEdgeAt(event: AgentSessionStatusEvent): number {
const { mainAgent, state } = event.payload
- if (!mainAgent || (mainAgent.state !== 'working' && state !== 'working')) {
+ if (!mainAgent) {
return event.stateStartedAt
}
- return event.evidenceObservedAt ?? event.receivedAt
+ const observedAt = event.evidenceObservedAt ?? event.receivedAt
+ if (mainAgent.state !== 'working' && state !== 'working') {
+ // The row clock does not move when the main agent settles under an unchanged `waiting` row.
+ return Math.max(event.stateStartedAt, observedAt)
+ }
+ return observedAt
}Add a recorder test with this sequence: (working, main working)@T``, then (waiting, main working)@T`+1000`, then `(waiting, main done)` with `stateStartedAt: T+1000` and `receivedAt: T+5000`. Expect `totalAgentTimeMs` to be 5000.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export function agentExecutionEdgeAt(event: AgentSessionStatusEvent): number { | |
| const { mainAgent, state } = event.payload | |
| if (!mainAgent || (mainAgent.state !== 'working' && state !== 'working')) { | |
| return event.stateStartedAt | |
| } | |
| return event.evidenceObservedAt ?? event.receivedAt | |
| } | |
| export function agentExecutionEdgeAt(event: AgentSessionStatusEvent): number { | |
| const { mainAgent, state } = event.payload | |
| if (!mainAgent) { | |
| return event.stateStartedAt | |
| } | |
| const observedAt = event.evidenceObservedAt ?? event.receivedAt | |
| if (mainAgent.state !== 'working' && state !== 'working') { | |
| // The row clock does not move when the main agent settles under an unchanged `waiting` row. | |
| return Math.max(event.stateStartedAt, observedAt) | |
| } | |
| return observedAt | |
| } |
There was a problem hiding this comment.
Important
The agentExecutionEdgeAt rewrite fixes the remote-clock skew and the pinned-row-clock reopen, but the stale-stop case from the previous review is unchanged: a stop is still dated by the row's clock when the row was already paused before the main agent settled. Re-verified against this head (1000 ms recorded where 5000 ms elapsed).
Reviewed changes
- Rewrote
agentExecutionEdgeAtto dropmainAgent.stateStartedAt(an SSH host's own clock) and date every edge fromstateStartedAt/evidenceObservedAt/receivedAt. - Renamed
isAgentExecutionOwedtoisAgentTimeAccruingand reworded its doc to scope it to the stats question. - Added recorder tests for SSH clock skew, an OSC-repaint reopen, and a subagent reopening a monitoring row dated by the evidence clock.
- Documented
mainAgent.stateStartedAtas the host's own clock in the plugin payload schema.
DeepSeek Flash (free via Pullfrog for OSS) | 𝕏
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
- Reversed the helper-wait semantics:
isAgentTimeAccruingnow returnsfalsefor any non-workingrow before consultingmainAgent, so a child's approval or question pauses "Time agents worked" and its resume is a second spawn. - Simplified
agentExecutionEdgeAtto!mainAgent || state !== 'working'→ row clock, otherwise evidence clock; under the new fold the oldmainAgent.state === 'working'evidence branch is unreachable, so the two changes stay consistent. - Replaced the recorder test that pinned one span across a child's approval wait with a parameterized test that pins the pause for both the main agent's own prompt and a child's, dated by the row clock.
- Rescoped the derivation docs and added
isAgentTimeAccruingcases for the pause, the watch loop, and the old-host fallback. - Verified the deltas: the five changed test files pass (100 tests), and the earlier stale-stop ordering can no longer strand an open session because
executingis true only while the row isworking.
DeepSeek Flash (free via Pullfrog for OSS) | 𝕏
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
- Derivation now reads the combined row.
isAgentTimeAccruingisstate === 'working' && workingMode !== 'monitoring'; themainAgentdependency and the old-host fallback are gone, so every host is read by the same rule. - Edge clock simplified to a state branch.
agentExecutionEdgeAtdates an edge insideworkingby the evidence clock and an edge that leavesworkingby the row's own state clock;mainAgent.stateStartedAtis no longer read, which structurally retires the SSH clock-skew concern. - Recorder mirrors
executing.classifyAgentSessionTransitionkeys on the derived boolean, and the recorder dates starts/stops only from this host's clocks. - Tests reworked around the combined row. The main-agent-fact suite was replaced by an old-host monitoring row that now stops the clock, a restored-row first-live start dated by the evidence, a real status-store hydration start, and the pause/SSH/reopen cases.
- The load-bearing invariant is pinned. The fold test cross-products every
foldAgentLeadStatusinput and asserts the derivation matches "main agent turn, or settled main agent plus live agent child work", which is what licenses readingworking+workingModealone.
The load-bearing claim — monitoring implies a settled main agent — holds across the codebase: the fold only emits it under leadState === 'done', the Claude roster, Grok, and the structured lane all route through that fold, Codex never emits workingMode, and the payload normalizer admits monitoring only while state === 'working'.
DeepSeek Flash (free via Pullfrog for OSS) | 𝕏
…they mean Since #22295 a row's combined `state` reads `working` both while the lead's turn runs and while a subagent or background shell outlives a settled lead. The lead's own state now rides beside it (`lead`); each consumer in this slice reads the question it actually asks. - Smart sort and the Activity unread badge keep reading the combined state: their classes and rows are what the sidebar shows. Pinned with tests, including a restored `lead.state: 'working'` row that must never read live. - The stats recorder asks "was an agent executing" and now reads a shared derivation (`isAgentExecutionOwed`): the lead's turn, or live agent child work holding a settled lead's row open. A settled lead's background shell no longer accrues "Time agents worked". Old hosts without `lead` fall back to today's read; restored and replayed rows still never open a session. - The `agent.status.changed` plugin event gains `lead` as an optional field through one tested projection; `state` keeps its meaning and restored rows still project to nothing. - A Codex root Stop that follows an inferred interrupt keeps the `cancellation` verdict, as the Claude lane already does at its turn boundary, on both the hook and relay paths.
… the recorded span The recorder's move to the lead fact quietly changed one more story: a Codex child's PermissionRequest turns the combined row waiting while the root's own turn keeps running. The old state read closed the span there and minted a second spawn on resume; the new read keeps one span, because the lead never stopped. Pin it at both boundaries (the shared derivation and the recorder) so the change is deliberate, not incidental.
…he accrual predicate to stats The recorder dated a start by the producer's mainAgent.stateStartedAt. An SSH host stamps that with its own clock while every stop is stamped locally, so each span gained or lost the clock skew. The same clock also survives a row that briefly lost the fact (an OSC repaint to another state), dating the reopen before the close already sent, and a subagent reopening a monitoring row took the row clock the hook lane pins to the main agent's turn start, re-billing the whole watch-loop window. Edges now use the row clock when the row settles or pauses and the evidence clock otherwise. Rename isAgentExecutionOwed to isAgentTimeAccruing and state that it is the stats question, not a liveness gate: it excludes watch loops, which lifecycle gates must keep treating as live. Note on the plugin schema that mainAgent.stateStartedAt is the execution host's clock.
…whoever asked Time agents worked now accrues only while the combined row reads working. A child's approval or question wait pauses the clock exactly like the main agent's own prompt, and the pause edge is dated by the row's own clock.
…e edges by the row's own state Time agents worked now accrues while the combined row reads working and is not a watch loop. The shared fold emits monitoring only for a settled main agent, and hosts that predate the main agent fact did the same, so this is the same answer on every new-host row without reading mainAgent, and it applies the watch-loop rule to older hosts too instead of billing their monitoring windows. An edge that leaves working is dated by the row's state clock; an edge inside working is dated by the evidence clock. This also stops a live repeat of a hydrated working row (any row without the main agent fact, such as an OSC row) from dating its start at the persisted state clock from the earlier runtime.
Grok's end-of-turn Stop lists each in-flight background task with its type (shell, monitor or subagent). Orca filed a running subagent with the shells, so a Grok subagent that outlived the main agent read "Monitoring background tasks" and, with the stats recorder now skipping watch loops, stopped the "Time agents worked" clock. Map shell and subagent entries to the shared child-work kinds and let the shared liveness classifier decide: any live subagent keeps the pane working, a shell alone or an active stop hook stays monitoring, monitors stay excluded.
…ccrual check Since the shared fold learned a child's human wait, a waiting child makes the row wait, so it must not accrue agent time whatever the main agent is doing. The exhaustive check now includes that input.
ba77215 to
91439ef
Compare
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
- Grok background tasks are now classified by kind.
grokFiniteTaskKindmaps abackgroundTasks[]entry'stype(subagent→ agent work,shell→ watch work, everything else excluded), andgrokChildWorkLivenessAfterStopaggregates them through the sharedagentChildWorkLiveness, falling back tomonitoringonly for an active stop hook. - A live Grok subagent holds the row as plain
working, notmonitoring. The parity story and the listener tests pin adonemain agent beside a listed subagent to{ state: 'working' }with noworkingMode, so its time counts toward "Time agents worked" and the sidebar reads "Working". A shell or an active stop hook still readsmonitoring. - A discriminating end-to-end test.
server-grok-background-status.test.tsdrives the real server and recorder: a subagent keeps one open session across the main agent's end of turn, while a shell closes at the stop and opens a second session on the wake-up turn — the assertion would fail under the old "all finite tasks are watch work" rule. - Completion notifications unchanged. A new notification test pins silence while a Grok subagent outlives the main agent, then one announcement at the terminal turn; a cancel or session boundary still settles the pane.
I re-ran the four touched test files (64 tests) against this head — all pass. The "in-flight only" premise the classification rests on is supported by the captured fixture, which empties backgroundTasks once the task completes, so ignoring status is safe.
DeepSeek Flash (free via Pullfrog for OSS) | 𝕏
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
- Every-input accrual check now covers a waiting child. The fold cross-product test adds
'waiting'to the liveness inputs and assertsisAgentTimeAccruingis false whenever a child waits on a human, whatever the main agent is doing — correct for every fold branch. - Integration with the rebased base. The branch now sits on a
mainwhose fold ranks a child's human wait above live work (childWorkLiveness === 'waiting'→state: 'waiting').isAgentTimeAccruingalready returns false for that row, so no code change was needed; the Grok path is unaffected because its candidates carry nostateand so can never produce the newwaitingarm.
I re-ran the six touched/dependent test files (96 tests) against this head — all pass.
DeepSeek Flash (free via Pullfrog for OSS) | 𝕏
Review status: ready for merge reviewHead: What this PR now does, as the user sees it
Review process
Product decisions made during review
Manual QA
Known limits, deliberately out of scope
|

ELI5
Since #22295 a status row can say "working" for two different reasons: the main agent is still on its turn, or the main agent has finished and one of its helpers (a subagent, or a background shell it left running) is still going. PR #22452 made the row carry the main agent's own state beside the combined one. This change goes through the four readers this slice owns, writes down which question each one is really asking, and moves only the ones that were asking the wrong one. Two readers keep reading the combined status because that is what they mean. One reader (the stats recorder) switches to "is an agent actually executing". The plugin event gains the new fact without changing anything it already said.
What Changed
Which question each reader asks, and what happened to it
resolveAttention)state. Pinned by tests so a future mechanical migration is a deliberate change.countActivityUnread)state. Pinned by tests.AgentSessionTransitionRecorder)agent.status.changedstatemust keep its meaningmainAgentas an optional field besidestate.Before and after, as the user experiences it
agent.status.changednow also receivesmainAgent: { state, outcome?, stateStartedAt }when the row carries it.stateis exactly what it was.mainAgent.stateStartedAtis stamped by the machine the agent runs on (a remote machine over SSH), unlikereceivedAt.The mechanism
isAgentTimeAccruing, sits beside the status fold that produces the row:state === 'working' && workingMode !== 'monitoring'. The fold emitsmonitoringonly when the main agent has finished and it was told only watch work still runs, and every lane goes through it (Codex never emitsmonitoring: it counts every child as agent work), so aworkingrow that is notmonitoringis exactly the main agent's turn or its live subagent work. Which children count as watch work is each lane's call, made through the shared child-work classifier: Claude and structured chat pass only shells and monitors, and Grok now maps each entry of its end-of-turn task list to the same kinds (asubagententry is agent work, ashellentry is watch work;monitorentries stay excluded as before, since they can run indefinitely). A test runs every input the fold accepts and checks the derivation against the rule written in terms of the main agent and its children. Awaitingorblockedrow never accrues, whoever raised the prompt. It does not readmainAgent, so hosts too old to send it get the same answer. It is deliberately not a liveness answer: a background shell is still live work.mainAgentsays. Each edge is dated only by clocks this machine stamped, chosen by the row alone: an edge that leavesworkingis a state change, so the row's own state clock dates it; an edge insideworking(a watch loop starting or giving way to agent work, or the first live update after a restored row) leaves that clock on an older state start, so the evidence clock dates it. On a live update that just turnedworking, the two clocks are the same instant. The main agent's ownstateStartedAtis never used for stats: over SSH it is the remote machine's clock.mainAgentthe bus considers malformed cannot take the whole event down.Restored rows, per reader
mainAgent.state: 'working'never opens a session (test, ablated).mainAgentreads working (test, ablated).mainAgent.state: 'working'row, and ablated by deleting that gate.Why
stateon purpose. The smart sort's classes and the Activity feed's rows are presentation, and both are built from the combined status the row displays. Ranking a working row among the finished ones, or counting an unread "done" event the feed does not show, would make the badge and the order disagree with the rows. The alternative, migrating every consumer tomainAgentmechanically, was rejected for exactly that reason.mainAgent. An earlier revision decided frommainAgentand fell back tostatewithout it. That gave the same answer on every row a current host produces, but made the recorder depend on a second fact with its own clock, and each review round found another edge dated by the wrong clock. ReadingstateandworkingMode, which every host already sends, removes that dependency, and older hosts follow the watch-loop rule instead of being exempt.Linked Issue
Follow-up to #22452 (merged) and #22295. No separate issue. A late Codex root Stop keeping an inferred cancellation, which this PR originally carried, landed in #22452, so the plugin event's
mainAgent.outcomeis already correct on that lane.Visual Proof
Run in the app (hidden dev build of this branch, real Grok 1.0.41, isolated profile). The sidebar order and the Activity badge are unchanged by construction and pinned by tests.
Grok background subagent after the main agent's turn ended: the row reads Working (before this change it read "Monitoring background tasks", and the time did not count under the new stats rule)
Grok background shell after the main agent's turn ended: still Monitoring background tasks
Stats pane. After the subagent run: 2 spawned, 1m worked (exact store total 73.7s for a 75s subagent). During the shell's ~90s monitoring window the exact total stayed at 82.0s (only the main agent's own 8.3s turn was added). After Grok's completion turn: 86.1s. The card shows whole minutes, so it reads 1m throughout.
Subagent finished: row settles to Done
Testing
At the final head, rebased on current
main:pnpm tcexit 0;pnpm exec oxlinton the 15 changed files exit 0;pnpm run check:code-quality:changedpassed (0 new findings).pnpm testoversrc/main/stats, the Grok status-store test, the Grok hook-listener and completion-notification tests, the fold, main-agent parity and child-work liveness tests,src/main/plugins,src/shared/plugins, smart sort and Activity unread: 75 files, 590 tests passed.New tests drive the real status store: a row saved as
workingand restored an hour later opens its first live session dated at the live event, not the saved clock; a Grok background subagent keeps one stats session open across the main agent's end of turn, and a Grok shell closes it. The fold test checks the accrual rule against every input the shared fold accepts, including a child waiting on a human.Deletion ablations, run when each behavior landed during review (the last one at this head), each file restored from a saved copy and hash-checked. Every one went red for the pinned reason:
monitoringexclusion deleted: 7/39;mainAgentdeleted, and the restored-row skip deleted: both red;mainAgent: both red.A real Grok 1.0.41 Stop hook payload was captured to confirm that a background subagent is listed as
backgroundTasks: [{ type: "subagent", status: "running", ... }].In the app: the three scenarios in Visual Proof (at the pre-rebase head
ba772157ea, which carries the same PR changes).Platforms: macOS only. No platform branches.
I manually tested these changes locally (hidden dev build, real Grok: see Visual Proof)
Automated tests added/updated, or explained why not below
AI Disclosure
Author: @BrennanKB5
Review
isAgentTimeAccruing, which excludes amonitoringrow on purpose.typeofshell,monitororsubagent; Orca used to treatshellandsubagentalike as watch work. It now feeds them to the shared child-work classifier, so the Grok row follows the same "any live agent work wins" rule as Claude and structured chat. A Grok pane on an SSH host whose relay predates this change keeps the old reading (Monitoring, not counted) until that relay updates, because the host normalizes Grok's events. Hook events Grok sends from inside a subagent's own session are still dropped, as before, so a Grok subagent's approval prompt is not seen: the row keeps reading Working and that wait counts as agent time, unlike a Claude or Codex helper's wait.mainAgent, so an older host's watch-loop row stops the clock like a current host's. Hosts from beforemainAgentproducedmonitoringonly after the main agent finished (Claude's pane resolver and Grok'sstophandling), so this applies the same rule, not a guess.Agent skill upstream boundary
docs/reference/agent-skill-sharing-upstream-boundary.mdand copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.Notes
mainAgenton the plugin event is a new optional field (rule 1 of the wire compatibility reference). Nothing a paired client and host exchange changes; the recorder and the plugin tap both read the host's own store.Checklist
N/Awith reasonpnpm lint,pnpm typecheck,pnpm test, andpnpm buildpass (or CI will cover; local preferred)