# EVENT-PART reassembly — JIT plan (root fix for F-033 listener deafness)

> **STATUS: SHIPPED / SUPERSEDED — 2026-07-25.** Delivered by `e99dafa`
> *"fix: reassemble oversized EVENT-PART listener frames (0.3.25)"*. The OPEN block below
> (doyle emit-format confirm) was resolved before ship; the hazard is now recorded as
> `docs/KNOWN-HAZARDS.md` **§8** with a passing regression test. Retained as the design record
> — do not re-plan from it. Unchecked task boxes below are historical, not outstanding work.
> Live successor: `RECV-SILENCE-WATCHDOG-PLAN.md` (defense-in-depth, still blocked).

Requirement: `REQ-HAZARD-LISTENER-EVENT-PART-REASSEMBLY` (new, root fix).
Companion defense-in-depth: `REQ-HAZARD-LISTENER-EMIT-SILENCE` (RECV-silence watchdog, separate plan).
Origin: F-033 live-reproduced on `hertz` 2026-07-24; root proven offline against the real `drainEvents`.

## The bug (proven)

The published listener-stream wire contract (docs `messaging/overview.md#the-event-wire-contract`,
localhost:5474 lines 208-209, 697-699): *"an oversized line splits into `<EVENT-PART seq="K/M"
id="…">` chunks the receiver reassembles."* Core emits these via `render_event_lines` →
`chunk_if_oversized` (startup.rs:918, `EVENT_LINE_THRESHOLD`) on the **live listener stream only**
(the hook-drain / `api poll` paths use the never-chunked `render_event_whole`, F-002 self-delimiting).

`omp-spt`'s `drainEvents` **never implements EVENT-PART reassembly** (grep = 0). Failure chain
(traced in `strings/omp-spt.mjs` `drainEvents`/`findEventClose`/`parseEventTag`):
1. `indexOf("<EVENT")` matches `<EVENT-PART` (shared prefix).
2. A part closes with `</EVENT-PART>`; `indexOf("</EVENT>")` never matches it.
3. `findEventClose` counts each subsequent real `<EVENT>…</EVENT>` as a **nested child**
   (depth++/--), so depth never returns to 0 → `close < 0` → frame perpetually "incomplete."
4. `drainEvents` returns **0 events**, leaves everything in `listenerBuffer`, **no error, no death**.
   Buffer grows monotonically until `listenerBufferLimit` → a much-delayed `LISTENER_DIED`.
5. Only `type === "msg"` is pushed anyway, so the triggering `echo_commune` never RECVs regardless.

Signature: **first delivery clean → deaf after the first oversized listener payload.** The
`echo_commune` (large session summary) is simply the first payload big enough to chunk — the
echo_mirror correlate. `claude-spt` reassembles EVENT-PART → does not repro (the discriminator).

Offline repro (imported real `drainEvents`): whole msg → 1 event; EVENT-PART chunk → 0 events;
EVENT-PART + subsequent msgs → 0 events, all swallowed, buffer grows. Becomes the regression test.

## Invariant (target)

An oversized listener delivery arriving as an ordered `<EVENT-PART seq="K/M" id="…">` set is
reassembled into its original whole `<EVENT>` line and delivered exactly once, in stream order,
with its body decoded once — identical to a never-chunked delivery. A partial set (missing parts,
or split mid-part across data chunks) is carried forward, never dropped and never wedging unrelated
frames. `<EVENT-PART` is never mis-parsed as `<EVENT`.

## Design

- **Prefix disambiguation (harden first, independently valuable):** after locating `<EVENT` at
  `start`, classify by the next char — `>` or whitespace ⇒ whole `<EVENT>`; `-` (i.e. `<EVENT-PART`)
  ⇒ a part. Never let a part satisfy the whole-event scan. (This alone converts silent wedge → a
  clean "incomplete/part" path even before reassembly logic.)
- **Part frame parse:** `<EVENT-PART seq="K/M" id="ID">FRAGMENT</EVENT-PART>`; extract `seq`→(K,M),
  `id`, and the raw `FRAGMENT` slice (verbatim, NOT decoded).
- **Reassembly (concat-then-parse-then-decode):** accumulate FRAGMENTs keyed by `id`; when all `M`
  present, `concat(FRAGMENT_1..FRAGMENT_M)` === the original whole `<EVENT>…</EVENT>` line → run it
  through the existing whole-event parse (which decodes the body once). Never decode per-fragment
  (split points may fall inside `&amp;`/`<br>`), so concat MUST precede decode.
- **State:** `drainEvents` is pure/stateless today. Prefer keeping reassembly WITHIN a single call
  when the full set is buffered (parts for one line arrive consecutively — pending doyle's ordering
  confirm); carry an incomplete set forward via `rest` exactly like an incomplete whole frame, so no
  cross-call state object is needed. IF doyle confirms parts of different ids can interleave, add an
  id-keyed pending map threaded through the call site instead.
- **Bounds:** cap reassembled size by `maxFrameChars` (or a reassembly-specific ceiling) so a
  malformed/never-completing set still trips a loud limit instead of unbounded growth.

## OPEN — pending doyle (emit-format confirm; blocks final impl)

Sent to doyle: (1) exact frame + closing tag + one-part-per-line; (2) seq 1-indexed K/M; (3) id
charset; (4) FRAGMENT = raw byte-substring of the rendered+escaped whole line s.t. concat === verbatim
original, split points entity/`<br>`-safe or not; (5) `EVENT_LINE_THRESHOLD` value (for a real
oversized regression input); (6) contiguous-in-seq vs interleavable ids; (7) any per-fragment escaping.
Docs-gap finding filed: no literal EVENT-PART example / concatenation semantics in the public docs.

## Tasks

- [x] doc: this plan + KNOWN-HAZARDS entry (#8) + REQ in traceable-reqs.toml.
- [ ] Confirm emit format with doyle → freeze the parse/concat rules.
- [ ] impl: prefix disambiguation + EVENT-PART parse + reassembly in `drainEvents`,
      tag `[impl->REQ-HAZARD-LISTENER-EVENT-PART-REASSEMBLY]`.
- [ ] unit: promote the offline repro to a regression test — chunked oversized delivery reassembles
      and delivers; interleaved/subsequent msgs are NOT swallowed; partial set carried, not dropped;
      `<EVENT-PART` never mis-read as `<EVENT`. Tag `[unit->REQ-HAZARD-LISTENER-EVENT-PART-REASSEMBLY]`.
- [ ] Restore pristine adapter (drop instrumentation) before shipping; version bump; ship decision
      is the operator's (outward). hertz recovery (bounce to clear its wedged buffer) is operator's call.
- [ ] `traceable-reqs check` green; build green.

## Gate

Build + `traceable-reqs check` green; the new hazard has a passing regression test (hazard list =
conformance checklist). doyle gut-checks the reassembly logic against the emit format before ship.
