{
  "summary": "Root-cause finding: the hosted failure is best classified as a two-layer defect exposed by a valid self-reply loop. The source defect is an spt-core public-wire-contract violation if the inferred bytes are confirmed: EVENT bodies are required to entity-escape `<` and `>`, so the assistant text ``No inbound `<EVENT>` containing the message…`` should have reached `api listen` as `No inbound `&lt;EVENT&gt;`…`, never with a raw inner `<EVENT>`. The adapter then turns that upstream violation into a fatal endpoint outage because both released v0.2.1 and current/tagged v0.3.3 classify every `<EVENT` substring before the outer close as a nested frame, without checking whether it is a syntactically valid opening EVENT. The screenshot is therefore not explained by `decodeBody`: nesting is checked against raw stdout before body decoding. Exact stdout was not captured, so a truly truncated frame followed by another frame cannot be ruled out from the error alone; however, the screenshot’s earlier assistant text contains the exact delimiter-shaped literal needed to reproduce the error, and the self-reply lifecycle deterministically routes that assistant text back through the same listener.\n\nLikely raw frame, with unknown attributes explicitly left unknown: `<EVENT type=\"msg\" from=\"hertz\"[ possible event_id/other attrs]>SENT confirms delivery to the live listener. No inbound \\`<EVENT>\\` containing the message has surfaced in this turn yet.</EVENT>\\n`. The error itself proves only the structural subset `<EVENT …>…<EVENT…</EVENT>`: `drainEvents` found a second raw `<EVENT` before the first raw `</EVENT>`. The screenshot supplies the likely literal candidate; it does not prove the complete byte string or attributes.\n\nDeterministic current-v0.3.3 adapter-level reproduction: `const raw = '<EVENT type=\"msg\" from=\"hertz\">No inbound <EVENT> containing the message.</EVENT>'; const result = drainEvents(raw); assert.equal(result.error.code, 'SPT_PROTOCOL_ERROR'); assert.match(result.error.message, /nested EVENT/); assert.deepEqual(result.events, []); assert.equal(result.rest, raw);`. Static evaluation is exact: `close` points at the outer `</EVENT>`, `nested` points at the body’s `<EVENT>`, and lines 73–79 return the protocol error before `decodeBody`. The public v0.2.1 tag and public v0.3.3 tag contain the same parser, so the prerelease still fails. A lifecycle reproduction in `tests/omp-extension.mjs` can emit that same `raw` through `harness.children[0].stdout.emit('data', raw)`; today it follows the existing `failProtocol` assertions (one shutdown, no submitted message, listener killed, session ended). After the fix, it should submit `<msg from=\"hertz\"/>`, preserve the envelope in context, and not shut down.\n\nSmallest safe adapter defense: scan every `<EVENT` candidate between the outer opening tag and its close, find that candidate’s complete opening tag, and classify it as nested corruption only when `parseEventTag(candidateOpeningTag)` succeeds. The literal `<EVENT>` fails `parseEventTag` with missing `type`, so it remains opaque body text. The existing truncated-frame input `<EVENT type=\"msg\" from=\"a\">truncated<EVENT type=\"msg\" from=\"b\">valid</EVENT>` still fails closed because the inner opening tag is a fully valid EVENT. The scan must continue after malformed candidates rather than checking only the first; otherwise `<EVENT> harmless <EVENT type=\"msg\" from=\"b\">…` could hide a later valid nested frame. Keep strict top-level opening-tag validation, frame/buffer limits, and the existing fail-closed lifecycle unchanged. Do not simply remove the nested check or split at the first close: that would merge a truncated first delivery with a later valid delivery, potentially consume the second sender’s frame as the first sender’s body, and violate custody/reply correlation. This compatibility rule deliberately continues rejecting an unescaped body that literally contains a fully valid-looking `<EVENT type=\"…\" …>`; that byte sequence is indistinguishable from a truncated-frame boundary without upstream length framing, so failing closed is the safe tradeoff.\n\nRecommended focused tests: (1) `drainEvents` accepts an outer message containing literal malformed `<EVENT>` and yields the literal body; (2) it also tolerates `<EVENTUAL`/malformed delimiter-shaped prose if desired; (3) it scans past a malformed `<EVENT>` and still rejects a later valid nested EVENT; (4) retain the current truncated-A/valid-B parser and lifecycle fail-closed tests unchanged; (5) add the full listener lifecycle acceptance case for the screenshot input; (6) assert the already-contract-correct `&lt;EVENT&gt;` frame still decodes to `<EVENT>`; (7) add `decodeBody('a<br/>b<br>c') === 'a\\nb\\nc'` and accept both newline spellings. The current published-doc capture reportedly spells the newline token `<br/>`, while this repo’s historical public-contract evidence and SaberMage’s public `spt-shell-notify` reference adapter spell it `<br>`; accepting both is the narrow compatibility behavior. This slash discrepancy does not affect the RCA because all sources agree that raw body `<`/`>` must be escaped.\n\nThe later Doyle message is not evidence that `drainEvents` decoded a non-empty body to empty. The visible OMP prompt is intentionally always only `<msg from=\"doyle\"/>`; the body stays in the full envelope injected by the `context` hook, and `event.body` is not used for submission. A code-supported race explains the screenshot ordering: a Doyle event can be parsed, made `current`, and have its bodyless stub submitted; a subsequent corrupt chunk calls `failClosed`; teardown’s `failPending` clears `current`; when OMP subsequently requests context for the already-submitted stub, there is no current item from which to append the envelope. The model then sees only the stub and reasonably reports an empty body. This is the likely secondary symptom of fail-close timing, not a second body-decoder loss. A genuinely empty Doyle payload or missed context cannot be excluded from the screenshot alone; raw listener capture and hook timing would distinguish them. Fixing the false protocol-corruption trigger prevents this race in the reported path; no separate change should infer or invent a missing body.",
  "files": [
    {
      "path": "C:/Users/decid/Downloads/parsecd_1pVv5t6lwP.png",
      "description": "Screenshot evidence: a self-message/reply loop, earlier assistant prose containing literal `<EVENT>`, fatal nested-EVENT error, then a Doyle sender-only stub perceived as empty."
    },
    {
      "path": "adapter/strings/omp-spt.mjs",
      "description": "Current v0.3.3 parser and lifecycle. `drainEvents` lines 46–110 performs unconditional lexical nested detection before decode; `decodeBody` accepts `<br>` only; `settleItem` sends completed assistant text through `spt send`; listener stdout feeds `drainEvents`; `failPending` clears current custody; the context hook injects the envelope only while current still exists."
    },
    {
      "path": "tests/omp-extension.mjs",
      "description": "Current tests cover escaped bodies, chunk completion, valid nested/truncated fail-closed behavior, malformed top-level attributes, buffer limits, listener fatal teardown, and reply custody. They do not cover a delimiter-shaped but invalid literal such as body `<EVENT>` or documented `<br/>`."
    },
    {
      "path": "https://raw.githubusercontent.com/BigscreenVR/omp-spt/v0.2.1/adapter/strings/omp-spt.mjs",
      "description": "Published v0.2.1 source confirms the same unconditional nested-substring parser and self-reply path (`completeTurn` → `extractReply` → `settleItem` → `spt send`)."
    },
    {
      "path": "https://raw.githubusercontent.com/BigscreenVR/omp-spt/v0.3.3/adapter/strings/omp-spt.mjs",
      "description": "Published/tagged v0.3.3 source independently confirms the current prerelease still has the identical failing nested check and `<br>`-only decoder."
    },
    {
      "path": "docs/adr/0002-hook-wiring-hand-written-hooks-json-shells-spt-api.md",
      "description": "Repo public-contract record: whole EVENT envelopes, self-delimiting framing, entity-unescape order, sender preservation, and historical `<br>` spelling."
    },
    {
      "path": "docs/SPT-CORE-FINDINGS.md",
      "description": "Byte-capture record from the published spt surface: bodies encoded as `&lt;`, `&gt;`, `&quot;`, `&amp;`, with amp-last decode; confirms literal `<EVENT>` should not appear raw in a conforming body."
    },
    {
      "path": "https://raw.githubusercontent.com/SaberMage/spt-shell-notify/main/src/main.rs",
      "description": "Public independent reference adapter speaking only the documented spt API/wire contract; explicitly says EVENT bodies are HTML-escaped and decodes `<br>` first, entities next, `&amp;` last."
    },
    {
      "path": "docs/adr/0010-native-delivery-self-heals-or-closes.md",
      "description": "Defines the sender-only OMP stub plus full-envelope context contract and correlated reply custody, explaining both how self-replies loop and why the visible stub never contains body text."
    }
  ],
  "architecture": "Observed/inferred flow: `api listen stdout bytes` → `listenerBuffer` → `drainEvents(raw)` → accepted queue/current → `sendUserMessage(<msg from=\"sender\"/>)` + context-time full-envelope injection → assistant completion → `settleItem(payload)` → child `spt send sender --from endpoint` with the assistant reply on stdin. For a self-sender, spt routes that outcome back to the same `api listen`, closing the feedback loop. Contract boundary: spt-core owns serialization/entity escaping between `spt send` stdin and `api listen` stdout; omp-spt owns strict-but-compatible parsing, custody, OMP context injection, and fail-closed recovery. Classification: (1) upstream public-contract violation is the likely source if raw literal `<EVENT>` is confirmed; (2) adapter parser bug is the overbroad lexical nested check that unnecessarily fatalizes malformed delimiter-shaped prose; (3) self-message is a valid exposure mechanism, not itself a failure; (4) the later bodyless turn is likely a teardown/context race after the fatal parser decision, while a truly empty test input remains possible without byte/timing logs. Public docs: https://sabermage.github.io/spt-releases/ and https://sabermage.github.io/spt-releases/llms-full.txt (current capture cited by parent); public reference implementation: https://github.com/SaberMage/spt-shell-notify."
}