Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -108,3 +108,33 @@ scoping delivery-input watching is a prerequisite, not a precaution.
- Root DQ2 and DQ3 are updated: pi gains an evented signal, the observed half
of DQ3 is specified here, and the declared half (activity status, plan,
plan step) plus supervisor-following behavior remain open.

## Amendment 1 — PTY session fidelity composes at read time

Accepted by Johannes on 2026-08-26 after deploying this decision's original
implementation to a 627-seat downstream catalog. The envelope and readers were
live while zero seats produced a record: the deployment launched harnesses
outside st2's native drivers, and nothing enforced or measured the assumption
that a driver owned every session. The original decision remains correct for
the **fine driver record** and wrong as the only coverage mechanism.

Observed harness state therefore gains an additive
`fidelity ∈ driver | session` discriminator and a launcher-agnostic session
projection:

- `driver` retains this decision's full record, fencing, freshness, and
complete tuple semantics;
- `session` is a read-time projection over the canonical PTY session's
persisted `lastOutputAtMs`, covering only `state` and `since`; its
blocked/input/ask axes remain `unknown`;
- a fresh definite driver observation wins; session fidelity replaces a
missing or derived-unknown driver observation;
- the session projection writes no `harness-state` record and creates no
second writer class.

The PTY daemon is the observer because it already processes every output byte.
st2 does not know or branch on the launcher — the canonical agent task's PTY id
is the bus id regardless of whether the child is axe, a native st2 driver, or a
future launcher. The decision rejects `pty stats` polling, scrollback deltas,
and event-stream following based on the fleet measurements in
[`05-harness-state/.experiments/2026-08-26-launcher-independent-session-activity.md`](../05-harness-state/.experiments/2026-08-26-launcher-independent-session-activity.md).
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# Launcher-independent PTY session activity

2026-08-26, Linux, a downstream catalog with 627 declared seats and 60 live PTY sessions during the measurement. The investigation followed a deployed observed-harness-state envelope whose reader was live in st2 and fractal while every seat still returned `observedState: null`.

## Question

What is the lowest-global-complexity, launcher-agnostic source of coarse harness activity for every managed session, and can it remain efficient in a busy 627-seat catalog?

## Method

The investigation traced the deployed producer/read paths in st2, identified the process refreshing presence, inspected the PTY daemon's output and persistence paths, measured the existing candidate surfaces, and compared their asymptotic and measured fleet costs.

Reproduction commands:

```sh
# Current-scale cost and live-session count
time pty stats --json > stats.json
jq 'length' stats.json

# Persisted registry shape and event distribution
jq 'keys' <pty-root>/<session>.json
jq -r '.type' <pty-root>/<session>.events.jsonl | sort | uniq -c | sort -rn

# Direct-read baseline (single process)
time jq -s 'length' <pty-root>/*.json >/dev/null

# Source contracts
rg -n 'scrollbackUsed|scrollbackCapacity|ptyProcess.onData' src/server.ts
```

## Result

**Coverage was conditional on the launch path, not the harness inventory.** The deployed catalog launched harnesses through an external wrapper. st2's rich producers run only inside st2's native session drivers (or their hook/channel siblings), while the harness-blind ding sidecar refreshed presence for every live seat. `st2 hooks verify`, presence, and catalog checks were green with zero `harness-state` records. A launcher may therefore adopt the roster reader without any producer; nothing measured that gap.

**The PTY daemon is the universal observer.** Its `onData` handler already receives every PTY output chunk before feeding xterm-headless and clients. Stamping a timestamp there is O(1) and adds no observer, stream, process, or harness/launcher coupling.

**Terminal-buffer deltas are not an activity clock.** `pty stats` reports `scrollbackUsed = buf.length` and capacity `rows + scrollback` (`src/server.ts`). The buffer is bounded; once full, its length stops advancing while output continues. Deriving activity from length deltas therefore fails systematically on the longest-running sessions.

**The existing event stream is sparse, not an output stream.** The three largest sampled event logs carried 840–961 records and were 99% `title_change`; only a few `user.agent.status`, lifecycle, bell, or cursor events appeared. Harnesses and launchers may emit useful semantic edges, but absence of an event is not evidence of idle output.

**Shelling to `pty stats` is too expensive at fleet scale.** One bulk `pty stats --json` snapshot took 520 ms for 60 sessions (~8.7 ms/session), projecting to ~5.5 s for 627 sessions. The cost includes process/resource probes that observed-state composition does not need.

**Direct metadata joins are cheap.** Reading 300 persisted session JSON files took 19 ms in one process (and 379 ms in the deliberately worst process-per-file form). A native Rust reader over the small files is well below the interactive roster budget. No subprocess is required.

**The implemented composed roster remains sub-second at full declared-fleet
scale.** A synthetic catalog with 627 local, live sessions (627 pid probes and
627 metadata reads; half stamped 500 ms ago, half 120 s ago) ran
`st2 agents --json` ten times after one warm-up: 361.89 ms minimum, 394.39 ms
median, 551.58 ms maximum, 412.64 ms mean. The result contained exactly
314 `active` and 313 `idle` session-fidelity observations. The benchmark used
the debug binary, so it is a conservative bound rather than a release-build
claim.

## Conclusion

The global minimum-complexity shape is:

```text
PTY output -> daemon lastOutputAtMs stamp (O(1)/chunk)
-> locked session metadata persist (trailing debounce <= 1/s)
-> st2 read-time join (alive + recent output => active; alive + older => idle)
-> fresh definite driver observation takes precedence
```

The coarse session projection does not write `harness-state`, so it introduces no writer identity, fencing, heartbeat, history, or retention contract. It is launcher- and harness-agnostic. `fidelity = session | driver` tells consumers which axes are proved; session fidelity covers only `state` and `since`.

The implemented benchmark covered all 627 sessions simultaneously live, with
the liveness and metadata join active for every row. The one-second metadata
debounce is a write-amplification bound, not an activity threshold; st2 owns
the 60-second activity window and 30-second future-skew guard. These constants
require tuning only if captured turn streams show maintained harnesses going
silent for longer than the window while still actively producing a turn.

## VRS Impact

- Amend decision 0006: the driver-written record remains the fine layer, not the only coverage mechanism.
- Add OHS-A04 and OHS-R11–R13: PTY evidence, read-time projection, precedence, and fleet cost.
- Extend OHS-R09 with `observedState.fidelity = driver | session`.
- Update the spec's overview, exposure wire, verification plan, and ontology.
- Leave DQ-H5 (remote supervisor semantics) open; session fidelity is deliberately same-host because PTY metadata is host-local.
57 changes: 47 additions & 10 deletions docs/vrs/05-harness-state/requirements.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@ path reads this record.
session's driver processes — the wrapper that owns the presence lease, and
the channel or hook subprocesses it shares its incarnation token with;
nothing verifies that claim.
- **OHS-A04 PTY output is universal session evidence:** Every maintained
launcher runs the agent task in the declared PTY session. The PTY daemon
necessarily observes every output byte to maintain terminal state, regardless
of which launcher or harness produced it. This is sufficient evidence for a
coarse `active | idle` classification, but proves nothing about `blockedOn`,
`ask`, or `inputBuffer`.

## Acceptable Tradeoffs

Expand Down Expand Up @@ -106,7 +112,7 @@ path reads this record.
not the agent directory wholesale. This is a prerequisite: the record sits
in a tree the Codex pump watches unfiltered today.

### Must be produced by drivers under the evidence rule
### Must derive from positive evidence

- **OHS-R05 Driver-owned projection:** Classification is driver work. The
Codex producer projects the existing control state with the corrected rows:
Expand All @@ -133,23 +139,44 @@ path reads this record.
narrowing, not a closure: what it cannot prove, the relaunch-time written
claim supersedes and the staleness horizon bounds. A fresh `ended`
survives the check: a terminal record is supposed to outlive its writer.
- **OHS-R08 All-harness coverage:** Codex, Claude, pi, and OpenCode each ship
a producer. pi's is evented through the injected extension (the positive
idle signal root `DQ2` asks for). OpenCode reaches driver parity first —
typed driver, session wrapper owning the presence lease, then its producer
and native delivery transport.
- **OHS-R08 All-harness coverage:** Every local running agent has session
fidelity independent of its launcher or harness. Codex, Claude, pi,
OpenCode, and OMP additionally ship fine driver producers for sessions the
corresponding st2 driver owns. Fine coverage may vary by launch path;
session coverage may not.

### Must be readable beside declared presence

- **OHS-R09 Roster join:** `st2 agents --json` carries `observedState` beside
declared `status` in one payload — the wedged-agent comparison (declared
`busy`, observed `idle`) must not require joining two commands. Observed
state is a third independent axis: it never rewrites presence, desired
lifecycle, or `lastActivity`, and the pinned roster wire assertions change
lifecycle, or `lastActivity`. `observedState.fidelity ∈ driver | session`
tells consumers which axes are proved: driver fidelity covers the full
tuple; session fidelity covers `state` only and leaves `blockedOn`, `ask`,
and `inputBuffer` `unknown`. The pinned roster wire assertions change
deliberately, in the same change, with the new proof named.
- **OHS-R10 Doctor exposure:** Doctor surfaces observed state for agents it
owns as advisory output — a stale or session-dead record beside a `running`
desired state is worth a warning, never an exit-code failure in v1.
- **OHS-R10 Doctor exposure:** Doctor surfaces composed observed state for
agents it owns as advisory output — fidelity and an indeterminate reason are
explicit; absence names a missing driver record *and* missing PTY activity
stamp. None is an exit-code failure in v1.
- **OHS-R11 Launcher-agnostic session projection:** The PTY daemon stamps
`lastOutputAtMs` while processing output and persists it to the canonical
session metadata, debounced to at most one metadata write per second per busy
session. st2 joins that metadata at read time using the canonical agent task
mapping `pty_id = bus_id`: alive plus output inside the activity window
derives session-fidelity `active`; alive plus older output derives
session-fidelity `idle`; missing liveness or output evidence derives nothing.
st2 never branches on, imports, or names the launcher.
- **OHS-R12 Fine-over-session precedence:** A definite fresh driver record
wins over session activity. A missing or derived-`unknown` driver record
falls back to session activity. The session projection never becomes a
`harness-state` writer and therefore introduces no writer identity, fencing,
heartbeat, or record-retention contract.
- **OHS-R13 Bounded fleet cost:** Output stamping is O(1) in the PTY daemon's
existing parse path. The persist debounce bounds write amplification.
Roster reads consume the small per-session metadata directly; they do not
shell out to `pty stats`, follow event streams, or scan terminal buffers.

## Evidence

Expand All @@ -162,3 +189,13 @@ the Codex `activeFlags` schema present on all supported codex-cli versions
machine and its hold reasons, the unfiltered agent-dir watch beside the
presence refresh that writes into it, and `src/harness_state.rs`, which
implements the envelope this file ratifies.

The session-fidelity measurements were taken 2026-08-26 on a 627-seat
downstream catalog: the shipped envelope had zero producer records because the
launcher bypassed st2 drivers; `pty stats --json` cost 520 ms for 60 sessions
(~5.5 s projected to 627); `scrollbackUsed` is the bounded terminal-buffer
length and saturates; the PTY event log is sparse and title-change dominated;
reading 300 persisted session metadata files cost 19 ms in one process. These
rule out stats polling, scrollback deltas, and event-following in favor of one
daemon stamp plus a direct read-time join. See
[`05-harness-state/.experiments/2026-08-26-launcher-independent-session-activity.md`](./.experiments/2026-08-26-launcher-independent-session-activity.md).
Loading
Loading