Skip to content

RS-13: A16 loss-repair fix (DEL repeat + repair share), VECTOR code guards, procedures and desk-check tool - #143

Merged
dorkmo merged 12 commits into
mainfrom
rs13-a16-fix-2026-10-10
Oct 11, 2026
Merged

dorkmo merged 12 commits into
mainfrom
rs13-a16-fix-2026-10-10

Conversation

@dorkmo

@dorkmo dorkmo commented Oct 10, 2026

Copy link
Copy Markdown
Contributor

These are the RS-13 follow-ups that need no radio, prepared after the RS-13.1 campaign (record: bench-evidence/RS_13_vector_scene_2026-09-26/RESULTS.md). Nothing here has flown. On-air confirmation belongs to the range-edge session, on an explicit operator GO.

A16 fix: loss repair on a saturated stream

The problem (RS-13.1 A16, reproduced off air):

  1. A lost frame carrying a DEL leaves a ghost at the base for 20 frames.
  2. Meanwhile the DIGEST mismatches and the store goes into resync, where it refuses CONFIRMs.
  3. CONFIRM-only shapes then expire at the base, while the encoder still counts them live.
  4. The slot-7 carousel, packed last, never re-sends them, so the base stays shapes short until the next epoch start. That happened twice on air, for about 30 s each.

The fix (4b464cba store, e0309c8f encoder):

  • DEL repeat-once. Every DEL goes out again in the next frame, ahead of that frame's changes and outside the CONFIRM/DIGEST reserve. A re-use define of the id replaces the repeat. The repeat never crosses an epoch start that clears the layer.
  • Repair share. Every fourth frame, the first 25 % of the frame goes to re-sends: carousel re-sends and repeat-once copies alike, the oldest statement first. It is taken before the changes and repeats.
  • Store. A DEL naming an absent id is no longer an orphan, because the second copy names an id the base already deleted.

How it was chosen. A judge panel compared three candidates built in parallel:

  • DEL repeat only: meets the single-drop bar, but collapses under 2–3-frame bursts and random loss.
  • Store accepts matching CONFIRMs in resync: fails at bw250 (1 failure in 100, recovery up to 31 frames) and weakens the §4.3 2-bit tag guard.
  • The chosen fix (DEL repeat plus repair share): the only one that holds up beyond single drops.

Results:

test before after
a16_sil.py, 100 single drops, bw500 / bw250 33 / 60 A16-like 0 / 0, recovery p50/p90/max 3/7/10 and 7/9/11 frames, loss-free out-of-step 0
judge's adversarial SIL, 2- and 3-frame bursts (repeat-only: up to 37/40) 0/40 at both budgets and on an id-pressure scene
5–12 % random loss: longest run with a shape missing until the epoch start 7–14 frames
residual (quality cost) — +0.3 % (bw500), +0.8 % (bw250)

Both full-run numbers were reproduced independently by the judge, and again on this integrated tree.

Spec (VECTOR_SCENE.md, needs the design owner's sign-off):

  • §4.3 gains the DEL-repeat and repair-share bullets and the repeat's drop rules.
  • §4.2 items 5 and 7 change to match.
  • §3.4 rule 4 says DEL is no longer an orphan, so it no longer feeds the orphan resync rule.
  • §4.5.4 records an open item: the fixed 1-in-4, 25 % share is not enough at V2 (SIL single drop 3/40; residual +3–4 % at V1/V2). Scaling it with the degradation ladder is the follow-up.

Code guards (rs13/guards, 32 unit tests)

  • Batching. image_tx_daemon never batches codec-6 frames. A frame that fits one fragment is priced as one; the old pricing turned near-full 243 B frames into 2-fragment trains under -TxBatch 1. This also changes tile-mode batching, which is unflown (every RS-13 leg ran -TxBatch 0).
  • Cold-start budget. camera_service limits the VECTOR byte budget to one fragment until link_budget arrives. On air, 2a_r4 started at byte_budget=2436.
  • Encoder pre-build. camera_service builds the VectorEncoder ahead of time, so a mode switch no longer leaves a ≥ 1 s TX gap. On FHSS such a gap would break the strict authority streak. LIFETRAC_VECTOR_PREBUILD=0 turns this off.
  • One epoch start. Entering VECTOR now gives one epoch start, not first followed by forced.

Procedures and the desk check

  • RS13_VECTOR_LEG.md "Amendments 2026-10-10" apply from the next round; RS-13.1 stays judged by the old text.

    • P8 becomes an absolute bound (A17).
    • P6's return clause counts the in-flight frames (A18).
    • Every profile-2 command gets -ForceFrfHz <spot-checked carrier> (A19).
    • The P6 cap and the P8 rule need the design owner's sign-off.
  • New RS13_RANGE_EDGE_LEG.md for the next radio session:

    • hardware for a conducted, shielded attenuator path;
    • a same-day channel spot-check;
    • a 0 dB step with VECTOR and mono_g4 interleaved and -ReactiveFire 1, which gives the §8.6 command-delivery row and the A16 confirmation;
    • the walk-down steps, stop rules, and a results template.
  • RS-13.2 desk check:

    • tools/vector_replay_publish.py replays a capture to MQTT at its own timing, with drop injection (23 tests);
    • RS13_2_DESK_CHECK.md and rs13_2_desk.compose.yml.

    A rehearsal already found renderer gaps F1–F10, recorded for follow-up: the RESYNC chip is clipped, the console buttons cover the banner, and fail-closed rules 1 and 5 are missing.

Verification

  • base_station suite on this branch: 1598 passed, 2 skipped; on origin/main it was 1537 passed.
  • The a16_sil.py full run above was done on this integrated tree.
  • Nothing touched the boards; the radios stayed asleep.

Notes

🤖 Generated with Claude Code

dorkmo and others added 11 commits October 10, 2026 13:29
…size

With LIFETRAC_TX_BATCH=1 (the radio-monitor harness default)
image_tx_daemon._batch_more priced the FIRST frame of a would-be batch with
the 4 B container overhead, although a batch of one goes out unbatched. Every
encode-to-fit frame of 240..243 B (BW500; 200..203 B at BW250) therefore
looked like 2 fragments, and two full single-fragment frames were paired into
a 492 B, 3-fragment train (2 fragments unbatched). Verified off air: two
243 B codec-6 deltas -> one 492 B batch -> 3 fragments.

- codec-6 (VECTOR) frames are never batched, as the first frame or as a
  follower (carried, air order kept). The VS1 encoder sizes every frame to
  one fragment body, so batching can only lengthen the train, and it couples
  the loss of consecutive scene frames with their DELs (A16).
- a lone frame is priced at its own payload, and a batch must never cost more
  fragments than sending the follower on its own (486 B + 243 B passed the
  per-frame guard at 4 fragments vs 3).
- small frames still batch (RS-3.1) and 246 B pairs/triples keep the runt
  amortization the guard was written for.

Tests: test_tx_batch_vector_guard.py (12; 8 fail on the old daemon),
including a pair-size property sweep at both fragment bodies.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nge-edge procedure

RS13_VECTOR_LEG.md, amendments dated 2026-10-10 that apply from the next
round (RS-13.1 stays judged by the text it flew under, quoted as "was"):
- P8 is an absolute bound: on-air vector_stats p95 <= the encoder budget at
  the leg's camera rate (350 ms at 2 fps); the ratio to step 1 is recorded
  for information (A17).
- P6's return clause counts the frame(s) already in the TX pipeline:
  codec-1 resumes within (1 + n) camera periods + one fragment airtime of
  the return command going on air, n <= tx_pipeline_depth +
  tx_prepare_ahead (A18).
- The literal profile-2 leg command carries -ForceFrfHz <spot-checked
  carrier>; 2b (profile 1) explicitly does not (A19).

RS13_RANGE_EDGE_LEG.md (new): the next radio session, not flown, radios
only on an explicit GO per round. Conducted SMA-attenuator path (30 dB pads
on both jacks, 20 dB fixed, 3/6/12/15/30 dB step set to 66 dB), safety rules
(never key an open port, never join the jacks without the fixed pad, touch
RF only with both radios parked), leakage check, same-day spot-check ->
-ForceFrfHz, Round A 0 dB step (2c, 2a, 2c', 2a' with -ReactiveFire 1
-ProbeEcho 0: the VECTOR_SCENE.md 8.6 command-delivery row and the A16-fix
confirmation at bw500), Rounds B/C walk-down per profile with per-step
measures, RSSI tracking rule, stop rules and a RESULTS template. FHSS
command and switch legs wait for the RS-12.15 reverse-delivery fix.

RS13_RESULTS_TEMPLATE.md: P6/P8 rows follow the amendments.
BENCH_RUNBOOK.md: index row for the range-edge procedure; -ForceFrfHz on
the generic leg line (profile 2 only, prep 8).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…one epoch start)

Three findings of the 2026-10-04 reviews, each verified against the 2d_r4
leg (bench-evidence/radio_monitor_20261004_110423_4ab58b8d):

(b) Cold-start budget. In bridge mode the boot budget is the 12-fragment
    LIFETRAC_FRAGMENT_BUDGET_BRIDGE_DEFAULT (byte_budget=2436 in the log)
    until image_tx_daemon's retained tractor/link_budget arrives. VECTOR is
    now held to one fragment of the active profile (LIFETRAC_REG_PROFILE,
    which the harness passes to camera_service) or of the most conservative
    image profile (203 B) until LinkBudget.update has run. Tile modes keep
    the provisional budget; a known link_budget stays authoritative.

(c) Lazy encoder build. The first VECTOR frame imported OpenCV +
    encode_vector and paid the process's first-call costs (l0 79.6 ms vs
    ~15 ms); the switch left a 1.038 s TX gap, past the strict < 1000 ms
    FHSS authority streak. main() now builds the encoder before the first
    capture and before MQTT, and warms the process with one frame on a
    throwaway encoder (first-call costs are per process, not per encoder;
    the kept encoder stays fresh, so its first frame is still "first").
    Off air on the dev PC: first VECTOR frame 350-510 ms lazy -> 50-80 ms
    prebuilt (steady ~45 ms). LIFETRAC_VECTOR_PREBUILD=0 restores the lazy
    build.

(d) Double epoch start on every VECTOR entry (trigger first, then forced):
    a real race, not cosmetic. The loop read the keyframe request BEFORE
    cam.grab_rgb() (which blocks up to a frame period on the ffmpeg pipe)
    and ENCODE_MODE AFTER it. The ENCODE_MODE command landed in that window
    (cmd 08:05:22.520, TX gap 22.245 -> 23.283, a second frame 186 ms later
    at 23.469), so the first VECTOR frame was built without its force (the
    fresh encoder's own "first" start) and the next frame consumed the
    force: a redundant LAYER_CLEAR 0 restart. On RE-entry the same race made
    the first frame a delta against the previous session's stale mirror,
    and a switch landing mid tile build could stamp codec 6 on tiles.
    Fix: _MODE_LOCK makes "commit mode + raise its keyframe"
    (_apply_encode_mode, dispatch_back_channel) atomic against "sample mode
    + consume request", which _build_frame now does after the capture
    (force_evt=); the frame is built in that one sampled mode end to end
    (tile encodes, cache key, codec byte). The M7 path's is_keyframe comes
    from FrameAccum.last_forced. Contract unchanged: a mode change still
    sets the event; quality-only changes still do not.

Tests: test_vector_entry_guards.py (20), including the 2d_r4 mechanism
reproduced through the legacy call order, single-start entry and re-entry,
lock-held keyframe raising, and main() building the encoder before the
first capture.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tools/vector_replay_publish.py publishes a vector_dry_run capture to an MQTT
broker at the capture's recorded inter-frame timing (absolute schedule, so a
slow publish never drifts), with --speed, --loop/--passes, row windows
(--start-row/--end-row/--start-at-key), seq-gap drop injection
(--drop-rows by capture row, --drop-every by position in the pass), a
per-frame JSONL --log and a --dry-run. Stdlib + paho only (runs inside
lifetrac-v25:latest); QoS 0, never retained. Rows are numbered as the dry-run
report and RS-13.1 RESULTS number them.

tests/test_vector_replay_publish.py (23 tests, no broker): capture reader
parity with vector_dry_run.iter_capture (incl. the real 2a_r4 capture),
timing math (speed, max-gap, backwards steps), windows and start-at-key,
drop injection keeping the time slot, publish order and absolute schedule
with a fake clock and publisher, passes/loop/stop/Ctrl-C, QoS 0 + no retain,
CLI dry run and usage errors.

bench_tools/RS13_2_DESK_CHECK.md: rebuild the base image first (the deployed
4623980c2dac predates VECTOR web_ui), stage, bring up mosquitto + web_ui only
through bench_tools/rs13_2_desk.compose.yml (lora_bridge behind an unused
profile, bench radio endpoint off, own volumes, down -v), replay the four
round-4 captures, the VECTOR_SCENE.md 7.5/4.3/3.5/6 checklist (C1-C20), what
A16 looks like on screen with store-checked injected-drop expectations, the
uplink tap, and the RESULTS skeleton under bench-evidence/RS_13_2_desk_check_*.
Appendix A records a PC rehearsal (loopback broker + working-tree web_ui, no
board): F1 banner under the console buttons, F2 RESYNC chip drawn past the
384 px canvas edge, F3/F4 fail-closed rules 1 and 5 not implemented, F5/F6
banner age/loss and SAFETY DETECTOR chip missing, F7 anchor age on
NO_HORIZON epochs, F8-F10 minor. The stale-tile worker stayed silent through
125 s of VECTOR. BENCH_RUNBOOK lists the new files.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
VS1 now sends every DEL twice (its frame and the next, VECTOR_SCENE.md
§4.3), so a base that applied the first copy sees the second name an id it
no longer holds. Counting that as an orphan (§3.4 rule 4) would put an
orphan on every DEL of a loss-free stream. The id is absent either way, so
the store ignores it without an orphan and leaves a tombstone at its
capture time (an existing tombstone is kept), which still refuses an older
define of the id. A DEL of a held shape is unchanged (LWW against the
define's geometry clock), so a repeat can never delete a newer shape.

- vector_scene_store.py: the DEL branch of _apply_record.
- VECTOR_SCENE.md §3.4 rule 4: DEL leaves the orphan list.
- test_vector_scene_store.py: new test_a_del_naming_an_absent_id_is_no_orphan
  (duplicate DEL, never-held id, older define refused, newer accepted, the
  repeat inside the next epoch's start); the three orphan->resync tests use
  an INSERT for the unknown id instead of a DEL, counts unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…frame

RS-13.1 A16: on a saturated VECTOR stream one lost frame that carried a
DEL left the base a ghost for TTL_FRAMES applied frames; the DIGESTs
mismatched on n_live, the resync refused the CONFIRMs that keep static
shapes alive, the base TTL-dropped them while the mirror still counted
them, and the carousel (slot 7, packed last) never got room to re-send
them, nor was a repeat-pending shape ever offered to it, so the base
stayed shapes short until the next epoch start (~30 s on air).

Encoder-only fix (with the store's DEL rule of the previous commit), no
CONFIRMs accepted in resync, so the 2-bit-tag guard of §4.3 stands:

1. DEL repeat. Every DEL goes out again in the next frame (slot 5 head,
   outside the CONFIRM/DIGEST reserve), so one lost frame never loses a
   DEL. A fresh define that re-uses the id in that frame replaces the
   repeat (it already states offset/colour for a base that missed the
   DEL; a DEL and a define of one id in one frame would share a capture
   time and the tombstone would refuse the define). The repeat stays owed
   until a repeat or a define of the id goes out; it is dropped for a
   layer an epoch start clears, and after TTL_FRAMES.
2. Repair share. Every fourth frame (V0's kappa duty at every level; at V0
   the carousel frame itself) up to 25 % of the frame goes to re-sends
   BEFORE slots 5-6: carousel and repeat-once candidates alike, oldest
   (re)statement (define_frame) first. For an unchanged shape that is its
   TTL risk at a base in resync (the mirror's frames_since_verify is reset
   by CONFIRMs, so it is no guide); a shape with an UPD/UCOL since sorts by
   the same old frame, so a lost change is re-stated early. Not every
   frame (measured worse in the RS-13.1 sweep).

Off-air acceptance (a16_sil.py, 400 frames, 100 single drops per case):
  case                     A16-like  recovery p50/p90/max  residual_p50
  bw500 243 B real          0/100        3 / 7 / 10        1656.6 (shipped 33/100, 20/54/67, 1651.6)
  bw500 243 B norefresh     0/100        3 / 7 / 9         1656.8 (shipped 39/100)
  bw250 203 B real          0/100        7 / 9 / 11        1668.6 (shipped 60/100, 23/60/66, 1655.3)
  bw250 203 B norefresh     0/100        7 / 9 / 12        1670.5 (shipped 81/100, 1656.8)

  every disturbed episode recovered in the 70-frame window (0 unrecovered),
  lossfree_out_of_step_frames 0, base TTL drops == encoder TTL drops (0) in
  every window, no ghost survives the frame after the loss, carousel-due
  frames packing no re-send 0-3/99 (shipped 54/99 and 93/99). Residual
  +0.3 % (243 B) and +0.8 % (203 B) against the shipped encoder.

Cost: ~2.6 DEL repeats per frame on the A16 scene (29 bits/frame at
243 B, 28 at 203 B, 1.5-1.8 % of the frame); the share moves up to 25 %
of one frame in four from changes to re-sends. Encoder time unchanged
(interleaved A/B on the A16 scene: p50 54.6 vs 55.8 ms at 243 B, 58.1 vs
57.7 ms at 203 B). Worst gap without a define/re-send/UPD for any live
shape on the A16 scene: 7-8 frames (was 67 at 243 B, 114 at 203 B).

Limits: at V1/V2 (100/40 B bodies) the fixed 1-in-4 share does not keep
the 35-shape A16 scene inside the TTL (worst gap 27 frames at V1, 61 at
V2; shipped 119/120). The floor/floorall columns of a16_sil.py patch
enc._pack with the shipped-packer counterfactual, so for this branch they
measure that patch, not the fix.

Tests: new SaturatedLossTests in test_vector_sync.py (A16 scene at 243 and
203 B: loss-free in step with no orphans; a lost DEL frame back in step
within TTL_FRAMES+3+one carousel period with no epoch start, base TTL
drops == encoder's, never shapes short after the ghost expiry; every DEL
repeated exactly once unless re-used; the hard-verify gap stays under the
TTL), test_del_repeat_yields_to_an_immediate_reuse_and_never_deletes_it.
Adjusted: test_upd_ucol_and_del (the repeat DEL), the lost-DEL-past-the-
cooldown test (loses the repeat frame too, to keep a ghost), the V2
fill-up precondition of the tiny-budget refresh test (7 frames, not 6).
VECTOR_SCENE.md §4.2 items 5 and 7, §4.3 DEL repeat and repair share.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… V2 repair-share open item

- VECTOR_SCENE.md §4.3: an owed DEL repeat is also dropped when an
  epoch start clears the id's layer and after TTL_FRAMES; on a range-1/2
  key frame it still goes out for the kept layers (relies on §3.4 rule
  4, tombstones are per epoch).
- §3.4 rule 4: DEL is no longer an orphan, so it no longer feeds the
  >20 % orphan resync input.
- §4.5.4: open item - the fixed one-frame-in-four, 25 % repair share is
  not enough at V2 (SIL single drop 3/40, 99-frame missing runs under 5 %
  random loss, residual +3-4 % at V1/V2); scale it with the ladder.
- TODO RS-13: A16 fix, code guards, procedure amendments, desk-check
  prep and the range-edge procedure marked as prepared on this branch,
  with what stays open (V2 share, carousel counters, grid-policy SIL,
  design-owner sign-off, on-air confirmation on a GO).

Integrated tree: base_station 1598 passed / 2 skipped; a16_sil.py full
run 0/100 at both budgets (recovery max 10 / 11), lossfree 0, residual
+0.3 % / +0.8 %.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 10, 2026 20:03

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Procedure scoring can produce invalid or ambiguous pass results, and replay CLI validation is incomplete.

4 open findings
What changed in this PR

Implements RS-13 loss repair, VECTOR entry safeguards, replay tooling, and future bench procedures.

Changes:

  • Adds DEL repetition and scheduled repair capacity.
  • Hardens VECTOR startup, budgeting, and transmission.
  • Adds replay tooling, tests, and range-edge procedures.
File Description
VECTOR_SCENE.md Specifies loss-repair behavior.
tools/​vector_replay_publish.py Adds timed MQTT capture replay.
TODO.md Updates RS-13 status.
RS13_VECTOR_LEG.md Amends existing leg criteria.
RS13_RESULTS_TEMPLATE.md Updates evidence fields.
RS13_RANGE_EDGE_LEG.md Adds range-edge procedure.
rs13_2_desk.compose.yml Isolates desk-check services.
RS13_2_DESK_CHECK.md Documents browser replay checks.
BENCH_RUNBOOK.md Links new procedures and tools.
encode_vector.py Implements DEL repeats and repair share.
image_tx_daemon.py Prevents VECTOR batching and fixes pricing.
camera_service.py Adds budget, prebuild, and mode-switch guards.
test_vector_sync.py Tests saturated-stream recovery.
test_vector_scene_store.py Tests absent-ID DEL handling.
test_vector_replay_publish.py Tests replay behavior.
test_vector_entry_guards.py Tests VECTOR startup guards.
test_vector_encoder.py Updates DEL-repeat expectations.
test_tx_batch_vector_guard.py Tests batching safeguards.
vector_scene_store.py Treats absent-ID DEL as idempotent.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

|---|---|---|
| P1–P5, P7, P8 | As in RS13_VECTOR_LEG.md, amended 2026-10-10: P8 is the absolute 350 ms bound at 2 fps. **P3 interleaved:** pooled seq-gap loss of 2a + 2a′ against 2c + 2c′, two-sided Fisher exact p ≥ 0.05, or a margin stated in RESULTS before the legs. These legs carry probes, so their image loss compares only with each other, not with RS-13.1's probe-free legs | RS13_VECTOR_LEG.md Step 2 |
| R0 | Per leg, the median of the base's 10-s `rx_rf:` RSSI medians, and the same for SNR. R0 is the median over the four legs and must sit in −65 to −75 dBm. If it does not, change the fixed middle pad (radios parked) before Round B and re-measure with one 120 s VECTOR leg. The 0 dB legs stand, with their level recorded | archive `rx_daemon.log` |
| command delivery (§8.6) | Per leg: the distinct PROBE seqs in the tractor's `LoRa cmd: PROBE seq=N … (rx#M)` lines that also appear in the base's `PROBE TX seq=N … (tx#K)` lines, over the base's PROBE TX count. Pool VECTOR (2a + 2a′) and `mono_g4` (2c + 2c′). **Not worse** = VECTOR ≥ `mono_g4`, or two-sided Fisher exact on delivered / undelivered p ≥ 0.05. Ignore the base's own `probe:` line: with `-ProbeEcho 0` it counts echoes and reads 0 %. Keep the score as `legs/<leg>_probe_score.txt` | archive `tx_daemon.log` (tractor), `rx_daemon.log` (base) |
Comment on lines +123 to +125
(1 + n) camera periods + 1 fragment airtime. n is the number of codec-6
frames the base receives between that line and the first codec-1 payload:
the frames captured or already queued when the command landed, which the
"broker at the capture's timing, with optional drop injection.")
ap.add_argument("capture", help="vector_dry_run capture JSONL")
ap.add_argument("--host", default=os.environ.get("LIFETRAC_MQTT_HOST", "127.0.0.1"))
ap.add_argument("--port", type=int, default=int(os.environ.get("LIFETRAC_MQTT_PORT", "1883")))
| P1–P5, P7, P8 | As in RS13_VECTOR_LEG.md, amended 2026-10-10: P8 is the absolute 350 ms bound at 2 fps. **P3 interleaved:** pooled seq-gap loss of 2a + 2a′ against 2c + 2c′, two-sided Fisher exact p ≥ 0.05, or a margin stated in RESULTS before the legs. These legs carry probes, so their image loss compares only with each other, not with RS-13.1's probe-free legs | RS13_VECTOR_LEG.md Step 2 |
| R0 | Per leg, the median of the base's 10-s `rx_rf:` RSSI medians, and the same for SNR. R0 is the median over the four legs and must sit in −65 to −75 dBm. If it does not, change the fixed middle pad (radios parked) before Round B and re-measure with one 120 s VECTOR leg. The 0 dB legs stand, with their level recorded | archive `rx_daemon.log` |
| command delivery (§8.6) | Per leg: the distinct PROBE seqs in the tractor's `LoRa cmd: PROBE seq=N … (rx#M)` lines that also appear in the base's `PROBE TX seq=N … (tx#K)` lines, over the base's PROBE TX count. Pool VECTOR (2a + 2a′) and `mono_g4` (2c + 2c′). **Not worse** = VECTOR ≥ `mono_g4`, or two-sided Fisher exact on delivered / undelivered p ≥ 0.05. Ignore the base's own `probe:` line: with `-ProbeEcho 0` it counts echoes and reads 0 %. Keep the score as `legs/<leg>_probe_score.txt` | archive `tx_daemon.log` (tractor), `rx_daemon.log` (base) |
| A16 confirmation (2a, 2a′, bw500) | Every resync episode opened by an **isolated** lost frame (no other loss within 25 frames after it) ends **by DIGEST** (`digest` in the dry-run `resync:` episodes line) within **≈ 25 frames of the lost frame**, counted in capture rows from the seq gap to the episode's end. That is the `a16_sil.py` acceptance figure; a lost DEL's ghost alone holds the base out of step for TTL_FRAMES = 20 applied frames. No such episode ends at an epoch start more than 25 frames after its loss. `scripts/digest_trace.py` over each episode shows the base's live count back at the encoder's DIGEST `n_live` within those frames, with no later row where the encoder counts more live shapes than the base. Record the base's `ttl_dropped`, and the encoder's TTL/carousel counters if `vector_stats` carries them. The final `digest_ok` is not `False` (`leg_replay.py`'s `last DIGEST checked` beside it) | capture summary, `leg_replay.py`, `digest_trace.py` |
Conflicts in BENCH_RUNBOOK.md and RS13_VECTOR_LEG.md resolved by keeping
both sides: the 2026-10-10 amendment notes and carrier-range detail from
this branch, the bench.env / serial / HostIp placeholders and legs/
pointer from the bench kit. base_station on the merged tree: 1598
passed, 2 skipped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@dorkmo
dorkmo merged commit cf71a58 into main Oct 11, 2026
17 checks passed
@dorkmo
dorkmo deleted the rs13-a16-fix-2026-10-10 branch October 11, 2026 00:58
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.

2 participants