---
status: resolved
trigger: "Echo commune logs show DOUBLE LAYER of EVENT tags. Outer envelope `<EVENT type=\"msg\" from=\"\">` wraps an HTML-escaped inner `<EVENT type=\"echo_commune\">` with `<br>` line-break tokens. Should be only ONE layer."
created: 2026-05-14T00:00:00Z
updated: 2026-05-14T00:00:00Z
---

## Current Focus

hypothesis: CONFIRMED. Phase 29 D13 introduced a typed `<EVENT type="echo_commune" ...>...</EVENT>` envelope built by `compose_echo_commune_payload` (src/owl/echo_commune.rs:59-72). The composed envelope is delivered via `send::deliver_body_anonymous(psyche_id, payload)` (echo_commune.rs:520) and `send::deliver_body(self_id, from, payload)` (echo_commune.rs:530). On the receive side, the poll listener's `emit_event_line` (src/owl/poll.rs:618-635) **unconditionally** wraps every delivered body in `<EVENT type="msg" from="...">{event_body_escape(body)}</EVENT>`. The inner `<` `>` `"` get HTML-escaped to `&lt;` `&gt;` `&quot;`, and the inner envelope's literal `<br>` tokens become `&lt;br&gt;`. Result: nested envelopes with the inner one double-escaped. Pre-Phase-29 echo-commune payloads were plain `[COMMUNE]...[/COMMUNE]` text — wrapping them produced ONE visible envelope. Post-D13 the payload already IS an envelope, so the unconditional wrap creates the observed nesting.
test: read src/owl/echo_commune.rs:59-72 (compose), echo_commune.rs:494-535 (dispatch closures), src/owl/send.rs:66-97 (deliver_message — does NOT wrap), src/common/protocol.rs:21-29 (write_message — pure transport, no wrap), src/owl/poll.rs:618-635 (emit_event_line — UNCONDITIONALLY wraps). Cross-checked plugin/spt/skills/listen/SKILL.md:129-136 — receivers expect `<EVENT type="echo_commune">` as a first-class on-wire envelope shape (NOT nested inside type="msg"). git log confirms `compose_echo_commune_payload` introduced by b438306 "feat(29-01): replace legacy ECHO_COMMUNE prose with EVENT envelope" — the bug shipped with Phase 29 v1.10.0.
expecting: fix in `emit_event_line` to detect "body is already a typed `<EVENT ...>...</EVENT>` envelope" and emit it raw instead of wrapping. The existing `body_skips_event_wrap` predicate (poll.rs:607-609) is the established pattern for the parallel PULSE_TRIGGER skip. Extend it (or add a sibling) to recognize the typed-envelope shape. This is a one-site fix at emit_event_line; `compose_echo_commune_payload` and its callers stay unchanged.
next_action: RESOLVED — see Resolution section below.

## Symptoms

expected: psyche wrapper resume frame shows a single `<EVENT type="echo_commune" ...>...</EVENT>` payload, with literal XML (no HTML escaping) and literal newlines
actual: outer `<EVENT type="msg" from="">` envelope wraps an inner `<EVENT type="echo_commune"...>` where `<` is `&lt;`, `>` is `&gt;`, `"` is `&quot;`, and newlines are `<br>` tokens; literal `<br>` inside the inner envelope becomes `&lt;br&gt;`
errors: none — no wire-format integrity assertion catches a double-wrap; tests for compose_echo_commune_payload verify the inner shape in isolation but not the on-wire end-to-end shape after `emit_event_line`
reproduction: psyche wrapper log line `[03:54:15] [MSG] from="" type=&lt;EVENT type=` followed by the 939-byte resume frame; see user-provided screenshot. Phase 29 v1.10.0 just shipped with dual-dispatch (Psyche bundle + Self-inbox EVENT).
started: Phase 29 commit b438306 (feat(29-01): replace legacy ECHO_COMMUNE prose with EVENT envelope) — shipped in v1.10.0.

## Eliminated

- hypothesis: H1 — the bug is in the Phase 29 dual-dispatch Self-inbox path specifically (forward_to_self=true)
  evidence: screenshot shows OUTER envelope `from=""` (anonymous). The Self-inbox path uses `send::deliver_body(self_id, from, payload)` where `from = "doyle-psyche"` — non-empty. Empty `from` means the visible double-wrap is on the **Psyche-bundle path** (`deliver_body_anonymous`), which is the legacy delivery target. The Self-inbox path also suffers the double-wrap (same `emit_event_line` codepath) — both paths regressed together at b438306. Phase 29 dual-dispatch did not introduce the bug; it shipped alongside it. The visible artifact in the screenshot is the Psyche-bundle leg.
  timestamp: 2026-05-14

- hypothesis: H2 — `compose_echo_commune_payload` is wrong (over-escapes the body before delivery)
  evidence: `event_body_escape` (poll.rs:516-522) is the correct escaper for content embedded **inside** an EVENT envelope per the spt:listen wire-format spec. The inner envelope IS correctly formed — `<EVENT type="echo_commune" from="doyle-psyche" timestamp="..." note="...">{body-with-br}</EVENT>` is exactly what the wire format calls for. The bug is that the outer transport then wraps this CORRECTLY-formed envelope a second time.
  timestamp: 2026-05-14

## Evidence

- timestamp: 2026-05-14
  checked: user-supplied psyche wrapper log excerpt
  found: outer envelope `<EVENT type="msg" from="">` (empty `from` → anonymous → `deliver_body_anonymous` path), inner `<EVENT type="echo_commune" from="doyle-psyche" timestamp=... note="...">...</EVENT>` fully HTML-escaped (`&lt;` `&gt;` `&quot;`) including literal `<br>` tokens becoming `&lt;br&gt;`
  implication: the body of the outer message is the HTML-encoded form of the inner envelope, including double-escaped `<br>` literals. Confirms the inner was composed CORRECTLY, then the outer transport HTML-escaped it AGAIN — the signature of a double `event_body_escape` application.

- timestamp: 2026-05-14
  checked: src/owl/echo_commune.rs:59-72 — `compose_echo_commune_payload`
  found: produces `<EVENT type="echo_commune" from="{escaped}" timestamp="{escaped}" note="{escaped}">{body_escaped_with_br}</EVENT>` where body uses `event_body_escape` (which converts `\n` → `<br>`).
  implication: this is the correct on-wire envelope shape per spt:listen SKILL.md:129-136. NOT the bug.

- timestamp: 2026-05-14
  checked: src/owl/echo_commune.rs:494-535 — dispatch loop in `run_echo_commune`
  found: `dispatch_commune_markers` invokes closures around `send::deliver_body_anonymous(psyche_id, &payload)` (Psyche bundle, line 520) and `send::deliver_body(target, &from, &body)` (Self inbox, line 530). The full composed envelope is passed as `body` to both.
  implication: both delivery legs send the typed envelope INTO `deliver_message`, which is a pure transport — no wrap there. The wrap happens downstream at the receiver's `emit_event_line`.

- timestamp: 2026-05-14
  checked: src/owl/send.rs:66-97 — `deliver_message`
  found: TCP path calls `protocol::write_message(stream, from, body)`. Spool fallback calls `spool::spool_message(target, from, body, &owlery)`. Neither adds an EVENT envelope; both are pure `(from, body)` transports.
  implication: sender side is innocent. The EVENT wrap is purely a receive-side concern.

- timestamp: 2026-05-14
  checked: src/common/protocol.rs:10-29 — `format_message` / `write_message`
  found: TCP wire format is `__REPLY_TO__:{from}\n{body}\n` (with `__REPLY_TO__:` line omitted when from is empty). No EVENT-envelope construction in the protocol layer.
  implication: confirms the wire format between sender owl.exe and receiver poll listener is raw body, not EVENT-wrapped. Wrap is added by the receiver's stdout emitter.

- timestamp: 2026-05-14
  checked: src/owl/poll.rs:618-635 — `emit_event_line` (the Psyche poll listener's stdout-write site for delivered messages)
  found: unconditionally calls `println!("<EVENT type=\"msg\" from=\"{}\">{}</EVENT>", event_attr_escape(from), event_body_escape(body))`. No detection of an already-formed inner envelope in the body.
  implication: this is the bug site. ANY pre-composed `<EVENT type="...">...</EVENT>` payload arriving at this emit point gets re-wrapped. Today only `compose_echo_commune_payload` produces such pre-composed bodies; in the future any other typed-envelope sender (e.g., a hypothetical `<EVENT type="signoff">` produced sender-side) would hit the same regression.

- timestamp: 2026-05-14
  checked: src/owl/poll.rs:591-609 — `body_skips_event_wrap` predicate (PULSE_TRIGGER short-circuit pattern)
  found: existing pattern for "this body must NOT be EVENT-wrapped — emit raw". Currently matches only `PULSE_TRIGGER` prefix. The actual short-circuit happens at poll.rs:252-260 BEFORE reaching `emit_event_line`; the predicate is documentation surface plus regression-test hook.
  implication: extending this pattern (or adding a sibling) to recognize pre-formed `<EVENT ` envelopes is the established codebase idiom. The fix should slot cleanly into `emit_event_line` (or be a pre-check ahead of it) so all delivered messages flow through the same gate.

- timestamp: 2026-05-14
  checked: plugin/spt/skills/listen/SKILL.md:129-136
  found: `<EVENT type="echo_commune" from="<self-id>-psyche" timestamp="<ISO-8601>" note="<descriptor>">body</EVENT>` is documented as a FIRST-CLASS on-wire envelope shape, parsed identically to `type="msg"` and `type="alarm"`. No mention of being embedded inside `type="msg"`.
  implication: the wire-format contract with skill-side parsers (and the live-agent Self side) EXPECTS `echo_commune` to arrive raw, not nested. Confirms the design intent matches the proposed fix.

- timestamp: 2026-05-14
  checked: git log src/owl/echo_commune.rs — origin of `compose_echo_commune_payload`
  found: introduced by commit `c2437d1 feat(29-01): add compose_echo_commune_payload helper; promote event escapers to pub(crate)` and wired into the dispatch loop by `b438306 feat(29-01): replace legacy ECHO_COMMUNE prose with EVENT envelope; extract fire_echo_commune_inner`.
  implication: pre-Phase-29 echo-commune bodies were plain `[COMMUNE]...[/COMMUNE]` text. Re-wrapping that produced one visible envelope: `<EVENT type="msg" from="">[COMMUNE]...[/COMMUNE]</EVENT>` — no nesting visible, mild over-escape. Phase 29 v1.10.0 shipped the typed-envelope refactor without updating `emit_event_line` in lockstep, surfacing the double-wrap.

## Resolution

root_cause: `emit_event_line` (src/owl/poll.rs:618-635) unconditionally wrapped every delivered body in `<EVENT type="msg" from="...">...</EVENT>` and applied `event_body_escape` to the body. Phase 29 D13 introduced `compose_echo_commune_payload` (src/owl/echo_commune.rs:59-72) which delivers a body that is ITSELF a fully-formed typed `<EVENT type="echo_commune" ...>...</EVENT>` envelope. The two changes shipped together in v1.10.0 (commits c2437d1 + b438306) without coordination, so every typed-envelope delivery hit the unconditional wrap and produced nested envelopes with the inner one double-HTML-escaped (`<` → `&lt;`, `>` → `&gt;`, `"` → `&quot;`, `<br>` → `&lt;br&gt;`). The spt:listen skill (plugin/spt/skills/listen/SKILL.md:129-136) documents `<EVENT type="echo_commune">` as a first-class on-wire envelope — confirming receivers expect it raw, not nested.

fix: Added a sibling to the existing `body_skips_event_wrap` short-circuit pattern: `body_is_typed_event_envelope` (src/owl/poll.rs:611-635) detects when `body` is already a typed `<EVENT type="..." ...>...</EVENT>` envelope via a cheap byte-slice check (`body.starts_with("<EVENT ") && body.ends_with("</EVENT>")`). `emit_event_line` checks this predicate first and, when true, emits the body verbatim via `println!("{}", body)` instead of double-wrapping. This is a single-site receive-side fix; `compose_echo_commune_payload` and its callers stay unchanged. Regression test `typed_event_envelope_bodies_emit_verbatim` (src/owl/poll.rs tests module) pins the contract — verifies the echo_commune envelope shape (and the minimal `<EVENT type="echo_commune" from="x" timestamp="t">hello</EVENT>` shape from this debug doc) is detected, plus negative cases (plain text, partial envelopes, PULSE_TRIGGER) to confirm orthogonality with the existing `body_skips_event_wrap` predicate. Backward-compatible: no production caller of `deliver_body_anonymous`/`deliver_body` other than echo-commune dispatch currently sends pre-formed envelopes.

commit: see git log on `main` — `fix(owl): emit_event_line passthrough for pre-formed EVENT envelopes`. cargo test --lib post-fix: 319 passed (318 baseline + 1 new test), 0 failed (single-threaded run; parallel failures observed on Windows are pre-existing SPT_HOME contention flakiness unrelated to this fix). cargo build --release clean. Parallel debug session `fire-ec-classifier-drops-non-control` (src/live/wrapper/mod.rs) retains its own scope and is NOT touched by this commit.
