Skip to content

Commit ef3c19a

Browse files
schicklingclaudeschickling-assistant
authored
docs(vrs): add the 05-harness-state subsystem intent layer (#320)
* docs(vrs): add the 05-harness-state subsystem intent layer Decision 0006 (observed harness state is a driver-written catalog record), the 05-harness-state requirements/spec/open-questions set, ontology terms with collision rules pinning the new axis against presence, session state, declared activity status, and working state, root DQ2/DQ3 updates, and DELTA-005 fencing the spec landing ahead of its producers. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(vrs): the intent layer carries the ask axis, token ownership, and the real writer set The v1 contract states ask and incarnation beside the original tuple, the codex/claude producer sections name their ask kinds, and OHS-R01/OHS-A03 name the actual writers — the owning session's driver processes sharing one incarnation token — instead of the wrapper alone. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(vrs): the record example carries its ownership fields incarnation and seq appear in the wire example with the directional ownership rule and the legacy missing-field derivation stated beside them. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(vrs): written claims in the contract, and an honestly scoped cross-check OHS-T04/OHS-R07 no longer overclaim: the probe narrows the ungraceful window only for provably dead sessions (pidfile present, process gone), pty kill's pidfile removal is named, and the relaunch-time written claim is what supersedes what the probe cannot prove. The ownership bullet carries the written-claim semantics. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(vrs): review is a reserved ask word — codex review holds nothing on a human Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(vrs): the OpenCode source stays conditional at this layer, and the ungraceful window is honest same-host too Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(vrs): the claim sequence is floor-aware, and the sidecar and its residual are on the record The spec now states the shipped algorithm — a claim mints one above max(on-disk sequence, .harness-state.seq floor) — and documents the sidecar file and the accepted both-files-damaged residual instead of asserting a plain on-disk-plus-one. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(vrs): review is not a human-blocked state in OHS-R05 The normative requirement still grouped Review under blockedOn:human while the corrected projection maps Held{Review} to active with no block — a producer built from the requirement would report 'blocked on you' during autonomous review. The grouping now matches the projection. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> agent-identity: unknown agent-persona: generalist agent-supervisor: unavailable agent-tool: OMP agent-tool-version: 18.0.3 agent-runtime: OMP 18.0.3 tooling-profile: dotfiles@f33cd9c-dirty --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: schickling-assistant <261620128+schickling-assistant@users.noreply.github.com>
1 parent 06e3e5b commit ef3c19a

7 files changed

Lines changed: 658 additions & 4 deletions
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
# Observed harness state is a driver-written catalog record
2+
3+
Status: accepted
4+
5+
Design decision made by Johannes on 2026-08-23 (interview over three research
6+
reports and issue #268, which carries the measurements cited below). Merge and
7+
acceptance approval required: upstream maintainers.
8+
9+
## Context
10+
11+
st2 exposes what an agent *declares* — presence, on a five-minute heartbeat
12+
with a fifteen-minute staleness horizon — and nothing about what its harness is
13+
*observed* doing. The one observed state machine that exists,
14+
`CodexObservedState`, persists host-local under `$XDG_STATE_HOME`, has zero
15+
production readers, and is invisible to the roster, Doctor, and every remote
16+
supervisor. Claude, pi, and OpenCode have no observed-state type at all. The
17+
concrete consumer is the one #261 and #268 name: a downstream TUI rendering a
18+
seat's working state, which today cannot distinguish "alive and doing nothing"
19+
from "recently active" because `lastActivity` advances on the agent's own
20+
heartbeat. #162 assigns provider-specific idle/active classification to
21+
drivers and keeps the generic envelope in core; this decision fills that
22+
envelope.
23+
24+
## Decision
25+
26+
Observed harness state is **one driver-written record in the catalog**,
27+
`<agent-dir>/harness-state`, schema `st2.harness-state.v1`, carrying the full
28+
tuple in v1: `state ∈ idle | active | child | ended` (`unknown` derived, never
29+
written; `child` reserved with no producer), **`blockedOn` as a field**
30+
(`human | none`), and `inputBuffer ∈ empty | nonempty | unknown`. The record
31+
follows the presence record's transport discipline — embedded origin
32+
timestamp, atomic tmp+rename, byte-distinct on every write — with its own
33+
staleness constants, deliberately not aliases of `status::STATUS_*`. Freshness
34+
is **transition writes plus a slow heartbeat** on the existing five-minute
35+
presence cadence, and a writer that loses sight of its harness stops
36+
heartbeating rather than refreshing a state it cannot see. **All four
37+
harnesses ship producers**: Codex projects its existing control state, Claude
38+
combines hooks with a wrapper-owned terminal write, pi exports the injected
39+
extension's positive idle signal as an evented producer, and OpenCode gains a
40+
full driver — session wrapper, presence lease, state producer, and a native
41+
delivery transport at parity with the other drivers. Exposure is **joined into
42+
`agents --json`** beside declared presence, plus a Doctor advisory. Teardown
43+
gets both fixes: the wrapper writes its terminal record *before* escalating
44+
SIGKILL into its own process group, and same-host readers cross-check the
45+
record's pty session, where only provable death downgrades. Transition history
46+
is deferred; `transitions` and `sinceMs` keep the record forward-compatible
47+
with it. The work proceeds without waiting on #242 and flags the delivery
48+
contract as that PR's territory.
49+
50+
## Options
51+
52+
| Option | Result | Reason |
53+
| --- | --- | --- |
54+
| Catalog record beside `status`, presence transport discipline | Selected | Remote supervisors and the TUI read through the catalog they already sync; the presence record's embedded-timestamp discipline is proven against transports that drop mtime. Accepted cost: a replicated write per state transition, unmeasured (OHS-T01). |
55+
| Host-local record plus CLI exposure only | Rejected | Zero transport cost, but remote readers get nothing and the consumer TUI must run on the owning host — the exact invisibility that leaves `control-state.json` unread today. |
56+
| Ride the stream/event ingress (#300) | Rejected | Events land in the inbox and DING the agent on every transition, emit is gated on a live owner binding and a healthy catalog, and stream direction is ingress into an agent, not egress about one. |
57+
| `blockedOn` as a fifth state | Rejected | An agent can be blocked while a child command runs; the field composes, a fifth state forces an ordering. A later-added axis would decode as `unknown` in every pinned v1 reader, so the field ships in v1 while the window is open. |
58+
| Sibling `st2 harness-state --json` command | Rejected for v1 | Zero pinned-wire edits, but the value of the surface is the comparison: a declared `busy` beside an observed `idle` is the wedged-agent signal, and a caller made to join two commands will skip it. The three pinned roster literals and the roster invariant wording change deliberately, in one change. |
59+
| Codex-only v1, other harnesses follow | Rejected | The TUI pins v1 when it ships; a fleet's Claude/pi/OpenCode seats reading `null` indefinitely re-creates the two-tier observability this exists to close. |
60+
| History (JSONL transitions) in v1 | Deferred | Measured burst coalescing — 4 transitions per turn, 0.1–0.4 ms apart (#268) — erases states shorter than the coalescing window, defeating the dwell-time analysis history is justified by. Deferred, not deleted: `transitions` + `sinceMs` make a later history additive. |
61+
62+
## Evidence and Argument
63+
64+
The signal already exists, typed and durable, for exactly one harness in the
65+
wrong place: `CodexObservedState` is written on every transition to
66+
`$XDG_STATE_HOME/st2/codex/<hash>/control-state.json`, and at current main it
67+
has no production reader — the pump rebuilds from `AwaitingStatus` on restart.
68+
So the Codex producer is a projection of shipped state, not a new observation
69+
path. The two rows #268's adversarial review corrected are binding here:
70+
`ActiveWithoutTurn` and `ConflictingTurn` project to `active`, because Codex
71+
positively reported activity and st2 merely cannot name a steerable turn; and
72+
`Held` never enters the published vocabulary, because it is defined as the
73+
complement of steerable — a delivery predicate, which decision 0001 already
74+
established must not govern an observability surface. pi is the strongest
75+
evidence for the evented path: the injected extension's `ctx.isIdle()` is a
76+
positive in-process signal (false exactly for `agent_start`..`agent_end`),
77+
which is the "stronger evented signal" root DQ2 asks for, on the one harness
78+
that already has a channel. On vocabulary provenance: the
79+
`idle|active|child|unknown × inputBuffer` words #268 reuses are from PR #123,
80+
which remains open — this decision *defines* those words for st2 rather than
81+
reusing something landed. The self-wake hazard #268 warns about is live today:
82+
the Codex delivery pump arms an unfiltered recursive watch on the whole agent
83+
directory while its own five-minute presence refresh writes into that tree, so
84+
scoping delivery-input watching is a prerequisite, not a precaution.
85+
86+
## Consequences
87+
88+
- The vocabulary (`idle`, `active`, `child`, `ended`, `unknown`; `blockedOn`;
89+
`inputBuffer`) is defined by this decision and
90+
[`05-harness-state`](../05-harness-state/requirements.md), not inherited
91+
from PR #123. If #123 lands with different words, that PR reconciles to
92+
these or supersedes this decision explicitly.
93+
- Homographs to manage: *observed harness state* is neither **presence**
94+
(agent-authored), **session state** (task-record liveness), R08's declared
95+
**activity status**, nor R09's **working state** (restored context). The
96+
ontology pins all four apart.
97+
- The roster wire shape changes: the three full-string pinned assertions and
98+
the stable-roster invariant wording are edited deliberately in the same
99+
change that adds `observedState`, with the new proof named.
100+
- `blocked` is vacuous under `bypassPermissions`, which is what
101+
`examples/native/agent-claude.kdl` ships today; a fleet running entirely on
102+
it gains nothing from the axis until that changes (OHS-T02).
103+
- The OpenCode native delivery transport is designed against #242's contract
104+
shape and does not wait for it; #242 reconciles at review.
105+
- The `.decisions/` series carries two documents numbered 0005 from separate
106+
PRs. This decision takes 0006 and leaves the collision as recorded history;
107+
numbers are not reused.
108+
- Root DQ2 and DQ3 are updated: pi gains an evented signal, the observed half
109+
of DQ3 is specified here, and the declared half (activity status, plan,
110+
plan step) plus supervisor-following behavior remain open.
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# DELTA-005: observed harness state is specified ahead of its producers
2+
3+
## Current mismatch
4+
5+
[`05-harness-state`](../05-harness-state/spec.md) and decision
6+
[`0006`](../.decisions/0006-observed-harness-state-is-a-driver-written-catalog-record.md)
7+
specify one observed-state record with producers on all four maintained
8+
harnesses, a roster join, Doctor exposure, and scoped delivery-input watching.
9+
The shipped implementation at the time this delta was filed is the envelope
10+
alone: `src/harness_state.rs` (record, writer, derivation, liveness
11+
cross-check hook) with its module tests. No producer writes the record, the
12+
roster does not expose it, the Codex delivery pump still watches its whole
13+
agent directory unfiltered, and OpenCode has no typed driver.
14+
15+
## Why the docs land first
16+
17+
The vocabulary and derivation rules are the contract the fractal TUI pins;
18+
they were interview-settled on 2026-08-23 and are cheaper to review as one
19+
document set than re-derived per producer PR. Producers are independent
20+
vertical slices (Codex projection, Claude hooks + wrapper, pi extension,
21+
OpenCode driver) and land incrementally against this spec.
22+
23+
## Direction
24+
25+
Update implementation to match the spec, in dependency order.
26+
27+
## Resolution Signal
28+
29+
Checked off as each lands with its named tests green; this delta is deleted by
30+
the change that completes the last box.
31+
32+
- [ ] Scoped delivery-input watching replaces the Codex pump's unfiltered
33+
agent-dir watch, with the no-self-wake regression test.
34+
- [ ] Codex producer: projection per the spec table, wired to the control
35+
pump's transitions, heartbeat, and terminal write.
36+
- [ ] Claude producer: hook-side classification plus wrapper-owned heartbeat
37+
and terminal write (pre-escalation ordering fixed).
38+
- [ ] pi producer: evented extension transitions over the channel; wrapper
39+
writes.
40+
- [ ] OpenCode: typed driver + `opencode-session` wrapper (presence lease),
41+
then its producer, then the native delivery transport.
42+
- [ ] Roster `observedState` join with the pinned assertions updated and the
43+
stable-roster invariant wording amended in the same change.
44+
- [ ] Doctor advisory for owned agents.
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# Observed harness state open questions
2+
3+
Each entry links a spec `DQ-H*`. Questions leave this file when resolved —
4+
into [spec.md](./spec.md) as decisions or `.experiments/` as tested
5+
hypotheses.
6+
7+
- **DQ-H1 Claude blocked-exit edge.** `PermissionRequest` carries no
8+
`tool_use_id`, so leaving `blockedOn: human` can only match on tool *name*
9+
and Claude batches tool calls (`PostToolBatch` carries plural `tool_calls`),
10+
so the first call's `PostToolUse` would clear `blocked` while the human still
11+
faces the second call's prompt. The corpus enters `blocked` in 2 of 9
12+
captures and exits in 1, with one tool and no batching: a rule validated on
13+
a single exit path is not validated. Until then the producer holds
14+
`blockedOn: human` until turn end (`Stop`) rather than encoding an exit rule
15+
that cannot hold. Resolves by: a capture with batched tool calls where the
16+
second call needs permission, then specifying the exit edge against it.
17+
- **DQ-H2 Transport cost of per-transition writes.** Presence refreshes every
18+
five minutes; turn boundaries are far more frequent, and burst coalescing
19+
measured 4 transitions per turn 0.1–0.4 ms apart. No measurement establishes
20+
what per-transition replicated writes cost on a real catalog under a real
21+
transport (OHS-T01 accepts this for v1). Resolves by: measuring write and
22+
sync volume on a live catalog; if unacceptable, a minimum-interval
23+
coalescing window is the tuning knob, at the cost of spinner latency.
24+
- **DQ-H3 `child` has no producer.** The word is reserved because the tuple's
25+
reasoning needs it (a long-running foreground command is neither the model
26+
working nor idle), but the producer that would have supplied it — the PTY
27+
screen observer — is cut: its idle proof was Codex-specific and its Claude
28+
arm collided with the empty-composer sentinel (#268 Limits). Resolves by: a
29+
harness exposing a positive child-process signal (Claude `PreToolUse`/
30+
`PostToolUse` pairs are the candidate), proven against batching.
31+
- **DQ-H4 Ungraceful-death coverage.** The wrapper's terminal write covers
32+
child-reap and SIGTERM; nothing in-process covers SIGKILL escalation into
33+
the wrapper's own group or an external forced kill (invariant row 11's
34+
case). Same-host readers NARROW the window via the liveness cross-check —
35+
only for provably dead sessions; `pty kill` removes the pidfile and leaves
36+
the probe indeterminate, so a same-host reader then shares the
37+
fifteen-minute horizon (or waits for the next relaunch claim) exactly like
38+
a cross-host one. Resolves by: either
39+
accepting the horizon (documenting it as the cross-host bound) or a
40+
supervisor-side terminal write derived from reconcile's session state —
41+
which would need its own fencing rules to avoid a supervisor overwriting a
42+
live wrapper's record.
43+
- **DQ-H5 Supervisor-following behavior.** Root `DQ3` sets two gates for
44+
catalog agent state: stale-state behavior (addressed throughout this
45+
subsystem) and supervisor-following behavior — what a *remote* supervisor
46+
may conclude and do from this record. The second gate is unmet, which is a
47+
reason decision 0006's spec ships Draft. #107 already bounds it: a remote
48+
`unknown` is no fresh observation, never proof of ill health, and never
49+
gates local work. Resolves by: specifying remote-reader semantics with a
50+
proof, or explicitly scoping the record same-host advisory.
51+
- **DQ-H6 OpenCode state source.** The producer needs a verified source.
52+
Candidate: OpenCode's server/SDK event surface (session state, message
53+
lifecycle); fallback: the shipped composer adapter's positive markers
54+
(`ctrl+p commands` footer) with documented limits. Nothing is measured yet —
55+
the DING adapter (#313) proves only composer classification. Resolves by:
56+
an `.experiments/` capture of OpenCode's event surface on a pinned version,
57+
then choosing the source and its skew policy — the repo's standing rule
58+
(pin where version skew fails silently, as `checks.pi-extension-types` and
59+
`SUPPORTED_CODEX_CLI_VERSIONS` do) applies.

0 commit comments

Comments
 (0)