Commit Graph
2 Commits
Author SHA1 Message Date
Brennan BensonandMerge Sim ebb1acfa37 refactor(agent-status): publish structured sessions into the hook server store (#19683)
* refactor(agent-status): publish structured sessions into the hook server store

Structured (native chat) sessions have no PTY and no hook script, so their
status never reached the hook server's store; #19217 gave `worktree ps` its
own adapter over the structured feed instead. The feed now writes every
projection into that store through a status sink the runtime wires, drops
the row when the host closes the session, and `worktree ps` reads the one
snapshot like every other agent.

Rows carry a `structuredHost` marker and the journal clock; they are never
persisted to last-status.json, and the main process does not forward them
to the renderer yet, whose feed bridge still owns them until it is retired.

Design and the two follow-ups: docs/reference/agent-status-store.md.

* chore: drop stray @pnpm/exe lockfile entry

An unrelated local pnpm run added @pnpm/exe as a packageManagerDependency
with no package.json change, so CI's --frozen-lockfile install failed
before any job ran.

* docs(agent-status): describe the step that actually landed

The design record claimed PR 1 deletes RuntimeAgentRowStore, drops the
retained-versus-hook reconciliation, stamps terminalHandle on OSC rows, and
tags rows with a source field of 'structured-host'. None of that is true of
the shipped code: the retained store and its reconciliation are still in
place, and the row field is structuredHost: 'held' | 'owned'.

AGENTS.md points every future contributor here before they touch agent
status, so split the roadmap into the 1a that landed and the 1b that has not,
and name the fields the code actually writes.

* fix(agent-status): pair session removal with the status-row forget

A session dropped from the host's map without an explicit forget left its row
in the store forever: `structuredHostOwned` bypasses the staleness check, so a
failed re-attach (the Claude rewind path reaches one) stranded a permanently
working agent in `worktree ps` and on mobile with no UI able to clear it.
Deletion and forget are now one operation both callers route through.

* fix(agent-status): give orcad the store worktree ps reads from

`orcad` constructed its runtime with neither `getAgentStatusSnapshot` nor
`structuredAgentStatusSink`, so once `worktree ps` sourced rows only from that
snapshot the headless host published nowhere and listed nothing. The hook
server's store is a module singleton whose import tree never reaches Electron,
and its file paths come from `start()`, which orcad never calls.

* fix(agent-status): drop a structured row without a renderer clear

`dropStructuredStatus` went through `clearPaneState`, which fans a pane clear
out to the renderer for a pane key the renderer's own feed bridge still writes
- so 'exactly one writer per pane key' held for writes and not for deletes.
`dropStatusEntry` routes through the status-drop tap instead, and skips the
resume-identity remnant: a structured session has no pane to resume into, and
every null-status publish would otherwise re-mint one.

* test(agent-status): pin both half-migration structured-row filters

Neither the `agentStatus:getSnapshot` filter nor the main-window listener's had
a single assertion, so deleting either — the first step of PR 2 — was green
everywhere. Also covers the perf skip and the drop's lack of a renderer clear.

* docs(agent-status): correct three statements this PR made false

The sink JSDoc claimed only tests construct a host without one; `orcad` did.
The doc argued a structured row needs no tab mirror 'because headless serve has
no renderer', reasoning about exactly the topology the wiring had not reached.
The deleted runtime adapter's warning that the pane key must be the DERIVED one
- never a bearer handle or minted worker key - was lost with it.

* test(agent-status): declare orcad in the hook-row producer census

Wiring the hook store into the orcad runtime added a production site that
hands hook rows to a consumer, which the census ratchet pins deliberately.

---------

Co-authored-by: Merge Sim <sim@local>
2026-09-10 11:27:27 -07:00
Brennan BensonandMerge Sim 1c1cb7115a fix(orchestration): unify structured-session status across worktree ps and sidebar (#19217)
* fix(orchestration): stop worktree ps reporting a busy structured session as idle

A worktree running a structured Claude or Codex chat read as idle to `orca
worktree ps`, while the desktop sidebar showed the same session working. The
sidebar was right: the host already projects a status summary for every
structured session and publishes it, and the renderer maps it into an agent
row. `worktree ps` simply never consumed it, so the agent-facing surface was
the blind one.

Structured sessions have no PTY, so they reach neither the hook snapshots nor
the retained ones that every other row is built from. This reads the summaries
the host has already published and applies the same projection the sidebar
does — working, attention as blocked, otherwise done — so the CLI and the GUI
cannot disagree about one session.

Two things worth knowing:

The connected-PTY evidence gate had to be skipped for these rows. It exists to
drop a row whose PTY is gone, which is the wrong question for a session that
never had one; a structured row's liveness evidence is the status feed that
produced it. The exemption is keyed on the row being structured, so every
PTY-backed row keeps today's behaviour.

`RuntimeWorktreeAgentRow` needed no change. It was already a non-PTY shape —
paneKey, state, agentType, and no ptyId, connected or writable — so a
structured row fits without inventing a fake terminal coordinate.

The pane key is the DERIVED one the renderer already publishes, never the
orchestration bearer handle or the minted worker pane key: both are
credentials, since `orchestration check` is identity-gated and accepts a
caller-supplied pane key.

`orca terminal list` is deliberately untouched, and a test pins that. Adding
rows there breaks real consumers: mobile mounts a terminal WebView per row that
can never receive a frame, a `connected`-keyed refresh check goes permanently
true and pins shipped clients to a fast cadence with no exit, and the plugin
projection has no field that can carry `writable: false`. Every safe consumer
of a terminal summary checks `ptyId`; the breaking ones key off `connected` or
mere row presence, which no added field can qualify. An honest partial-listing
count there is a separate change.

* fix(runtime): report only live structured sessions in worktree ps

The status feed's `published` map is a broadcast cache, not a roster. It
deliberately never retracts — an evicted idle session is still idle, and a
reloading renderer must not lose every settled row — so enumerating it lists
every session the host has ever opened, and eviction's `forget-session` step
deletes the session from the live map while touching nothing else.

Reading it as a roster made `worktree ps` report a closed chat forever. The
sharp edge is a chat closed while an approval was pending: a deliberate close
does not settle a pending prompt, so the retained summary stays `attention`,
maps to a `blocked` row, and merges the worktree to `permission` for the whole
30-minute freshness window — on the CLI and on the mobile sidebar it backs.

The poller now answers from the sessions the host still holds, intersecting the
live map with the retained projections. `subscribe()` and its snapshot are
untouched: retention there is the point. Gating on the live session set rather
than the visible tabs keeps a headless orchestration worker listed, which is
what the agent-facing surface is for.

Also folds out two things the enumerator left behind: the working/attention/idle
to working/blocked/done mapping now lives once in the shared projection module
instead of once per process, which is what actually enforces "the CLI and the
GUI cannot disagree"; and the structured row source no longer builds a
write-only `payload` behind an `as` cast that compensated for nothing. The
structured source construction moves to its own module to keep
runtime-worktree-agent-rows.ts clear of the 300-line cap.

* test(runtime): execute the structured-host call site in worktree ps

Both structured-row suites called attachRuntimeWorktreeAgentRows directly
with summaries they built themselves, so nothing ever ran getWorktreePs's
own `getStructuredAgentSessionHost()?.liveSessionStatusSummaries()`. That
file carries `@ts-nocheck`, so renaming the accessor was green in typecheck
and in the suite, while `orca worktree ps` and mobile's 3s poll would throw
a TypeError for every user — the `?.` optional-chains the host, not the
method. Swapping the call back to a whole-cache read was equally invisible:
the liveness suite injects feed.liveSessionSummaries() itself, and the
string-match guard only needs the identifier to appear somewhere in the file.

Drives the real runtime with a stub host over a real status feed that has
published two sessions and forgotten one, asserting the live session's row
reaches ps output, that the live accessor is the one called, and that the
`?? []` fallback still returns a page with no host installed. The stub is
typed against the real host, so a class-side rename reddens tc here.

* refactor(runtime): admit agent sources before worktree row projection

* fix(runtime): preserve host-authoritative structured status

* Fix structured host session activity lifecycle

---------

Co-authored-by: Merge Sim <sim@local>
2026-09-08 21:50:51 -07:00