Skip to content

feat(harness-context): the OpenCode producer, and all five rows reconciled - #381

Merged
schickling-assistant merged 3 commits into
schickling/hc-04-pi-ompfrom
schickling/hc-05-opencode
Aug 29, 2026
Merged

feat(harness-context): the OpenCode producer, and all five rows reconciled#381
schickling-assistant merged 3 commits into
schickling/hc-04-pi-ompfrom
schickling/hc-05-opencode

Conversation

@schickling-assistant

@schickling-assistant schickling-assistant commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

Problem

OpenCode is the last harness with no context, and the only one whose numerator is pushed while its denominator must be pulled: SSE message.updated carries the token counts, and the model's context limit lives behind GET /config/providers. The server displays no percentage of its own, so there is nothing to mirror.

It is also the harness with the most ways to publish a wrong number. summary is overloaded (an object, hence truthy, on user messages); the compaction summarizer's own assistant message carries the summarization call's cost rather than the new context size; and session.updated's info.tokens is cumulative with no total key — it is sessionTotalTokens and never the occupancy.

This PR also closes the stack: the spec's Status paragraph and HC-R13 verification bullet are reconciled across all five slices, and one INVARIANTS row states the guarantee no single slice could.

Goal

The fifth producer, and the stack's honest final state: all five rows of the producer table implemented and fixture-pinned, HC-R11 met, and every declaration on any supported harness publishing a non-null context.

Decisions

  • Require the key, never read a missing one as zero. The numerator is tokens.total of the last non-summary assistant message.updated, and the key must be present — a message's opening frame carries all-zero token fields and no total, so reading absence as zero would publish an empty window at every turn start.
  • st2 computes the percent, unrounded and unclamped. OpenCode displays none, so there is no harness number to mirror (HC-R02's "where st2 computes it, the row says so"). Rounding is not cosmetic here: it would move the written value into a different 1% bucket than the truth, and the bucket is what the write guard keys on.
  • Withhold the denominator until the pull lands. The window is cached per providerID/modelID behind a 60 s backoff; while unknown, window and percent are null rather than guessed from a model table. A providers document that answers without naming the model is not going to start naming it on the next poll — a custom or local provider can be absent for the life of the session — so that case backs off to the API gate's own terminal horizon instead of re-pulling the whole document every minute.
  • No heartbeat (HC-R06). The numerator is pushed and not pullable, so the producer never holds a re-taken reading; publishing on a timer would restamp a stale number as fresh. A quiet seat's record ages visibly instead — which means an idle OpenCode seat past the 60-minute horizon legitimately trips Doctor's stale-record advisory. That is the intended reading, and it must not be closed by heartbeating a reading nobody re-took.
  • The summarizer message is skipped. It is an assistant message and it carries tokens, but they are the summarization call's, not the new context size. In the live smoke, publishing it would have written 0.7% of the window instead of the true 4.3%.
  • State beside EventMachine, not on it. That machine is rebuilt on every SSE reconnect; a blip must not discard the window cache.
  • DQ-C10 v1 rule: last writer wins. The producer publishes on one condition — a frame that carried a fresh numerator — with adjacent facts folded in from session.updated and riding out on that publish. Adjacent facts therefore lag until a write lands, which keeps the record one coherent snapshot rather than a mix of ages.
  • Stack reconciliation. Each slice rewrote the same Status paragraph and HC-R13 bullet from the pre-producer tree, so each carried a sentence true of its own branch and false of the stack. Both are rewritten from the tree that now exists, in producer-table order. The new INVARIANTS row Version-pinned producer arithmetic names one fixture per producer (six proofs, including Claude's withholding fixture) — a row no earlier slice could add without claiming all-harness discipline that did not yet exist.

Verification

nix develop -c cargo test --all-targets --all-features --no-fail-fast on the stack tip: 1253 passed, 12 failed (suite exit 101). Eleven are the sandbox's pre-existing pty/network failures from the branch-base baseline: st2_up_boots_a_specs_team, st2_up_once_atomically_respawns_a_hard_killed_agent, st2_up_spec_supervises_and_respawns_a_killed_agent, st2_down_tears_down_a_spec_fleet, up_materialize_only_writes_the_overlay_without_needing_pty, targeted_once_real_pty_preserves_sibling_generation_across_selected_lifecycle, clean_path_supports_help_validate_env_and_doctor, tracked_product_surface_contains_only_native_names, canonical_agents_freeze_the_admitted_route_across_post_boot_catalog_mutation, completed_catalog_aba_during_runtime_observation_is_incomplete, external_stream_removal_performs_no_runtime_operation. The twelfth is carrier_change_emits_one_superseded_resync_event_and_silent_stores_stay_quiet in tests/resync.rs — code this stack does not touch, landed in #371/#372 — which passes 3/3 in isolation and is a parallel-execution flake. The baseline's codex_app_server::tests::a_stop_during_the_websocket_handshake_ends_startup_gracefully passed in this run. Net: this stack introduces no failure.

nix flake check --print-build-logs on the stack tip: exit 0, all thirteen checks green: st2, pi-extension-types, atomic-pty-snapshot, catalog-bootstrap, completions, help, hooks-replacement, message-cli, otel-export, parked-recovery, pty-fleet-contract, wasm-resolver-artifact, wasm-resolver-feature. The new pi-extension-types check reports pi extension smoke: ok and omp extension smoke: ok.

The first invocation exited 1 on codex_app_server::tests::a_stop_during_the_websocket_handshake_ends_startup_gracefullyIO error: Interrupted system call (os error 4), a sandbox flake that is already in this machine's pre-existing failure baseline and that passed in the cargo test run above. Rebuilding that one derivation (nix build .#checks.x86_64-linux.st2) passed (604 lib tests, 0 failed), and the full re-run exits 0. main's own CI run of this workflow is green.

tests/invariants.rs::qualified_proof_references_resolve passes, so every proof named in the new INVARIANTS row resolves to a declared test. axe vrs check --profile strict docs/vrs reports 107 errors, exactly the pre-existing baseline — no new error in 08-harness-context/ or the 0014 decision files, and the DELTA-005 record introduced by PR 4 conforms to the canonical delta shape.

This slice's own pre-rebase run: 1199 passed, 13 failed (the 12 baseline failures plus bootstrap_publishes_its_lock_before_readers_can_enter, a parallel-sandbox flake). A live 1.18.25 smoke over 571 SSE frames landed exactly one write, counted the compaction once, and skipped the summarizer message.

Complexity

One ContextProducer on the existing opencode wrapper, beside its harness-state writer and under the same incarnation (provenance only), owning a harness_context::Writer. No new module, no new dependency. One new INVARIANTS row; no new requirement.

Concerns

  • An idle OpenCode seat past 60 minutes trips Doctor's stale-record line. This is correct behaviour given a push-only numerator, but it is the one harness where a healthy seat produces an advisory. Operators will see it; the alternative (a heartbeat) would restamp a reading nobody re-took.
  • The window pull depends on /config/providers staying at that path and shape; one test now drives it through the real Client and the exact path so the endpoint string is not the single untested link in the denominator.
  • usedPercent here is st2's arithmetic, not the harness's — the only row in the producer table where that is true, and stated as such.
  • The reconciliation commit rewrites documentation authored by four other slices. Each intermediate branch states the truthful subset for its own tree, so a reviewer reading PR 2, 3, or 4 in isolation is not told about producers that do not exist there yet.

Friction & bottlenecks

  • Friction: the five producer slices were developed in parallel against the same pre-producer spec section, so every one of them conflicted on the same paragraph. A per-producer status list rather than one prose paragraph would have merged cleanly; the reconciliation commit is the cost of not having one.
  • Friction: summary being an object on user messages and a boolean elsewhere is invisible to any test that does not use verbatim frames — it was found only by replaying a captured session.

Follow-ups

  • DELTA-005 (from PR 4) still needs maintainer approval to amend HC-R16.
  • Widen SUPPORTED_CODEX_CLI_VERSIONS (from PR 3) to the version the arithmetic was verified on.
  • History, supervisor actionability, and the tokenlens metrics feed — docs/vrs/08-harness-context/roadmap.md.

References

  • Parent: the pi/omp producer PR (PR 4 in this stack). Spec: docs/vrs/08-harness-context/spec.md §opencode and §Status; decision 0014; HC-R02, HC-R06, HC-R11, HC-R13; DQ-C10.
Posted on behalf of @schickling
field value
agent_identity dev3.direct.claude.4f8m9tm7
session dev3.4f8m9tm7
agent_persona generalist
agent_supervisor unavailable
agent_tool Claude Code
agent_tool_version 2.1.250
agent_runtime Claude Code 2.1.250
tooling_profile dotfiles@a1a5f89

@schickling-assistant schickling-assistant changed the title schickling/hc 05 opencode feat(harness-context): the OpenCode producer, and all five rows reconciled Aug 29, 2026
@schickling-assistant
schickling-assistant marked this pull request as ready for review August 29, 2026 16:48
schickling-assistant and others added 3 commits August 30, 2026 00:14
…rames

The one producer whose numerator is pushed and whose denominator must be pulled.
`ContextProducer` sits on the opencode wrapper beside its harness-state writer,
under the same incarnation (provenance only), and owns a `harness_context::Writer`.

Numerator: `tokens.total` of the last non-summary assistant `message.updated`,
requiring the KEY — a message's opening frame carries all-zero token fields and no
`total`, and reading a missing key as zero would publish an empty window at every
turn start. Denominator: `providers[].models[<modelID>].limit.context` from
`GET /config/providers`, pulled on demand under a 60s backoff and cached per
`providerID/modelID`; while it is unknown the window and percent are withheld
(HC-R02) rather than guessed from a model table. `usedPercent` is st2's own
unrounded, unclamped division — OpenCode displays none, and rounding would move the
written value into a different 1% bucket than the truth.

The three measured traps are the tests. `summary` is overloaded (an object, hence
truthy, on user messages), the compaction summarizer's own assistant message carries
the summarization call's cost rather than the new context size, and
`session.updated`'s `info.tokens` is cumulative with no `total` key — it is
`sessionTotalTokens` and never the numerator.

`session.compacted` carries `{sessionID}` and nothing else, so the trigger is
`unknown` and the count is st2's, incarnation-scoped. The edge carries no reading,
so the numbers keep the age they had: they genuinely predate the compaction.

The producer publishes on one condition — a frame that carried a fresh numerator —
with the adjacent facts folded in from `session.updated` and riding out on that
publish. It has no heartbeat: its numerator is pushed and not pullable, so it never
holds a re-taken reading, and a quiet seat's record ages visibly instead (HC-R06).
The state lives beside `EventMachine` rather than on it, because that machine is
rebuilt on every SSE reconnect and a blip must not discard a window cache.

Fixtures are verbatim `data:` payloads from the credential-free lab run, with the
HC-R13 version read out of the frames themselves (`session.updated.info.version` =
1.18.25). A live 1.18.25 smoke over 571 SSE frames landed exactly one write, counted
the compaction once, and skipped the summarizer message — which would otherwise have
published 0.7% of the window instead of 4.3%.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
agent-identity: dev3.direct.claude.4f8m9tm7
agent-persona: generalist
agent-supervisor: unavailable
agent-tool: Claude Code
agent-tool-version: 2.1.250
agent-runtime: Claude Code 2.1.250
tooling-profile: dotfiles@a1a5f89
…e join

A providers document that answers without naming the model is not going to start
naming it on the next poll — a custom or local provider can be absent for the life
of the session — so that case backs off to the API gate's own terminal horizon
instead of re-pulling the whole document every minute. One test now drives the pull
through the real Client and the exact `/config/providers` path, so the endpoint
string is not the one untested link in the denominator.

The spec gains the two consequences a reader would otherwise rediscover: adjacent
facts lag until a write lands, keeping the record one coherent snapshot rather than
a mix; and an OpenCode seat idle past the 60-minute horizon legitimately trips
Doctor's stale-record advisory, which must not be closed by heartbeating a reading
nobody re-took.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
agent-identity: dev3.direct.claude.4f8m9tm7
agent-persona: generalist
agent-supervisor: unavailable
agent-tool: Claude Code
agent-tool-version: 2.1.250
agent-runtime: Claude Code 2.1.250
tooling-profile: dotfiles@a1a5f89
The five slices each rewrote the same Status paragraph and the same HC-R13
verification bullet from the pre-producer tree, so each landed a sentence that
was true of its own branch and false of the stack. Reconcile both to the tree
that now exists: all five producers ship, HC-R11 is met, and nothing in the
producer table is left unimplemented.

The HC-R13 bullet now reads in producer-table order — claude, codex, pi and
omp, opencode — and names Claude's two fixture tests, which no slice had done
because the claude slice wrote the composition note instead.

Add the INVARIANTS row *Version-pinned producer arithmetic*: one row for the
guarantee no single slice could state, that every producer publishes the number
its own harness means and is pinned to the build the meaning was measured on.
The failure it bounds is a harness bump that changes a key's meaning without
changing its shape, which no type gate can see.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
agent-identity: dev3.direct.claude.4f8m9tm7
agent-persona: generalist
agent-supervisor: unavailable
agent-tool: Claude Code
agent-tool-version: 2.1.250
agent-runtime: Claude Code 2.1.250
tooling-profile: dotfiles@a1a5f89
@schickling-assistant
schickling-assistant merged commit 08b67b3 into main Aug 29, 2026
2 checks passed
@schickling-assistant
schickling-assistant deleted the schickling/hc-05-opencode branch August 30, 2026 09:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant