Skip to content

Commit df327b5

Browse files
feat(harness-state): add v3 reader foundation
agent-identity: dev3.direct.omp.9exwnk6h agent-persona: generalist agent-supervisor: unavailable agent-tool: OMP agent-tool-version: 18.1.7 agent-runtime: OMP 18.1.7 tooling-profile: dotfiles@931583a
1 parent f3db35e commit df327b5

18 files changed

Lines changed: 2773 additions & 120 deletions

INVARIANTS.md

Lines changed: 2 additions & 2 deletions
Large diffs are not rendered by default.

docs/vrs/.decisions/0006-observed-harness-state-is-a-driver-written-catalog-record.md

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,3 +137,72 @@ declared native driver is expected to publish it. Telemetry repeats only the
137137
closed axes on
138138
metrics and `span.label`; raw versions and identities remain trace/log-only,
139139
and prompt/message/path content never enters the record or labels.
140+
141+
## Amendment 2: the fault axis is a third record version with one shared disposition
142+
143+
Accepted by Johannes on 2026-09-05 for the harness-state v3 foundation.
144+
145+
The original decision put activity, `blockedOn`, and `inputBuffer` in one
146+
record and left everything else to prose in `reason`. That is what broke: an
147+
authentication rejection, an exhausted quota, a throttle, and a human
148+
permission prompt all arrive as some combination of `active`, `blockedOn`, and
149+
a `reason` word, so every consumer that wants to know "does this seat need a
150+
person, and to do what" ends up parsing prose or inventing its own rules. The
151+
amendment separates the axes and moves the fold into st2.
152+
153+
**Condition becomes its own axis, in a new record version.** Because a version
154+
suffix is this record family's read contract, changing what the record says
155+
reserves the next version rather than widening the current one:
156+
`st2.harness-state.v3` carries a tagged `condition ∈ clear | fault`, where a
157+
fault names a closed category, an open provider-namespaced code, a recovery
158+
class, and its own semantic observation time. The overloaded `blockedOn` is
159+
replaced by a tagged ask axis that speaks only about actual human prompts. A
160+
fault is not an ask — a throttled provider asks nobody anything — and where
161+
both hold, remediation is primary while the ask stays visible. Versions 1 and 2
162+
project their condition as EXPLICITLY absent: their legacy words never infer a
163+
fault, and absence is never `clear`.
164+
165+
**st2 owns normalization and publishes one derived disposition.** Three closed
166+
axes — a state, how soon a human is needed, and what that human would do first
167+
— from one pure function, exposed on the roster, the catalog graph, and Doctor.
168+
Consumers read it; none re-derives it. The raw axes ride beside it, so nothing
169+
is hidden by the fold and a consumer that disagrees can see its inputs. A
170+
native-driver diagnostic failure contributes through the same function — it is
171+
a fault the harness could not report itself — and, as Amendment 1 established,
172+
still never changes delivery. Ended and record-level indeterminate never page.
173+
174+
**Two clocks, deliberately.** The heartbeat proves only that a writer still
175+
holds evidence; it never moves a fault's observation time or its recovery
176+
deadline. Attention is derived at read time, so an `automatic` recovery past
177+
its own deadline becomes an untyped, unknown-recovery fault that pages until an
178+
explicit paired clear, a terminal record, a new claim, or a new incarnation
179+
replaces it. Without that separation a seat could heartbeat its way out of an
180+
overdue recovery forever.
181+
182+
**Options considered and rejected.** Widening version 1/2 in place: rejected,
183+
because the version suffix is the read contract and a pinned reader would
184+
decode new words with old meanings. A separate fault record beside
185+
`harness-state`: rejected for the same reason Amendment 1 chose one diagnostic
186+
snapshot — two files make every consumer choose precedence independently, and
187+
condition and activity are observations of one thing by one owner. Free-form
188+
provider strings promoted to a routable field: rejected, since the whole point
189+
is that consumers stop branching on prose; provider vocabulary is kept in the
190+
open `code` under a closed category. Each consumer deriving its own urgency:
191+
rejected, because two derivations are how one consumer starts paging for what
192+
another ignores.
193+
194+
**Consequences.** Reader-first, again: this version reads, strictly validates,
195+
and projects version 3 while the single writer-selection point stays on version
196+
2, and version 3 activation replaces that point rather than adding a second
197+
selector. The ownership envelope is version-independent, so a version 2 claim
198+
refuses a version 3 record instead of overwriting a meaning it cannot read, and
199+
an undecodable record's ownership sequence still carries forward. Every
200+
projected row carries the exact version its record declared, which makes the
201+
migration's drain gate positive. The roster wire grows the appended version 3
202+
axes plus a row-level `disposition`; the pinned literals and the stable-roster
203+
invariant wording change deliberately in the same change. Per-provider
204+
mappings, the writer cutover, and what a recovery deadline should be are open
205+
(`DQ-H7`, `DQ-H8`). A conversation reference — identity and capability only,
206+
with a finite verification bound and no conversation content — ships in the
207+
same version, because a consumer that can see a fault will ask which
208+
conversation it belongs to.

docs/vrs/05-harness-state/open-questions.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,3 +77,28 @@ hypotheses.
7777
(`src/opencode_session.rs::captured_permission_grant_pair_enters_and_exits_blocked`,
7878
`::captured_question_reply_pair_enters_and_exits_blocked`); the raw frames
7979
and commands are in `.experiments/2026-08-23-opencode-surface.md`.
80+
- **DQ-H7 Version 3 producers and the writer cutover.** The fault axis is
81+
specified, read, strictly validated, and projected, and the shared
82+
disposition is derived from it; no producer emits it yet and the
83+
writer-selection point stays on version 2. Two things remain open. First,
84+
the per-provider mapping: which typed signal each driver projects into
85+
which closed category, code, and recovery — the same evidence rule the
86+
activity projection follows (a driver publishes what it positively
87+
observes, never prose), and the same measurement burden, since a category
88+
is only worth routing on if the provider's signal really means it.
89+
Second, the cutover order: the version 3 writer may activate only where
90+
every reader of that catalog already accepts version 3, and the one-way
91+
claim fence plus the version-independent ownership envelope are what make
92+
a mixed fleet safe in the meantime — a version 2 writer refuses a version
93+
3 record instead of overwriting a meaning it cannot read. Resolves by:
94+
per-driver capture evidence for the mappings, then flipping the single
95+
writer-selection point.
96+
- **DQ-H8 What a `nextObservationDueMs` should be.** The overdue rule is
97+
specified and proved, but nothing yet establishes what deadline a producer
98+
should publish for a given automatic recovery — a provider's stated retry
99+
window, a measured one, or a conservative multiple of it. Too short pages
100+
for faults that were going to clear; too long delays a page indefinitely,
101+
which is the failure the rule exists to prevent. The field is optional
102+
precisely so a producer that cannot justify a number withholds it, and a
103+
fault with no deadline never becomes overdue. Resolves by: measured retry
104+
behavior per provider, or an explicit decision to page on a fixed bound.

docs/vrs/05-harness-state/requirements.md

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,86 @@ authorizes or changes delivery.
204204
reached its ordinary end — clears it; an unclassified failure leaves a
205205
standing rejection standing. Delivery semantics are unchanged.
206206

207+
### Must separate what is wrong from what is being asked
208+
209+
- **OHS-R17 Condition is its own axis:** The record carries a tagged
210+
`condition ∈ clear | fault`, published under its own record version so the
211+
meaning of a version's bytes stays decidable from those bytes. A fault
212+
carries a CLOSED `category ∈ authentication | account | quota | rateLimit |
213+
provider | context | configuration | policy | harness` — closed because
214+
consumers route on it — an OPEN, provider-namespaced `code` for diagnostic
215+
granularity, a `recovery ∈ automatic | human | terminal | unknown`, and its
216+
own semantic observation time. Provider prose is diagnostic only; no
217+
consumer branches on it. A category word outside the closed set leaves the
218+
fault UNTYPED and still routed by its recovery: neither borrowing a
219+
neighbouring category nor discarding the whole observation is acceptable,
220+
because the first invents a claim and the second makes a real fault stop
221+
being reported. Versions without the axis project it as EXPLICITLY absent —
222+
never `clear`, and no fault is inferred from their legacy words.
223+
- **OHS-R18 An ask is an actual human prompt:** The ask axis is tagged so
224+
`none`, `pending` with its kind, and `unknown` are three distinct
225+
statements, and it speaks only about prompts. A fault is not an ask: a
226+
throttled provider asks nobody anything. Where a fault and an ask coexist,
227+
remediation is primary and the ask remains visible on the raw axis.
228+
- **OHS-R19 Strict edges, typed indeterminacy:** A record whose OBSERVATION
229+
axes contradict each other is not a weaker observation; it is not an
230+
observation. Every rejection carries its own reason word — a `clear`
231+
bearing fault evidence, a fault missing recovery or its observation time, a
232+
recovery deadline on a recovery this version recognizes as non-automatic,
233+
an inverted deadline, an ask that names a kind while claiming none — so an
234+
operator can tell a producer bug from a stale seat and one bug from
235+
another. Rejection is scoped to what the contradiction actually damages:
236+
strictness must not destroy evidence. A deadline beside an UNRECOGNIZED
237+
recovery word is kept, because that class may be automatic in a version
238+
the reader predates and rejecting it would turn a fault that pages into a
239+
non-paging row; and a badly stated conversation reference degrades only
240+
that axis, because a broken side-channel is not evidence about the harness.
241+
Indeterminacy is exposed TYPED, carrying that word and the age of the
242+
evidence it was derived from when the bytes carried a usable stamp; the
243+
legacy scalar reason remains a projection of the same single value and is
244+
never derived independently.
245+
- **OHS-R20 Two clocks:** Transport freshness and semantic observation are
246+
separate. The heartbeat proves only that a writer still holds evidence; it
247+
never moves a fault's observation time or its recovery deadline, and
248+
attention is derived at READ time. An `automatic` recovery past its own
249+
deadline becomes an untyped, unknown-recovery fault that pages until an
250+
explicit paired clear, a terminal record, a new claim, or a new incarnation
251+
replaces it — a recovery that missed its own deadline is no longer evidence
252+
of anything automatic.
253+
- **OHS-R21 One shared disposition:** st2 owns normalization and publishes
254+
ONE derived disposition — exactly three closed axes: a state, how soon a
255+
human is needed, and what that human would do first — from one pure
256+
function, exposed on the roster, the catalog graph, and Doctor. Consumers
257+
read it; none re-derives urgency, because two independent derivations are
258+
how one consumer starts paging for what another ignores. Raw activity, the
259+
actual human ask, the condition, and recovery stay orthogonal and ride
260+
beside it, so a consumer that disagrees can see exactly what was folded.
261+
Ended and record-level indeterminate never page. A native-driver diagnostic
262+
failure contributes through the same function — it is a fault the harness
263+
could not report itself — and still never changes delivery.
264+
- **OHS-R22 Conversation reference is identity and capability:** The
265+
conversation bridge is tagged `linked | unavailable | unsupported`, and a
266+
linked reference carries the driver's namespace, the provider's opaque
267+
conversation identity, the runtime incarnation, an explicit history
268+
mutability claim with the evidence for it, and a FINITE verification bound
269+
so a consumer ages the claim instead of trusting it forever. A record that
270+
states nothing about a conversation claims no capability, which is distinct
271+
from `unsupported`. Conversation content stays out of the record entirely.
272+
A `linked` reference that is not fully stated is not trusted and not
273+
discarded either: it degrades to `unavailable` carrying st2's own closed
274+
rejection word — never provider prose — and the observation's activity,
275+
condition, and ask axes stand untouched.
276+
- **OHS-R23 Reader-first activation with a positive drain gate:** A record
277+
version is read, strictly validated, and projected before any writer emits
278+
it, and exactly one writer-selection point decides which version this build
279+
writes. Every projected row carries the EXACT version its record declared,
280+
including versions the build cannot interpret, so a migration's drain gate
281+
is positive — "every row reads the new version" is checkable, while "no row
282+
is still the old one" is not checkable from any absence. The ownership
283+
envelope is version-independent, so a claim honors the schema fence and the
284+
ownership sequence of a record whose meaning it cannot read rather than
285+
treating those bytes as an empty seat.
286+
207287
## Evidence
208288

209289
The measurements are #268's, taken 2026-08-16/17 on one host and carried with

0 commit comments

Comments
 (0)