Skip to content

Commit 009a9bc

Browse files
fix(catalog): isolate harness context staging (#429)
* fix(catalog): isolate harness context staging agent-identity: dev3.direct.omp.quzh99p3 agent-persona: generalist agent-supervisor: unavailable agent-tool: OMP agent-tool-version: 18.1.2 agent-runtime: OMP 18.1.2 tooling-profile: dotfiles@9323c3a * test(watch): use canonical harness context fixture agent-identity: dev3.direct.omp.quzh99p3 agent-persona: generalist agent-supervisor: unavailable agent-tool: OMP agent-tool-version: 18.1.2 agent-runtime: OMP 18.1.2 tooling-profile: dotfiles@9323c3a * fix(catalog): exclude harness runtime records agent-identity: dev3.direct.omp.quzh99p3 agent-persona: generalist agent-supervisor: unavailable agent-tool: OMP agent-tool-version: 18.1.2 agent-runtime: OMP 18.1.2 tooling-profile: dotfiles@9323c3a
1 parent 7cb7f64 commit 009a9bc

7 files changed

Lines changed: 493 additions & 81 deletions

File tree

INVARIANTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ materialization, messaging, DING, or presence must preserve them.
2424
| **Scoped delivery-input wakeups** | Native delivery pumps watch only their inputs: the agent's `resources/inbox` subtree and its `status` file. Runtime records written beside them by the pump's own process group — presence temp siblings, the `harness-state` and `harness-context` records with their locks and staged siblings, stream state — never wake delivery, so a producer that writes on every turn boundary cannot pump its own delivery loop. | `src/watch.rs::delivery_watcher_ignores_runtime_records_but_wakes_on_inbox_and_status` |
2525
| **Observed harness state discipline** | The `harness-state` record is written only by the owning session's driver processes — wrapper, channel, or hooks — serialized by a cross-process lock and coalesced against the on-disk record, atomically and byte-distinct on every write that lands, with freshness from its embedded timestamp and never file mtime; restating an unchanged state touches the record only when the refresh cadence is due. `unknown` is derived, never written: staleness, future skew, malformation, an unsupported schema, and a provably dead pty session each read as `unknown` with a distinct reason; an indeterminate liveness probe downgrades nothing; a missing record is no observation rather than `unknown`; no absence derives a definite state. A writer that loses sight of its harness stops heartbeating instead of refreshing a state it cannot see, and a predecessor session's record is never re-stamped. A reaped provider yields a terminal `ended` record carrying its real exit — written before the stop path's SIGKILL escalation and rewritten from the escalation cover when a grace-window reap observes the real status, proven against the real wrapper binaries of both stop implementations — and never a live state. | `src/harness_state.rs::unknown_state_is_derived_and_cannot_be_written`; `src/harness_state.rs::malformed_record_is_unknown_without_mtime_fallback`; `src/harness_state.rs::staleness_and_future_skew_derive_unknown_with_distinct_reasons`; `src/harness_state.rs::a_dead_session_reads_unknown_even_while_fresh_but_ended_survives`; `src/harness_state.rs::every_landed_write_is_byte_distinct_and_fresh_restatements_do_not_write`; `src/harness_state.rs::a_chatty_producer_restating_its_state_causes_zero_writes`; `src/harness_state.rs::concurrent_writers_defer_to_the_on_disk_record_not_their_cache`; `src/harness_state.rs::a_predecessor_sessions_record_is_never_heartbeat_eligible`; `src/harness_state.rs::missing_record_reads_as_none_not_unknown`; `src/codex_app_server.rs::pump_publishes_observations_and_stops_heartbeating_on_evidence_loss`; `src/claude_session.rs::a_provider_killed_mid_turn_reads_ended_rather_than_active`; `src/claude_session.rs::a_clean_provider_exit_writes_the_terminal_record`; `tests/harness_state_teardown.rs::stop_escalation_writes_the_terminal_record_before_sigkill`; `tests/harness_state_teardown.rs::opencode_stop_escalation_writes_the_cover_record_before_sigkill`; `tests/harness_state_teardown.rs::opencode_graceful_stop_records_the_real_reaped_exit` |
2626
| **Harness context discipline** | The numeric `harness-context` record is a sibling of `harness-state` and shares none of its ownership machinery: `incarnation` is provenance and never a fence, and a straggler's write lands rather than being refused. A reading is written only when it enters a different bucket of `usedPercent`, when a compaction edge arrives, when proven Claude account-window exhaustion changes, or when the record is older than the heartbeat, so a chatty producer cannot inflate the write rate; Codex account-window occupancy does not classify availability without its separate credit metadata. Every landed write is byte-distinct, and freshness comes from the record's own `observedAtMs`, never file mtime. A withheld value is carried as `null` and never fabricated from zero, the previous reading, or a division st2 could have done itself, and a reading above the window is carried unclamped. There is no `unknown` on this axis: past the horizon the reading is returned marked `stale` with its age, surviving every derivation the categorical record makes, while an unparseable record, a foreign schema, an uninterpretable harness, and an untrustworthy clock each read as absent. The relaunch claim removes the record, and Doctor's high-reading and stale-record lines are advisory in both directions and never change its exit status. Where a producer ships, its numerator is the harness's own and is pinned to the build it was measured on: pi's `tokens` is the last assistant message's `totalTokens` and omp's is its prompt figure, so neither harness's arithmetic can be published under the other's tag, and a harness bump that changes a number's meaning without changing its shape fails a fixture rather than shipping. A harness that positively reports it does not know its own occupancy is forwarded as withheld in the same write as the compaction edge that emptied it, a compaction edge whose harness names no reason yields `unknown` rather than an invented word, and a durable count the producer cannot read narrows to st2 counting edges rather than losing them. | `src/harness_context.rs::a_reading_inside_the_written_bucket_does_not_write_and_a_crossing_does`; `src/harness_context.rs::claude_rate_limit_exhaustion_and_reset_crossings_land_inside_one_usage_bucket`; `src/harness_context.rs::codex_account_window_exhaustion_does_not_prove_the_runtime_is_blocked`; `src/harness_context.rs::a_withheld_percent_has_no_bucket_and_only_a_compaction_or_heartbeat_writes`; `src/harness_context.rs::a_record_older_than_the_heartbeat_is_rewritten_and_every_landed_write_is_byte_distinct`; `src/harness_context.rs::a_compaction_always_lands_with_its_trigger_and_may_carry_a_durable_count`; `src/harness_context.rs::withheld_values_are_null_and_are_never_fabricated`; `src/harness_context.rs::a_reading_above_the_window_is_carried_unclamped`; `src/harness_context.rs::a_stale_reading_is_returned_with_its_age_rather_than_derived_away`; `src/harness_context.rs::freshness_comes_from_the_record_bytes_and_never_from_file_mtime`; `src/harness_context.rs::additive_fields_decode_but_foreign_schema_and_harness_read_as_nothing`; `src/harness_context.rs::a_record_beyond_the_future_skew_bound_reads_as_nothing`; `src/harness_context.rs::a_straggler_lands_and_is_visible_as_provenance_rather_than_being_refused`; `src/harness_state.rs::the_relaunch_claim_removes_the_harness_context_record`; `src/agents.rs::context_is_a_fourth_axis_that_survives_an_indeterminate_observed_state`; `tests/doctor.rs::harness_context_doctor_lines_are_advisory_and_never_change_the_exit_status`; `src/pi_channel.rs::the_pi_0_84_2_fixture_pins_total_tokens_as_the_numerator`; `src/pi_channel.rs::the_omp_18_0_9_fixture_pins_prompt_input_as_the_numerator`; `src/pi_channel.rs::a_pi_compaction_withholds_the_reading_it_emptied_in_the_same_write`; `src/pi_channel.rs::an_omp_compaction_yields_unknown_because_the_event_names_no_reason`; `src/pi_channel.rs::an_unreadable_durable_count_degrades_to_counting_edges_not_to_losing_them`; `src/pi_channel.rs::context_frames_decode_conservatively_or_not_at_all`; `src/pi_channel.rs::the_measured_pi_release_is_the_one_the_extension_gate_pins`; `src/omp_session.rs::the_measured_context_builds_are_admitted_by_this_gate` |
27-
| **Replicated-path discipline** | st2 pins the exact driver-record names it expects the replication transport's include list to carry — `harness-state` and `harness-context` — and both readers derive their paths from that list, so a rename cannot silently stop replication in a repository that does not own the list. A harness-context write never creates a staging file inside the agent directory: it stages outside the agent subtree, on the same filesystem, and leaves the record plus its own permanent lock and nothing else behind. `harness-state` still stages beside itself, pending its own change; the shared write helper takes the staging directory as an argument precisely because the two answer this differently. | `src/harness_context.rs::the_replicated_driver_record_names_are_pinned`; `src/harness_context.rs::writes_stage_outside_the_agent_subtree_and_leave_nothing_behind` |
27+
| **Replicated-path discipline** | st2 pins the exact driver-record names it expects the replication transport's include list to carry — `harness-state` and `harness-context` — and both readers derive their paths from that list, so a rename cannot silently stop replication in a repository that does not own the list. A harness-context write stages in the catalog control plane, outside the complete `agents` identity namespace, after validating canonical ancestry and same-filesystem atomic rename. Current-catalog identity walkers overlook only an exact legacy `.harness-context.tmp-<numeric-pid>-<numeric-counter>` regular non-symlink host child and leave it untouched; directories, symlinks, special files, generic dotfiles, near misses, and prepared topology remain strict. `harness-state` still stages beside itself, pending its own change; the shared write helper takes the staging directory as an argument precisely because the two answer this differently. | `src/harness_context.rs::the_replicated_driver_record_names_are_pinned`; `src/harness_context.rs::writes_stage_in_catalog_control_and_clean_up_after_success_or_failure`; `src/catalog_transaction.rs::current_projection_ignores_and_preserves_an_exact_legacy_staging_file`; `src/catalog_transaction.rs::current_projection_rejects_legacy_shape_type_confusion_and_near_misses`; `src/message.rs::transition_addressability_ignores_and_preserves_exact_legacy_staging_files`; `src/message.rs::transition_addressability_rejects_legacy_type_confusion_and_near_misses`; `tests/catalog_apply.rs::snapshot_ignores_and_preserves_an_exact_legacy_harness_context_staging_file` |
2828
| **Status-line slot chaining** | Claude's `statusLine` is a single slot whose winning declaration replaces the others outright, and `.claude/settings.local.json` — the file st2 materializes — wins over every other settings file. So st2's entry there is a tee: it records the reading and then invokes the operator's own renderer, resolved from `$ST_CLAUDE_STATUSLINE_RENDERER` and then `~/.claude/statusline-renderer.json`, first hit wins and never both. Where no renderer resolves, and wherever a resolved renderer fails, the tee writes NOTHING to stdout and puts its diagnostic on stderr: the payload is machine JSON, so echoing it into the slot is strictly worse for the operator than a blank row. Recording is unaffected by which arm runs. | `tests/claude_statusline.rs::the_tee_records_the_reading_and_hands_the_same_payload_to_the_env_renderer`; `tests/claude_statusline.rs::the_operator_file_supplies_the_renderer_when_no_variable_does`; `tests/claude_statusline.rs::the_variable_wins_over_the_file_and_the_file_is_never_also_run`; `tests/claude_statusline.rs::with_no_renderer_the_tee_renders_nothing_rather_than_the_raw_payload`; `tests/claude_statusline.rs::a_recording_failure_still_renders_the_status_line`; `tests/claude_statusline.rs::a_renderer_that_exits_non_zero_leaves_stdout_empty`; `tests/claude_statusline.rs::a_renderer_file_that_is_not_executable_leaves_stdout_empty`; `tests/claude_statusline.rs::without_st2_on_path_the_script_drains_stdin_and_renders_nothing`; `tests/claude_statusline.rs::the_rendered_registration_names_the_chaining_tee_and_carries_the_whole_slot` |
2929
| **Version-pinned producer arithmetic** | Every harness-context producer publishes the number its own harness means by that number, and each one is pinned to the exact build the meaning was measured on: Claude's integer percent over `total_input_tokens` and `context_window_size` (2.1.250), Codex's window with the 12,000 baseline subtracted from both sides (codex-cli 0.151.0), pi's last assistant `totalTokens` and omp's prompt-only `input` (0.84.2 and 18.0.9), and OpenCode's last non-summary assistant total over the providers window (1.18.25). The failure this bounds is a harness bump that changes what a key means without changing its shape, which no type gate and no round-trip assertion can see, so each fixture decodes a verbatim capture, asserts the version literally, and asserts the wrong numerator the same capture would yield. | `src/claude_session.rs::a_mid_session_statusline_payload_yields_claudes_own_triple`; `src/claude_session.rs::a_pre_turn_statusline_payload_withholds_rather_than_reporting_zero`; `src/codex_app_server.rs::codex_context_recomputes_the_captured_reading_and_pins_its_verified_version`; `src/pi_channel.rs::the_pi_0_84_2_fixture_pins_total_tokens_as_the_numerator`; `src/pi_channel.rs::the_omp_18_0_9_fixture_pins_prompt_input_as_the_numerator`; `src/opencode_session.rs::captured_opencode_turns_publish_the_assistant_total_over_the_providers_window` |
3030
| **Native-driver diagnostics are bounded and recoverable** | One additive-tolerant `st2.driver-diagnostic.v1` snapshot exposes the earliest failing native-driver boundary through closed stage/reason/source/support vocabulary and origin-timed evidence age. Absent, malformed, foreign, unknown, and wrongly paired records never read healthy. Stage recovery clears only its own failure and removes the record after final recovery without changing prompt submission, retry, read-back, or archive semantics. Roster and Doctor share the typed reader/repair policy; telemetry labels contain only bounded stage/reason/source/support/outcome values, never versions, identities, prompts, messages, or paths. | `src/driver_diagnostic.rs::every_stage_reason_and_source_has_bounded_wire_vocabulary`; `src/driver_diagnostic.rs::additive_fields_decode_but_malformed_foreign_and_unknown_records_are_indeterminate`; `src/driver_diagnostic.rs::recovery_clears_only_its_stage_and_reveals_the_next_failure`; `src/opencode_session.rs::delivery_and_read_back_boundaries_publish_and_clear_diagnostics_without_changing_retry`; `src/metrics.rs::driver_diagnostic_metric_attributes_are_exactly_the_bounded_axes`; `tests/doctor.rs::native_driver_diagnostic_roster_and_doctor_agree_and_recovery_clears` |

docs/vrs/08-harness-context/spec.md

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -363,19 +363,19 @@ Two obligations follow on st2's side:
363363
and renames only the canonical path in. Measured free: p50 0.217 ms staged
364364
outside versus 0.222 ms as a sibling.
365365

366-
**Where, decided during implementation (2026-08-29): the agent directory's
367-
parent** `<catalog>/agents/<host>/` for the layout st2 publishes. The
368-
obvious alternative, walking up for the catalog's control directory and
369-
staging in `<catalog>/.st2/staging`, reads better and is wrong: the search has
370-
no way to tell *this* catalog's control directory from any unrelated one above
371-
the agent, and it was caught doing exactly that in a test — an agent directory
372-
under `/tmp` on a host carrying a stray `/tmp/.st2` staged into a foreign
373-
tree, and potentially a foreign filesystem, which costs the rename its
374-
atomicity. The parent needs no discovery, cannot escape, holds nothing the
375-
transport replicates (only agent directories, and a dotted temporary name
376-
matches no include entry), and is correct for a flat catalog as well as a
377-
published one. An agent directory with no parent is an error rather than a
378-
quiet write inside the subtree.
366+
**Where:** `<catalog>/.st2/harness-context-staging`, derived only from the
367+
exact canonical `<catalog>/agents/<host>/<identity>` ancestry. Every ancestry
368+
component and the staging directory must be a real directory, and the staging
369+
and agent directories must report the same filesystem device; otherwise the
370+
writer fails rather than searching upward, following a symlink, or degrading
371+
atomic publication to a copy.
372+
373+
Earlier writers staged at `<catalog>/agents/<host>` and could leave
374+
`.harness-context.tmp-<numeric-pid>-<numeric-counter>` behind after a crash.
375+
Current-catalog identity walkers overlook only an exact legacy name that is a
376+
regular non-symlink file, and leave it untouched for a possibly-live old
377+
writer. Directories, symlinks, special files, generic dotfiles, near misses,
378+
and prepared-catalog topology remain strict.
379379

380380
`harness-state` keeps staging beside itself for now. The two records share the
381381
extracted `write_json_atomic` helper, which takes the staging directory as an
@@ -1058,10 +1058,11 @@ each only once a real test proves it (per `CLAUDE.md`):
10581058
- **Replicated-path discipline** (new row, once proved) — st2 pins the exact
10591059
driver-record names it expects the transport's include list to carry, and no
10601060
staging file is ever created inside the replicated subtree. Both are silent
1061-
failures in production, so both need a test that asserts the names and paths
1062-
themselves: one pinning `harness-state` and `harness-context` as the names
1063-
st2 publishes for replication, one asserting that a write leaves no
1064-
non-canonical file behind in the agent directory.
1061+
failures in production, so tests pin `harness-state` and `harness-context`,
1062+
assert staging below the catalog control directory with same-filesystem
1063+
atomic publication and cleanup, and bound legacy-reader compatibility to the
1064+
exact regular-file shape without changing a digest, snapshot, or message
1065+
address.
10651066
- **Status-line slot chaining** (HC-R18) — a rendered status-line registration
10661067
invokes the operator's downstream renderer. The slot is single-valued and the
10671068
winner replaces rather than merges, so a test that only checks st2's command

0 commit comments

Comments
 (0)