From 853ad9cc176f98f328cd01a1417b82b2e11db882 Mon Sep 17 00:00:00 2001 From: schickling-assistant <261620128+schickling-assistant@users.noreply.github.com> Date: Sun, 30 Aug 2026 21:49:57 +0200 Subject: [PATCH] Admit codex-cli 0.151.0 agent-identity: dev3.direct.omp.jmg9x29x agent-persona: generalist agent-supervisor: unavailable agent-tool: OMP agent-tool-version: 18.0.11 agent-runtime: OMP 18.0.11 tooling-profile: dotfiles@2161b9c --- INVARIANTS.md | 2 +- docs/vrs/05-harness-state/spec.md | 6 ++-- docs/vrs/08-harness-context/spec.md | 26 ++++++++--------- src/codex_app_server.rs | 43 ++++++++++++++++++++--------- 4 files changed, 47 insertions(+), 30 deletions(-) diff --git a/INVARIANTS.md b/INVARIANTS.md index 01caae32..8dcd5ad9 100644 --- a/INVARIANTS.md +++ b/INVARIANTS.md @@ -26,7 +26,7 @@ materialization, messaging, DING, or presence must preserve them. | **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, or when the record is older than the heartbeat, so a chatty producer cannot inflate the write rate; 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::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` | | **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` | | **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` | -| **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.150.1), 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` | +| **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` | | **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` | | **Retirement health** | A retired declaration is healthy only after every declared task ID is absent. Any live or dead declared task record reports incomplete retirement; retired declarations do not require presence. Live declarations retain their existing task and presence checks. | `tests/doctor.rs::retired_declaration_is_healthy_when_tasks_and_presence_are_absent`; `tests/doctor.rs::retired_declaration_is_unhealthy_while_a_declared_task_is_alive`; `tests/doctor.rs::retired_declaration_is_unhealthy_while_a_dead_task_record_remains` | | **Suspension health** | A suspended declaration is healthy when no declared task is live and every retained dead record is explicitly keep-pinned. It requires no presence, but this weaker result never proves retirement. Resume preserves ordinary keep and adopt-only policy. | `tests/doctor.rs::suspended_declaration_is_healthy_when_tasks_are_absent_without_presence`; `tests/doctor.rs::suspended_declaration_distinguishes_live_dead_keep_and_dead_nonkeep`; `tests/reconcile.rs::resuming_uses_ordinary_reconcile_and_does_not_override_keep` | diff --git a/docs/vrs/05-harness-state/spec.md b/docs/vrs/05-harness-state/spec.md index 6585906c..8d9ddee3 100644 --- a/docs/vrs/05-harness-state/spec.md +++ b/docs/vrs/05-harness-state/spec.md @@ -184,9 +184,9 @@ comes first. The cross-check is a narrowing of the ungraceful-death window OHS-T04/OHS-R07 say exactly this, and no death tombstone is attempted: the kill that removes the registry entry leaves nothing behind to prove death with, and fabricating evidence is the one thing this design never does. And -hosts running codex-cli at or above 0.148 produce no Codex observed state at -all: `SUPPORTED_CODEX_CLI_VERSIONS` refuses the launch, correctly, until the -pin moves (#267). +hosts running a codex-cli version outside the exact +`SUPPORTED_CODEX_CLI_VERSIONS` allowlist produce no Codex observed state at all: +the provider launch is refused before the control channel starts. ## Codex producer (OHS-R05) diff --git a/docs/vrs/08-harness-context/spec.md b/docs/vrs/08-harness-context/spec.md index 8784a213..0bf5d1c7 100644 --- a/docs/vrs/08-harness-context/spec.md +++ b/docs/vrs/08-harness-context/spec.md @@ -389,14 +389,15 @@ never what this subsystem needs to work. ## Producers (HC-R02, HC-R11, HC-R13) -Every row was measured on 2026-08-29 against the named version. `usedPercent` -is that harness's own displayed number (HC-R02); where st2 computes it, the row -says so. +Every live row was measured on 2026-08-29. The Codex payload was captured on +0.150.1 and its version-coupled arithmetic and notification shape were reverified +against 0.151.0 before admission. `usedPercent` is that harness's own displayed +number (HC-R02); where st2 computes it, the row says so. | Harness | Version verified | Channel | `usedTokens` | `windowTokens` | `usedPercent` | | --- | --- | --- | --- | --- | --- | | claude | 2.1.250 | `statusLine` command stdin JSON | `context_window.total_input_tokens` = `input + cache_creation + cache_read` of the last response | `context_window.context_window_size` | `context_window.used_percentage` — Claude's own integer, clamped 0..100 | -| codex | codex-cli 0.150.1 | app-server `thread/tokenUsage/updated` | `tokenUsage.last.totalTokens` | `tokenUsage.modelContextWindow` | st2 computes with the baseline rule below; equals `100 −` Codex's displayed "% context left" | +| codex | codex-cli 0.151.0 | app-server `thread/tokenUsage/updated` | `tokenUsage.last.totalTokens` | `tokenUsage.modelContextWindow` | st2 computes with the baseline rule below; equals `100 −` Codex's displayed "% context left" | | pi | 0.84.2 | injected extension `ctx.getContextUsage()` | `.tokens` = last assistant `totalTokens` (input + output + cacheRead + cacheWrite) | `.contextWindow` | `.percent` (float) | | omp | 18.0.9 (and 18.0.3) | injected extension `ctx.getContextUsage()` | `.tokens` = last assistant **`input`** only | `.contextWindow` | `.percent` (float) | | opencode | 1.18.25 | SSE `message.updated` joined with `GET /config/providers` | last **non-summary** assistant `tokens.total` | `providers[].models[].limit.context` | st2 computes `usedTokens / windowTokens`; the server displays none | @@ -596,7 +597,7 @@ A compaction carrying an `agent_id` is a subagent's and never touches the record matching the categorical producer's guard for the same reason: this record describes the top-level window (`DQ-C9`). -### codex (codex-cli 0.150.1) +### codex (codex-cli 0.151.0) st2 does not re-derive the percentage; it **mirrors** Codex's own `TokenUsage::percent_of_context_window_remaining` and subtracts the result from @@ -701,14 +702,13 @@ Three limits of the shipped producer, all deliberate: before the binding candidate names one, and a thread starting now has no history to replay. -Finally, a version note. The arithmetic above was settled by reading codex-cli -0.150.1's Rust source, and `CODEX_CONTEXT_VERIFIED_VERSION` pins that literal in -the fixture (HC-R13). It is **ahead of `SUPPORTED_CODEX_CLI_VERSIONS`**, the -delivery-path launch gate, which admits 0.145.0–0.147.0 — so in the shipped tree -this arithmetic runs against versions whose source was not read for it. The -baseline is a long-lived Codex constant, but that is an expectation rather than a -measurement, and closing the gap means repeating the delivery gate's own -admission checks for 0.150.1, which is a separate act from this producer. +Finally, a version note. The arithmetic and notification shape above were +reverified against codex-cli 0.151.0's Rust source, and +`CODEX_CONTEXT_VERIFIED_VERSION` pins that literal in the fixture (HC-R13). That +matches the newest build admitted by `SUPPORTED_CODEX_CLI_VERSIONS`; it does not +turn the measurement into a semantic-version promise. Every later Codex release +still requires its own source comparison and live delivery proof before the exact +launch gate moves. ### pi (0.84.2) diff --git a/src/codex_app_server.rs b/src/codex_app_server.rs index 16b07c21..6d7f6c28 100644 --- a/src/codex_app_server.rs +++ b/src/codex_app_server.rs @@ -43,10 +43,19 @@ use crate::{ding, harness_context, harness_state, message, run, status}; /// `turn/start` response body, `turn/started`, `turn/completed`, the typed `item/completed` /// receipt, `turn/steer`, and the `thread/resume` subscription path are therefore unproven on this /// version. See #267. +/// +/// `codex-cli 0.151.0` is admitted after comparing its delivery-critical source and schemas with +/// 0.147.0. A live native run passed the exact version gate, initialized control, bound a fresh +/// thread, submitted `turn/start`, and completed the model turn. Restarting the driver then proved +/// `thread/resume` subscription and reconciled the attempted delivery to `Accepted` from the +/// matching `clientId` in typed thread history. The live `item/completed` notification did not +/// independently advance that receipt before restart, so only the durable resume receipt path is +/// claimed here. pub const SUPPORTED_CODEX_CLI_VERSIONS: &[&str] = &[ "codex-cli 0.145.0", "codex-cli 0.146.0", "codex-cli 0.147.0", + "codex-cli 0.151.0", ]; const RUNTIME_SCHEMA: &str = "st2.codex-runtime.v1"; const BINDING_SCHEMA: &str = "st2.codex-thread-binding.v1"; @@ -368,7 +377,7 @@ impl CodexDeliveryState { } /// The exact codex-cli version whose Rust source settled the occupancy arithmetic below, read at -/// tag `rust-v0.150.1` (tag object `0eb410ad0dd161ea323b05452f978de01cd63430`) because the Nix +/// tag `rust-v0.151.0` (tag object `d8673cb68e349c208659b986697773d3145dbb14`) because the Nix /// package ships a prebuilt musl tarball with no vendored source. HC-T03 calls Codex's baseline a /// version-coupled constant — a property of a build, not of a documented contract — and HC-R13 /// bounds that with a fixture pinned to this literal, in the shape of `omp_session`'s @@ -377,16 +386,15 @@ impl CodexDeliveryState { /// [`tests::codex_context_recomputes_the_captured_reading_and_pins_its_verified_version`] rather /// than silently publish a differently-meaning number. /// -/// Deliberately NOT a launch gate: this constant refuses nothing, it names what was measured. Note -/// it is ahead of [`SUPPORTED_CODEX_CLI_VERSIONS`], which admits 0.145.0–0.147.0 — so in the -/// shipped tree this arithmetic runs against a version whose source was not read for it. The -/// baseline is a long-lived Codex constant, but that is an expectation, not a measurement; closing -/// the gap means repeating the delivery-gate admission checks for 0.150.1, which is a separate act. -pub const CODEX_CONTEXT_VERIFIED_VERSION: &str = "0.150.1"; +/// Deliberately NOT a launch gate: this constant refuses nothing, it names what was measured. +/// Admitting 0.151.0 aligns the newest delivery-gated build with this measurement; a later Codex +/// admission must still re-read the version-coupled arithmetic rather than infer compatibility +/// from the unchanged literal. +pub const CODEX_CONTEXT_VERIFIED_VERSION: &str = "0.151.0"; /// Codex's `BASELINE_TOKENS`, subtracted from BOTH the numerator and the denominator of its -/// displayed occupancy: `codex-rs/protocol/src/protocol.rs:2242` and -/// `codex-rs/tui/src/token_usage.rs:9` at `rust-v0.150.1` carry the same literal with an identical +/// displayed occupancy: `codex-rs/protocol/src/protocol.rs:2332` and +/// `codex-rs/tui/src/token_usage.rs:9` at `rust-v0.151.0` carry the same literal with an identical /// function body, and no configuration override exists. Its doc comment: "should capture tokens /// that are always present in the context (e.g. system prompt and fixed tool instructions) so that /// the percentage reflects the portion the user can influence." @@ -1244,7 +1252,7 @@ impl CodexControlState { return Ok(false); } let item_type = required_string(message, "/params/item/type", method)?; - // Codex 0.146/0.147 `ThreadItem` has eighteen variants; only these three carry + // Codex 0.151 `ThreadItem` has nineteen variants; only these three carry // steerability. Every other item reports work inside a turn that the turn and // thread status already model, so it is ignored on purpose — a later protocol // item that gates or releases input has to be added here explicitly, because @@ -3360,6 +3368,15 @@ mod tests { #[test] fn protocol_version_gate_accepts_only_the_exact_allowlist() { + assert_eq!( + SUPPORTED_CODEX_CLI_VERSIONS, + &[ + "codex-cli 0.145.0", + "codex-cli 0.146.0", + "codex-cli 0.147.0", + "codex-cli 0.151.0", + ] + ); let tmp = tempfile::tempdir().unwrap(); let write_version = |name: &str, version: &str| { let path = tmp.path().join(name); @@ -3444,8 +3461,8 @@ mod tests { } /// HC-R13's Codex fixture. The frames are a transposition, and the comment says which half came - /// from where: the SHAPE is codex-cli 0.150.1's own app-server schema dump - /// (`ThreadTokenUsageUpdatedNotification`, `AccountRateLimitsUpdatedNotification`), and the + /// from where: the SHAPE is codex-cli 0.151.0's own app-server schema dump + /// (`ThreadTokenUsageUpdatedNotification`, `AccountRateLimitsUpdatedNotification`), while the /// NUMBERS are verbatim from a real rollout captured on 2026-08-29 from a 0.150.1 session /// (`session_meta.payload.cli_version = "0.150.1"`) — its first and last `token_count` events /// and the `rate_limits` snapshot riding them. Fields the capture elided are omitted rather @@ -3457,7 +3474,7 @@ mod tests { /// the only thing tying this arithmetic to a build whose source was actually read. #[test] fn codex_context_recomputes_the_captured_reading_and_pins_its_verified_version() { - assert_eq!(CODEX_CONTEXT_VERIFIED_VERSION, "0.150.1"); + assert_eq!(CODEX_CONTEXT_VERIFIED_VERSION, "0.151.0"); assert_eq!(CODEX_BASELINE_TOKENS, 12_000); let frames = include_str!("../tests/fixtures/codex_token_usage_inbound.jsonl")