mirror of
https://github.com/stablyai/orca.git
synced 2026-09-25 16:02:38 +00:00
stack-foundation
4
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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> |
||
|
|
266b2ea190 |
fix(agent-status): report a stale pane that still holds a PTY as unverifiable, not idle (#18012)
* fix(remote): stop one unlabelled inventory tombstoning a live worktree mirror #11495 Step C. `buildMissingWebSessionTabsRemovals` synthesised a `removed: true` tombstone -- emptying a worktree's entire mirror -- for any tracked worktree absent from a single inventory frame, without ever consulting the host's own authority label. `mirror-settle` already refuses to settle an *empty* inventory that is not `authoritative` (#16414, #16546); the strictly more destructive action was ungated. An inventory the host labels `authoritative` carries a complete PTY census, so one omission is host attestation and removal stays immediate. An unlabelled inventory is a degraded or version-skewed census: `unverifiable`, not `exited`. It must now repeat before it can destroy anything, reusing the two-observation shape of `confirmSurfaceInventoryAbsence`. A legacy host that never negotiates the capability still converges after two rounds, so ghost rows cannot outlive the fence. The 14 tests from #13621 that blocked this were all written before the `authoritative` label existed (#13621 landed 2026-08-11; the capability landed 2026-08-26 in #16546). #13621's own summary says "Reconcile each resumed host from an authoritative inventory, including removals", so their fixtures are retargeted to say so explicitly rather than weakened. Refs #11495 * fix(agent-status): stop a reconnect replay restamping the staleness clock #15317 correctness half. `receivedAt` was doing two jobs: delivery order and evidence age. A relay reconnect replays every cached row, and `receivedAt` must restamp to clear the connection watermark that `clearStatusEntriesForConnection` raises -- so a pane stuck at `working` had its 30-minute deadline pushed out by another 30 minutes on every reconnect. The TTL was never reached, which is why this read as a tuning question. Two clocks, not one rewritten clock: - `receivedAt` is untouched. The transient-clear watermark and the four `<` ordering drops (`agent-status-event-applicator`, `agent-status-live-entry-builder`, `agent-status-cleanup-actions`) keep working unchanged. Restamping a replay with its original time would have made it `<= watermark` and dropped it outright, leaving the pane with no row at all. - `evidenceObservedAt` is new, optional, and read only by the staleness comparison (`isFreshNonDoneAgentStatus`, `isExplicitAgentStatusFresh`, the freshness scheduler). Main holds it per pane across the transport clear -- the clear deletes the row on purpose, but the *age* of evidence a later replay restates is not a claim about the pane. Absent means "no separate observation", and every consumer falls back to `receivedAt`/`updatedAt`, so old hosts and old rows behave exactly as today. Behaviour: a genuinely active pane keeps stamping the observation clock from its real events, so it stays `working` across a reconnect. A pane whose relay restarted replays nothing and still falls through to title evidence. A torn-down pane drops its remembered clock in `clearPaneState`, so a reused pane key cannot inherit one. `AGENT_STATUS_STALE_AFTER_MS` is deliberately unchanged -- the window length remains a product decision. Refs #15317 * fix(sidebar): stop a stale agent row claiming the pane is empty A stale non-`done` entry decayed to `idle` whether or not Orca still held the pane's PTY, so "we lost the reporting stream" and "nothing is running here" were the same display class. Split the destination on evidence already computed: with a live PTY the row is `unverifiable` and reports the observer's own fact — how long the silence has run — so the user can apply context Orca has no way to know. With no PTY it stays `idle`. Smart sort gains class 4 for it, between working (3) and idle (now 5): still plausibly the most important pane, never outranking one that is reporting, and never a claim that the agent finished. `unverifiable` stays renderer-local; the dashboard card projection publishes today's `idle` because that vocabulary is validated against a fixed allowlist in main and read by older pop-outs. AGENT_STATUS_STALE_AFTER_MS is unchanged. * fix(agent-status): decay a mirrored remote row on the replica's own clock A paired client mirrored a remote host's status rows verbatim, host wall clock included, and the staleness gate then computed `rendererNow - hostStamp`. The effective window was 30 minutes plus or minus the two machines' skew: a host running fast held every remote row permanently fresh, a host running slow decayed them on arrival. The constant was never the lever there — the subtraction straddled two clocks. The replica now stamps `mirroredEvidenceReceivedAt` from its own clock when the authority's observation advances, carries it forward across an exact repaint (a restated observation is not a new one), and decays against it. Both sides of the subtraction come from one machine; locally observed rows carry no stamp and are unchanged. The alternative the type comment named — carrying the authority's freshness verdict — was rejected: a verdict is computed at publish time and cannot age between snapshots, so once the host goes quiet the replica would hold `fresh` forever. That is precisely the loss-of-contact case the window exists for. AGENT_STATUS_STALE_AFTER_MS is unchanged; the clock rules move to agent-status-freshness.ts to keep agent-status-types.ts under its line budget. |
||
|
|
94f231737d |
fix(agent-status): retire panes whose agent process is gone (STA-4612) (#15212)
* fix(agent-status): retire panes whose agent process is gone (STA-4612) Agent status can hold `working` on a pane where no work is outstanding, and nothing closes the gap. A pane's Claude state is a join of a lead turn and three latches — the subagent roster, the background-task gate and the session-cron gate — and each is set by a hook and cleared only by another hook. Claude Code emits no terminating hook on `/exit`, `/clear`, Ctrl+C, crash, SIGKILL or terminal close, so every one of those latches is a claim with no owner and no expiry. The join is also materialised at ingest time and persisted, so a stale `working` survives restart and blocks hibernation, which requires `done`. Registering `SessionEnd` is not the fix: it covers roughly a third of exit paths (measured on 2.1.231/2.1.233; upstream anthropics/claude-code#17885 and #6428 are both closed as not planned). Nor is a TTL — `AGENT_STATUS_STALE_AFTER_MS` only decays the sidebar dot at read time while the stored row stays non-terminal. So the backstop is built from evidence Orca already owns. A session id that changes means the conversation was replaced. On the first hook of the new session — whatever that hook is — the previous session's own claims are void: its session crons and its one-shot subagents. Deliberately not voided: the background-task gate (a background shell is an OS process that survives `/clear`, and the previous inventory is positive evidence it was running), and `confirmedTeammate` rows (persistent in-process teammates a lead swap cannot end). The lead record is left to the incoming event's own fold. A certified process exit retires the pane. Orca already does this on every attributable PTY exit — `clearProviderPtyState` resolves the pane key and calls `clearPaneState` — but that resolution depends on the spawn-time `ptyPaneKey` mapping, which a restored or reattached PTY may never rebuild. Those panes keep their row and latches for good. `onPtyExit` knows the keys teardown could not resolve, so it reconciles them from its own records. The certificate is `exitCode >= 0 || hostExitConfirmed || providerExitObserved`: a synthetic `-1` from a failed stop is not a death (the PTY can have survived it), while a real exit can also report `-1`, so neither the code nor the SSH surface predicate is sufficient alone. `providerExitObserved` is additive and separate from `hostExitConfirmed`, which also drives the liveness verdict and the SSH surface decision. A confirmed shell foreground is the `/exit` case: the agent died, the shell lived. That already dropped the row, but through `agentStatus:drop`, which by its own contract preserves a live pane's caches — so every latch survived and the next event resolved the pane back to `working`. It now routes through the reconciler instead, gated on a per-pane accepted-status generation rather than row identity: the confirming process read can take seconds, and `updatedAt` cannot order two writes inside one millisecond (the store deliberately admits equal timestamps). Cold start generalises the same way. The startup sweep required a restored subagent roster, so a stranded lead row, background-task gate or cron gate — the shapes with no child event left to reap them — were never candidates. Hibernation needs no change: with the above, those rows become genuinely `done` and the lockout resolves through the front door. A `restoredUnconfirmed` bypass in the planner would let it reclaim the heap of an agent that may be working. Not included: folding `background_tasks` from a child-attributed `SubagentStop`. Writing its test surfaced #11838's deliberate assertion that child inventories are not authoritative for lead-owned background work, and the listener says the same — "background_tasks is trusted only where unambiguous". An empty list on a `SubagentStop` does not prove the lead's shell ended, so the fold would have cleared a gate on evidence that establishes nothing. STA-4119's live-side question — whether a genuinely live background shell should hold the lead row after the lead turn ends — is untouched. This change extends gate-clearing to zero new triggers. * fix(agent-status): make the confirmed-shell reconcile survive its own drop The /exit leg never fired. `settleDeferredCommandFinishedStatusDrop` runs the paired drop before the reconcile, and `dropAgentStatus` cleared the per-pane accepted-status counter the reconcile's guard then read — so the guard compared a live anchor against a zeroed counter and skipped itself on every pane that had a status row, which is every pane worth reconciling. The existing test passed only because it used a pane with no row, where the drop early-returns and both sides read 0. Stop keying the guard on a counter a sibling teardown path can reset: the ordinal is now stamped on the row itself, derived from the row it replaces, so there is no side table to clear and a batched burst lands the same ordinals as the equivalent sequential writes. A removed row means "nothing reported", which is exactly what the paired drop leaves behind. Also: - Keep the `providerSessionOnly` resume identity that the paired dismissal mints when the shell outlived the agent; a certified PTY exit still takes it, since there is no pane left to resume into. - De-vacuum two guard tests. The confirmed-teammate pin never anchored a session owner, so the void it claimed to survive never ran; the unavailable-inspection pin asserted before the confirm ladder settled. Both now fail when their guard is removed. - Derive `hasLiveClaimsForPaneKey` from a predicate that lives beside `clearPaneCacheState`, so a new latch cannot be added to the teardown and silently missed by the claim check. - Drop the unreachable compact-`trigger` clauses; SessionStart is the whole guard. - Cover the connectionId arm of the exit certificate, where a provider-observed death and a preserved SSH surface are deliberately independent. * fix(agent-status): keep agent-status-types under its line cap main already sits exactly at the 300-line max-lines cap for this file, so the single `acceptedStatusSeq` field this branch adds pushed it to 301 once main's observation facet merged in. Declared the field as a mixin beside the observation facet instead. Both are per-write facets mixed into `AgentStatusEntry` rather than fields a reporter supplies, so they belong together — and the capped file loses a line rather than gaining one, since it already imports from that module. No lint suppression. * fix(agent-status): collapse the entry facets into one intersection The previous attempt still tripped max-lines: two mixins on one intersection wrap across two lines under oxfmt, so removing the field line bought nothing. Expose a single AgentStatusRowFacets that already includes the observation facet, so the entry intersects one short name on one line. The payload keeps intersecting the observation facet alone — it must not carry the renderer-local ordinal. Verified by formatting first and then linting, which is the order that catches this. * fix(agent-status): retire resume authority with dead panes |
||
|
|
26bdfc0fe4 |
feat(agent-status): stamp observation provenance at every status ingress (STA-4293) (#14706)
Add an optional `observation` facet to agent status rows recording the origin (hook | osc | title | process | launch | orchestration), the authority that sequenced it, a per-pane incarnation, a monotonic revision, and the authority's own clock. Stamp it at every ingress; no consumer reads it. Boundary is stamped from the hook listener's existing per-provider `isNewTurnEvent`, not a second list of event-name literals. Identity-only (`providerSessionOnly`) rows are tagged `kind: 'identity-only'` so future consumers do not each rediscover that they are not turn transitions. The staleness-decay contract is documented at the type: staleness must be computed against the same authority clock that stamped `observedAt`, or replicas must decay on local receipt time. Not fixed here. Behavior-neutral: optional field on existing JSON, never persisted, never published to paired clients, and never inherited across writes. |