Skip to content

fix(openclaw): read a strand's tail past checkpoint lines; pause a sealed lane - #94

Draft
BitmapAsset wants to merge 6 commits into
chore/drop-jevfrom
fix/plugin-checkpoint-tail
Draft

BitmapAsset wants to merge 6 commits into
chore/drop-jevfrom
fix/plugin-checkpoint-tail

Conversation

@BitmapAsset

Copy link
Copy Markdown
Owner

After each event the OpenClaw plugin reads the strand's tail: for the event number (the last line) and, with
sensesMode shadow or feed, for the tempo reading (the elapsed_hours of recent events). Four cases were wrong.
In each one the engine's own record (the strand, its chain, live verify) was right; only what the plugin showed or
fed was wrong.

Stacked on #93 (chore/drop-jev).

  1. A checkpoint line as the last line. After every K-th event (--checkpoint-every K, default 1,000) probbit live appends a checkpoint line, {"checkpoint":n,"prev":...,"stance":{...},"state":{...}} (docs/persona.md section
    5.7). The plugin accepted only an event line or the header as the last line, so on every 1,000th event it logged
    no stance this turn (strand unreadable after the event) and injected no stance. A last line longer than the
    plugin's 64 KiB read (a checkpoint carries the whole state) was cut and taken for a header, so the turn reported
    event 0. Now a checkpoint line gives its checkpoint number, and the read widens (largest window 16 MiB) until it
    holds the start of the last line. A torn last line, an empty file or a missing file still give no number.
  2. Tempo past a long checkpoint line. The tempo read took the last 512 KiB; a checkpoint line longer than that
    filled it, and the next turn's tempo was 1 (this event alone). The read is now repeated with a larger window (2,
    8, then 16 MiB) until it holds more than an hour of gaps or the whole file; the largest window is 16 MiB (with the
    re-reads at most 26.5 MiB are read in one turn). There is no event-count cutoff: every event of the last hour that
    is inside the window counts.
  3. A sealed lane is paused. probbit live control STRAND seal continues a strand in NAME.0001.strand. The plugin
    kept reading the first file: before change 1 every later turn got no stance (or reported event 0 when the sealing
    line was longer than the 64 KiB read); with change 1 alone every later turn
    would have shown the sealing line's event number. The plugin does not follow segments in this version. If the
    first segment file exists (named as the engine names it: NAME.0001.strand, or PATH.0001 for a path without the
    .strand suffix), the lane is paused:
    • for a new message the plugin does not call the engine, so no event is written, and injects no stance; it warns
      once per lane: no stance this turn (the strand was sealed into segments (NAME.0001.strand exists); this plugin version does not follow segments, so the lane is paused; warned once per lane);
    • when the seal lands while a turn is running and the first segment exists by the time the engine call returns,
      that turn gets no stance either: after a successful probbit live the plugin checks for the segment again (the
      engine appends to a segment only if it existed when it took its writer lock, and never removes one). Its event is
      in the engine's record; the plugin writes no lane-log line, no last/ timestamp and no senses line for it, as on
      any failed turn. A seal that lands after that check leaves the turn its stance, with the right number;
    • a retried message that was answered before the seal gets the stance of its own event again (from the plugin's
      dedupe), with that event's number;
    • the shadow gate (before_tool_call) does not read the strand and keeps logging.
      The plugin README says so under "Logs and receipts".
  4. Tempo counted lines that are not events (since plugin 0.2.0). The tempo read counted every strand line with an
    n, so the consolidation line a rest pass writes ({"at","consolidation","n","prev","state"}) added one to the
    count. Only event lines count now, by the engine's own rule (strand::kind: a line with a control,
    checkpoint, rest, reflection or consolidation key is not an event). The plugin README's tempo paragraph
    now also describes the widening read and its 16 MiB limit.

Who was affected: every plugin lane on its 1,000th, 2,000th, ... event (the plugin passes no --checkpoint-every, so
the default applies); lanes with a very large expectation memory (checkpoint lines over 64 KiB or 512 KiB); lanes
sealed by hand (the plugin never seals a lane itself); with sensesMode shadow or feed, lanes whose strand has rest
passes (change 4).

Tests (plugins/openclaw/test/strand.test.ts, 12 new; plugin suite 153 -> 165):

  • the last line: event line, checkpoint line, header only, a header longer than 64 KiB, events followed by a
    checkpoint longer than 64 KiB, a torn line, an empty file, no file;
  • tempo with ten events and a 600 KiB checkpoint: 11, not 1; tempo with 3,000 events inside the hour, more than the
    first 512 KiB window holds: 3001; tempo over five events with a consolidation line (as the engine writes it), a
    checkpoint and a control, rest and reflection line between them, each of those with an n: 6;
  • the first segment's name, with and without the .strand suffix;
  • through the hook with the real binary: the 1,000th event (999 events written by the engine, then the hook's event,
    on which the engine appends its checkpoint) gets its stance and event 1001 works; a lane with three events, sealed
    with the real binary, then two turns: no stance block on either, one warning, every file under the plugin's state
    directory byte-identical before and after the two turns; a retry of the message answered before the seal gets the
    identical block (event 3) and changes no file; a destructive tool call then changes only the gate log (one line);
    probbit live verify passes on the segment. The same test checks that a normal turn writes the strand, the state,
    last/, the lane log and the senses line.
  • a seal that lands while a turn runs, without timing: runLiveEvent's binary check (awaited after the first segment
    check, before the engine call) runs the real live control STRAND seal; the result is the sealed refusal, the
    engine's event 3 is the last line of segment 1, and the base ends on the sealing checkpoint (2).
  • Each fix was checked against its test: with the fix reverted the matching test fails. With a 1,000-event cutoff
    put back, the tempo test gives 1834 instead of 3001; with the sealed-lane check removed, the paused turn gets a
    stance labelled event 3 while the engine has written event 4 into the segment; with the second check removed, the
    racing turn returns a stance with event number 2 for the event the engine wrote as 3; with the line-kind check
    removed, the tempo test gives 10 instead of 6.

CHANGELOG "Unreleased / Fixed" has one entry per change. The plugin version stays 0.3.0 (not yet published). The test
count in docs/adapters.md is 165.

Validation on the host (macOS arm64), plugin tests run against a release build of probbit from this stack, as CI
does:

  • Plugin npm test at the branch head: 165 tests, 165/165 on five consecutive runs. Two tests with a 500 ms spawn
    budget ("hook, a binary with --label", the safety "exit 2" test; both on main too) failed now and then in
    earlier runs of this branch and pass on a rerun.
  • Python python3 python/test_probbit.py: 16/16 (at the first commit; later commits touch no Python).
  • Rust cargo test --offline -p probbit-cli: 305 passed, 0 failed, 3 ignored (at the first commit; later commits touch
    no Rust).
  • scripts/generate_llms_full.py --check passes.

This branch has not been deployed

No deployments
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