# W5 #346 TRUST-WARNING → NOW-SIGNAL — census + picks (todlando → doyle, 2026-09-27)

Branch `feat/351-w5-trustwarn` off 8cea2af4 (W4 head), worktree `.worktrees/351-w5`. No impl, no build, no pool claimed.
Sources: releases#346 body + doyle's 2026-09-25 shape comment; doyle's census `.spt/preserved/346/census-doyle.md` (probed at 5eea489e).
**Every cite below I re-read myself at 8cea2af4.** W4 moved wan.rs by 0 to +12 lines, and CONTEXT.md §trust warning moved +12 (it is now :1101-1112, not :1089-1096).
REQs: REQ-TRUST-WARNING-NOW-SIGNAL (`traceable-reqs.toml:8133`, `[]`), REQ-TRUST-WARNING-ENVELOPE (:4026), REQ-TRUST-WARNING (:4011), REQ-TRUST-WARNING-CADENCE (:4021).

## Census facts that shape the picks

**One producer, one edge (R).** The trust warning is decided only in `receive_wan` (`wan.rs:1425-1460`), through `owed_trust_warning` (:1110). Same-node sends never warn, which is ratified. There is no other caller of `warrants_trust_warning` in production (`access.rs:137`; its other hits are :309-320, the units). `cli.rs:6493/:6540` call `compose_trust_warning` only for `spt endpoint trust-warning show`, and that stays.

**Today's order (R):** decide → compose the body with the override (:1157) → render the envelope with the attr (`emit.rs:179`) OR take the fail-safe `warn_alongside` (:1231) → deliver the message → claim iff it `reached_the_agent()` (:1456). The cadence marker is `session_dir(<perch info.json session>)/trust-warned/{endpoint-|node-}<peer>` (`trustwarn.rs:289-362`).

**Doyle's census needs one correction on emit.rs.** `compose_line_warned_at` (`emit.rs:83`) is NOT retire-whole. It is the body of the plain `compose_line_at` (:67-68 calls it with `None`), and it carries the F14 typed-msg splice that attaches `mnemonics-json` to every ordinary send. The retirement is: drop the `warning` parameter from it and from `splice_typed_msg` (:145), delete the two `EVENT_ATTR_TRUST_WARNING` pushes (:124, :165), and delete `render_event_whole_warned_for` (:179). The `typed` return then has no consumer left; I will check that nothing else reads it before dropping it.

**The session keys agree while live (MEASURED, this box, just now).** My perch `info.json.session_id` = `7c736928-…`. That is my current CC session (post-/clear; the pre-clear one was `8870d774-…`), and `sessions/7c736928-…/now-signal/*.seen` is where this session's polls write. So the receipt-time key (bound session) and the render-time key (poll `--session`) are the SAME string for a live agent. They DIFFER in exactly one case: the away burst. Messages arrive while `info.json` still names the old session S1, and the agent returns as S2.

**F1 is not one site (R).** "One fix where all route" is false in two places:
- **HINTS never touches `SeenSet`.** `select_and_mark_hint` and `select_and_mark_shell_hint` (`nowsignal.rs:449, :509`) mark their own `hints-seen` store at select time. A `max_lines` cut of HINTS loses a hint exactly like F1, and a `SeenSet` fix does not reach it.
- **Keys and lines are not 1:1:**
  - ENDPOINT_MENTIONS takes up to 2 keys per line (state + `#desc`, :582/:598).
  - FILE_ACCESS_HELPER takes a key and then may collapse the line into an EARLIER identical fetch line (:730-737, :753-760).
  - EDGE_TRANSITIONS seeds keys that have no line (:792, :809), and those must always flush (:1309-1311).
  - LAN_EXPOSED has no keys, and 1+N lines (:1072-1078).

  A post-hoc "unmark the keys of the cut lines" at :1312-1313 therefore has no key↔line map to work from.

## Picks

**P1 — the record store.** `trust-warnings.jsonl` goes in the RECEIVING perch, beside `helper-fetch.jsonl`, which gives node-local scope per the F3 ruling. One JSON line per owed warning: `{msg_id, peer_kind, peer, receipt_session?, body, at_ms}`.
- `msg_id` is the envelope `msg-id` attr, read at receipt the same way the helper arm does (`wan.rs:1512`). When the attr is absent (a legacy or foreign sender), it falls back to `op:<op_id>`, so the entry always names what it concerns.
- The body is composed AT RECEIPT with the override, exactly as today (`compose_trust_warning`, :1158), so the reader stays dumb and the override semantics do not move.
- The append/trim/read-tail machinery comes from generalizing `helperline.rs:62-128` over (filename, row type), rather than being copied. That covers the torn-tail close and the bounded KEEP/READ.
- The write is best-effort, and a failed append is the loud `TRUST_WARNING_UNRECORDED` line. This replaces `TRUST_WARNING_UNDELIVERED` (:1200), which retires with the fail-safe. Delivery is untouched either way.

**P2 — receipt (daemon).** The steps, in order:
1. Keep `owed_trust_warning`'s decision as-is: the condition, the monic read, and the bound-session owed check.
2. If owed, append a record UNLESS an unrendered record for the same peer is already in the file. That is the burst suppression.
3. No claim at receipt.
4. The message is delivered with NO attr. The strip at :1404 stays, so an inbound forged attr is still stripped.

**P3 — render (now-signal), new category `TRUST_WARNINGS`,** appended to `POST_V1` (:123).
- **Line:** one line per PEER per poll: `<peer> — msg <msg_id>[ (+N more)]: <body as one line>`. It shows the newest msg-id and counts the rest, because the body is multi-line today and a now-signal line is single-line (`Block` doc :179). How to fit the ~665-byte, multi-line block into a one-line row is Q2.
- **Which records render:** a record renders iff:
  - its msg_id is not in this session's seen-set, AND
  - `warning_owed(poll_session, peer)`, AND
  - it has not been claimed under its own `receipt_session`.
- **On render:** `claim_warned(poll_session, peer)` AND `claim_warned(receipt_session, peer)`. The second claim is what stops a record rendered in S1 from resurfacing in every later session. It reuses the existing marker store, so no new "rendered" store is needed.
- **Covered records:** a record whose peer the poll session is already claimed for is marked seen silently (it is covered).

**P4 — F2, the LEAN, is REFINED rather than refuted.** Evidence: the away burst. Under the lean, "pending record for (peer, session)" is keyed on the receipt session S1. The agent reads in S2, where S1's marker is never written, so S2 must render S1-era records, and any S1-keyed filter on the reader drops them. That is the #63 case, the reason the cadence exists, and today's envelope carrier gets it right because the spooled message carries its attr into S2.

So the refinement is:
- Burst suppression keys on (peer, UNRENDERED), with no session in the key.
- The render-time claim keys on the POLL session (plus the receipt session, per P3).

Answers to doyle's asked units:
- **Burst → one entry.** YES: one record, one line.
- **An unrendered entry, next session?** It STILL SURFACES in the next session, because its receipt-session marker was never claimed. This is deliberate: an unpolled caution is never claimed (the REQ-…-NOW-SIGNAL text).

**P5 — F1 fix, at the source rather than post-hoc.**
- `NowSpec.max_lines` becomes a line budget handed to `SeenSet` (`SeenSet::load(session, cat).with_cap(spec.max_lines)`).
- `take_new` answers false and does NOT mark once the budget is spent, and each `true` answer spends one line.
- A new `take_new_detail` covers a key that decorates an already-counted line (ENDPOINT_MENTIONS `#desc`): it marks without spending.
- `seed` is unaffected, so EDGE still flushes.
- A FILE_ACCESS_HELPER collapse spends one line for nothing, which under-renders by at most the collapse count. That is safe, because a cut key stays unmarked.
- HINTS gets the same budget as a parameter: `gather_hints` stops calling `select_and_mark_*` when `out.len() == cap`.
- The post-hoc `spec.bound` stays only for the keyless LAN_EXPOSED. For every other category it becomes a no-op backstop.
- **Units:**
  - For each keyed category, a `max_lines` cut leaves the cut entry unseen, and it renders on the next poll.
  - One HINTS unit.
  - One TRUST_WARNINGS unit: a cut warning stays owed AND unclaimed.

**P6 — retire in this lane (per the ruling):**
- The fail-safe: `deliver_warning_alongside` (:1173), `warn_alongside` (:1231), `claim_warning_delivered` (:1219), `TRUST_WARNING_UNDELIVERED` (:1200), and the riding/claim block (:1432-1460).
- `TRUST_WARNING_AUTHOR` (`trustwarn.rs:67`). I will check that it has no other reader before removing it. Its only production reader today is the fail-safe, and the reserved-author idea goes with it.
- The emit.rs attr path, per the correction above.
- **KEEP:** `EVENT_ATTR_TRUST_WARNING` (`event.rs:251`), `RECEIVER_COMPOSED_ATTRS` (:322) + strip, and the strip's 7 tests plus `deliver.rs:282-289`.

**P7 — tests to retire/rewrite (at 8cea2af4):**
- wan.rs: `warn_via_fallback` helper (:1659), `warnings_spooled` (:1671), `a_burst_from_one_peer_surfaces_one_warning` (:1688, rewrite: one RECORD), the riding and typed-body cells (around :2414-2467), and `a_forged_inbound_trust_warning_attribute_is_inert` (:2487), which stays: the strip is kept.
- emit.rs: :366-402 and :719-803.

## Questions for doyle

- **Q1 — can a spec suppress TRUST_WARNINGS?** Today `NowSpec::allows` (:385) lets an adapter's `only` or `without` drop any category. Under the envelope carrier, no adapter config could drop the caution. Moving it into now-signal makes it spec-suppressible, which is a quiet loosening. **Pick: `allows` answers true for TRUST_WARNINGS whatever the spec says.** It is the ignored-is-failure attr in a new carrier. I did NOT verify what the claude-spt or omp-spt manifests declare today.
- **Q2 — the line shape.** The block is multi-line, roughly 665 B. **Pick:** the now-signal line carries the factual spine on one line (peer, msg-id, "no note held", how to classify), and the advisory/override text follows as the same entry's continuation, which `compose` would need to allow. The alternative is to one-line the whole block (`one_line`, :1113) at some cap, which would truncate operator override text. I lean spine+continuation, but it touches `compose`'s single-line contract (`Block` doc :179).
- **Q3 — REQ-TRUST-WARNING (:4011) also needs amendment by replacement,** not only ENVELOPE. Its text states "THE CARRIER IS THE DELIVERED MESSAGE'S OWN ENVELOPE", "delivered FIRST", and the reserved author. REQ-…-CADENCE (:4021) says the "claim only after delivery", which becomes render; its units (e) no-key-warns-every-message and "undelivered leaves unclaimed" carry over as "unrecorded warns loudly" and "unrendered stays owed". Pick: amend all three by replacement in the REQ commit, and retire ENVELOPE with its reason.
- **Q4 — adapters that never poll now-signal.** An adapter that renders EVENTs but never polls now-signal loses the caution entirely. Under the attr carrier it got it for free. This belongs in the adapter note to perri and emphasys, as "polling now-signal is now REQUIRED for the trust warning". Is it a gate item or only a note?

## Docs (cites at 8cea2af4)
- CONTEXT.md :1101-1112 (§trust warning). The :1105 envelope paragraph gets struck and marked superseded, and :1106 already carries the new carrier. Also the `_Avoid_` #170 items.
- docs-site messaging/overview.md :173, :191, :197, :217.
- networking/monics.md (the trust-warning section). knocking.md:329 links it, and the anchor must survive.
- shells/frames.md now-signal table, which needs a TRUST_WARNINGS row (doyle also noted LAN_EXPOSED is missing from it).
- harness-contract api.md adapter note.
- changelog.md :890/:1235/:1437 are history; leave them.

## Plan once picks are ruled
1. REQ commit: amend three by replacement, retire ENVELOPE, activate NOW-SIGNAL.
2. The store generalization plus the trust-warnings record.
3. Receipt rewire and retirement.
4. The category plus render/claim.
5. F1 budget.
6. Docs.
7. Battery: `--lib` plus an explicit `--test` per crate (the disk-fill rule), clippy, and treqs from the committed tree.

No two-host cell is needed, per F3: the record is node-local, so a single-node int covers it (receive_wan → record → now-signal render → claim).
