feat(harness-context): the OpenCode producer, and all five rows reconciled - #381
Merged
schickling-assistant merged 3 commits intoAug 29, 2026
Merged
Conversation
schickling-assistant
marked this pull request as ready for review
August 29, 2026 16:48
schickling-assistant
force-pushed
the
schickling/hc-05-opencode
branch
from
August 29, 2026 21:20
3763215 to
e8ef335
Compare
schickling-assistant
force-pushed
the
schickling/hc-05-opencode
branch
from
August 29, 2026 21:29
e8ef335 to
4b4f11c
Compare
…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
force-pushed
the
schickling/hc-05-opencode
branch
from
August 29, 2026 22:14
4b4f11c to
600c445
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
OpenCode is the last harness with no
context, and the only one whose numerator is pushed while its denominator must be pulled: SSEmessage.updatedcarries the token counts, and the model's context limit lives behindGET /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.
summaryis 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; andsession.updated'sinfo.tokensis cumulative with nototalkey — it issessionTotalTokensand 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-
nullcontext.Decisions
tokens.totalof the last non-summary assistantmessage.updated, and the key must be present — a message's opening frame carries all-zero token fields and nototal, so reading absence as zero would publish an empty window at every turn start.providerID/modelIDbehind a 60 s backoff; while unknown, window and percent arenullrather 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.EventMachine, not on it. That machine is rebuilt on every SSE reconnect; a blip must not discard the window cache.session.updatedand 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.Verification
nix develop -c cargo test --all-targets --all-features --no-fail-faston 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 iscarrier_change_emits_one_superseded_resync_event_and_silent_stores_stay_quietintests/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'scodex_app_server::tests::a_stop_during_the_websocket_handshake_ends_startup_gracefullypassed in this run. Net: this stack introduces no failure.nix flake check --print-build-logson 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 newpi-extension-typescheck reportspi extension smoke: okandomp extension smoke: ok.The first invocation exited 1 on
codex_app_server::tests::a_stop_during_the_websocket_handshake_ends_startup_gracefully—IO 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 thecargo testrun 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_resolvepasses, so every proof named in the new INVARIANTS row resolves to a declared test.axe vrs check --profile strict docs/vrsreports 107 errors, exactly the pre-existing baseline — no new error in08-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
ContextProduceron the existing opencode wrapper, beside its harness-state writer and under the same incarnation (provenance only), owning aharness_context::Writer. No new module, no new dependency. One new INVARIANTS row; no new requirement.Concerns
/config/providersstaying at that path and shape; one test now drives it through the realClientand the exact path so the endpoint string is not the single untested link in the denominator.usedPercenthere is st2's arithmetic, not the harness's — the only row in the producer table where that is true, and stated as such.Friction & bottlenecks
summarybeing 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
SUPPORTED_CODEX_CLI_VERSIONS(from PR 3) to the version the arithmetic was verified on.docs/vrs/08-harness-context/roadmap.md.References
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
agent_identitysessionagent_personaagent_supervisoragent_toolagent_tool_versionagent_runtimetooling_profile