Skip to content

protocol: reuse-feedback-obs, SPEC 10.1 - feature negotiation, and each observation sent once - #11

Merged
tactino merged 9 commits into
mainfrom
feat/reuse-feedback-obs
Oct 2, 2026
Merged

tactino merged 9 commits into
mainfrom
feat/reuse-feedback-obs

Conversation

@tactino

@tactino tactino commented Oct 1, 2026 •

Copy link
Copy Markdown
Member

Every observation crosses the link twice: once in the feedback that ends a chunk, and again in the next infer as the state the new chunk starts from. The two differ only after a reset. That second copy is the factor of two in E43's cost model (2.8 ms + 2 × observation bytes / bandwidth). This removes it, and adds the negotiation it needs to stay compatible both ways. Stacked on #10 (and #9, #8): merge those first. The three commits after the merge of #8's branch are new.

Negotiation (SPEC 10).

  • A server lists the features it implements in metadata's features.
  • A client uses a feature only if it is listed there.
  • A server that lists one still accepts every client that does not use it.

So a new client works with an old server, and an old client with a new one. protocol_version stays 1. A change that cannot be made this way is the one that would move it to 2.

reuse-feedback-obs (SPEC 10.1). An infer may carry reuse, a bool array with one entry per row:

  • A row it marks sends no observation. The server takes it from the obs of that env's last feedback on this connection.
  • data is batched to the rows that are not reused.
  • A client MUST NOT reuse for an env whose last feedback ended its episode, since that observation was the terminal one. It also MUST NOT reuse for an env with no feedback on this connection yet, which covers every env after a reconnect.
  • A server MUST close for a resync on a reuse it cannot honour.

What is in it:

  • plugrl_protocol.reuse.ObservationCache: the server's half, one per connection. on_feedback stores each env's row, or marks that its episode ended. complete rebuilds the full observation of an infer and raises ReuseError on a forbidden reuse. on_feedback never raises. A server feeds it every feedback, from clients that never reuse too, so a feedback it cannot split (no text key, say) only makes those envs not reusable.

  • plugrl-conformance --features reuse-feedback-obs: offers the feature to a client. The probe checks still see the full observation, so every other clause is graded as before. A client that reuses something the server does not hold fails 10.1. A client that is offered the feature and never uses it gets a note. Without the flag nothing changes, and a client that sends reuse anyway fails.

  • plugrl-conformance-server: a new reuse phase, which runs when the server lists the feature. It checks four things:

    • an infer with some rows reused is answered;
    • an infer with every row reused (an empty data batch) is answered;
    • a reuse right after a terminal feedback closes for a resync;
    • a reuse on a fresh connection closes for a resync.

    A server that does not list the feature gets one note.

  • Reference implementations: raw_client.py uses the feature when offered (--no-reuse turns it off), and reference_server.py offers it.

  • Bugs that show each clause is checked: reuse-unoffered and reuse-after-reset on the client, lenient-reuse on the server.

Checks:

  • Local run: 71 passed, 17 of them in tests/test_reuse.py, which unit-tests the cache on its own. ruff 0.14.2 is clean.
  • plugrl-server main (835f691) passes the new server checker with no violations and one note, because it does not offer the feature yet.

Before this commit, the branch merges in #8's last commit (the 7.6 refinement), so that #10's branch carries all of #8.

Users:

  • plugrl-server#109 offers the feature, using ObservationCache. A reusing client trains the same weights bit for bit.
  • plugrl-env-client#16 uses it when offered.
  • E46 re-measures E43's ladder next, interleaving v1 and v2 runs.

tactino and others added 9 commits October 1, 2026 16:34
…the checker sees what a client does with each action and how it handles closes

The passive checker watched one connection and could not tell chunk-summed
reward, terminal observations, time-major order, metadata-first, the close
reasons or text frames from their violations. With --probe the client runs a
probe env whose observation counts steps and echoes the applied action, and
the checker drives the connection: it speaks late, sends a 2 MiB metadata
frame, closes for a resync, stops the run, and answers with a text frame.
raw_client.py implements the probe env, and its --bug option breaks each
clause in turn; tests/test_conformance_probe.py checks that each is named.
…fter

Found by the probe checker itself, on Linux, in about one run in two of the
resync scenario: when the server's resync close arrived while the feedback
was being sent, the send raised, the reset after it never ran, and env 0
carried its finished episode onto the next connection (t = 7, 11, 15 for an
episode of 3). The packed feedback already holds the terminal observation,
so resetting first changes nothing the server sees.
…ed in CI in every scenario

`plugrl_client --probe host port batch` runs the probe environment, executes
each chunk time-major and flushes it at the terminal step, and handles the
section 7 close reasons: it reconnects after plugrl-server-resync with
nothing resent, exits 0 on plugrl-server-stop, and exits 2 on a text frame.
A close now raises ServerClosed with its code and reason, and a text frame
TextFrame, instead of one generic error. Passive mode is unchanged.
…s a 7.6 violation; raw_client --bug stale-chunk

plugrl-env-client failed the resync scenario this way: after reconnecting it
fed back chunks answered on the old connection, which the checker reported
under 4.3. On a reconnected connection, feedback for an env that was never
given an action there is now named for the clause it breaks. The new
stale-chunk bug does exactly that, and the test expects the 7.6 clause.
… 8.2), and examples/reference_server.py is one written against SPEC alone

A client that drives a server the way SPEC lets a client behave - ragged
batches, a feedback set unlike the infer's, large frames, three kinds of
malformed message, two connections sharing env 0, and the end of the run -
and checks the action layout and env_ids, the resync closes, that the server
survives a bad client, and the stop reason. The reference server trains
nothing and passes; its --bug option breaks each server clause in turn.
plugrl-server main passes; its version before #108 fails two checks.
…e - each observation crosses the link once

Every observation crossed the link twice: in the feedback that ends a
chunk, and again in the next infer as the state the new chunk starts
from. That second copy is the factor of two in E43's cost model. A server
now lists the features it implements in its metadata's `features`, and
with this one an infer marks in `reuse` the rows whose observation the
server already holds from that env's last feedback on the connection, and
sends only the others.

- SPEC: 5.1 `features`, 5.2 `reuse`, section 10 says what a feature is
  (an addition a server offers and a client uses only when offered, so
  old and new versions talk both ways), 10.1 the feature itself, and a
  line in each checklist.
- plugrl_protocol.reuse.ObservationCache is the server's half: it keeps
  each env's last feedback observation, or that its episode ended, and
  completes an infer from it, raising ReuseError on a reuse SPEC forbids.
- plugrl-conformance --features reuse-feedback-obs offers the feature to
  a client and checks that it reuses only what the server holds; without
  the flag nothing changes.
- plugrl-conformance-server exercises it when the server lists it:
  partial and full reuse are answered, and a reuse after a terminal
  feedback, or with no feedback on the connection, closes for a resync.
  A server that does not list it gets one note.
- raw_client.py uses it when offered (--no-reuse turns it off), and
  reference_server.py offers it. The bugs reuse-unoffered,
  reuse-after-reset and lenient-reuse show each clause is checked.
… ObservationCache

Merging rows whose text was None produced the strings "None". Only the
end-to-end checker tests covered reuse.py, and their clients always send
text, so nothing caught it. tests/test_reuse.py now checks the cache on its
own: row order, every row reused, the latest feedback wins, no reuse after
terminated or truncated or with no feedback, malformed reuse and data, and
text in each form a client may send it.
…s its envs not reusable

A server feeds every feedback to the cache, from clients that use the
feature and clients that do not. on_feedback raised ReuseError on an
observation it could not split by env, such as one with no text key, so a
version 1 client whose feedback the server had always accepted would have
been closed for a resync. Now the envs of such a feedback are only marked as
not reusable, with the reason, and a later reuse of one raises in complete.
Validating a feedback stays the server's business: reference_server.py and
the client checker no longer treat the cache as a validator.
@tactino
tactino merged commit 0a76192 into main Oct 2, 2026
3 checks passed
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