# #113 fork 3 — BOUNDARY DELTA measurement (todlando, 2026-08-30)

Ordered by doyle before any build: *does spt-core catch the echo delta from the last echo to the
boundary edge, the way legacy did through its `--source` re-emit path?*

**Answer: NO — and on three independent arms, any one of which is sufficient.** Measured by
reading the code at `main` tip (`aa9ac15a`), file:line below. No build performed.

---

## 1. `api boundary` fires no echo and arms no gate

`cmd_boundary` (`crates/spt/src/api/reporting.rs:43`) does exactly this, in order:

| step | site |
|---|---|
| psyche-custody squat guard | `reporting.rs:57` |
| sid rotation under the per-perch lock | `reporting.rs:81` (`info::mutate_info`) |
| engine-room empowerment CARRY | `reporting.rs:117` |
| ready re-stamp + status online | `reporting.rs:169-170` |
| session-ledger append (REQ-TERM-6) | `reporting.rs:179` |
| ONE IO boundary event, gated on `ledgered == Ok(true)` | `reporting.rs:213` |
| project-index invalidation nudge | `reporting.rs:219` |
| `resurface_notifs` | `reporting.rs:226` |

No echo. No `arm_transition_echo`. No gate write.

The production `fire_echo` call sites are **exactly three**, and a boundary is none of them:

- `lifecycle.rs:1061` — trigger `"pulse"` (the echo-gate fire)
- `lifecycle.rs:1237` — trigger `"rest-transition"`
- `lifecycle.rs:1913` — trigger `"signoff"`

The soft `api session-end` the adapter fires for the DEPARTING session at a `/clear` does not
fire one either (no echo/gate site in `cmd_session_end`).

## 2. Even if an echo fired after a boundary, it could not see the pre-boundary work

`fire_echo` feeds `self.history(session_id)` (`lifecycle.rs:1185`), which resolves `[history]`
through `base_keys(session_id)` — **the current session id**. `[history]`'s locate pattern
resolves a *single* `{session_id}` file; ADR-0019 §Context problem 3 states this property in as
many words ("`[history]` `locate_template` resolves a *single* `{session_id}` file").

The boundary has already rotated that id before anything downstream could fire. So the echo's
input after a boundary is the NEW session's log — empty at that instant — and the departing
session's tail is unreachable through this path.

**Session-spanning exists in this codebase, but it belongs to the DIGEST, not to the echo.** The
ledger + last-K enumeration (ADR-0019 "Thread-spanning", REQ-TERM-6) is what bridges a boundary,
and the echo does not read it.

## 3. There is no last-echo watermark at all

`last_echo` / `echo_watermark` / `since_last_echo`: **zero hits across `crates/`.** Each echo
feeds the whole current-session history to the summarizer; "the delta since the last echo" is
not a quantity spt-core holds anywhere. Legacy's boundary re-emit (`--source clear|compact`,
selecting the RE-EMIT prompt) has no counterpart here.

---

## ⚠ CORRECTION to my own W0 report — fork 1's premise is FALSE

W0 (`NOW-SIGNAL-23-W0-REPORT.md` §5) states, as measured:

> **spt-core's current cadence, measured:** the gate is armed by TRANSITION EDGES only —
> `resting.rs:365 arm_transition_echo`, fed by the attach arm's detach, the registry arm's
> attention shift, and `spt suspend`

**That is incomplete, and the missing arm inverts the conclusion drawn from it.** There is a
SECOND armer, and it is the work-driven one:

- `crates/spt/src/api/delivery.rs:439` — `cmd_state`'s `idle` arm writes `ECHO_GATE_SENTINEL`
  **unconditionally** (not conditioned on `was_idle`, so not edge-gated), suppressed only by an
  explicit `--no-gate`.
- This is **published contract**, not an implementation detail:
  `docs-site/src/harness-contract/api.md:296` — "Reporting `idle` also arms the echo gate
  (below) unless `--no-gate`."
- The endpoint's pulse loop consumes it read-and-clear at `pulse_period` (5s default) and fires
  (`lifecycle.rs:1043` → `1061`).
- Nothing on that path applies a time window: `fire_echo`'s only early exits are "no commune
  dir" and "no `[session.echo_commune]` role" (`lifecycle.rs:1280-1288`).

`api state idle` is the adapter's **turn-end** report. So a working agent arms the echo gate at
**every turn end**, and the next 5s tick fires the commune.

**Consequence — the coverage gap named in W0 fork 1 does not exist, and the real defect is its
opposite.** W0 said "an agent that works for hours without a transition edge never echoes in
spt-core, where legacy would have echoed every ~15 minutes." Measured: such an agent echoes at
*every turn end*, i.e. far MORE often than legacy. spt-core is not missing the work-driven
trigger — **it is missing the age gate**.

**Measured vs not measured, stated separately:** the arming is core's contract and is measured
here. Whether a given adapter passes `--no-gate` at turn end is adapter-side (`claude-spt` is
another repo) and is **NOT measured** — so the field firing frequency is bounded above by "once
per turn", not asserted to equal it.

### What this does to fork 1's ruled build shape

The ruling ("ADD the legacy work-driven age-gated echo: turn-end sentinel writer + 15-min age
fire") was decided on the W0 premise. Half of it — the turn-end sentinel writer — **already
ships**. What is genuinely absent is the rest: the 15-minute age gate, `SkipSentinelFresh
{remaining}` with a short next pulse near the crossing, birthtime-with-mtime-fallback age with
the degradation declared, and fail-open on metadata failure/clock skew.

That turns "add an arm beside the edge arm" into "**put an age gate over the existing idle
arm**", and it re-opens a question the ruling answered under the old premise: the ruling says
the age gate "governs the work-driven arm only" and the edge arm is kept ungated — which is
still coherent, but the two arms are no longer *trigger-distinct*, they are the same sentinel
file written by two callers. Distinguishing them at fire time needs a carrier that does not
exist today (the gate is a zero-byte sentinel; `arm_transition_echo` and the idle arm write the
identical file).

**Stop-refer, not an edit.** Fork 1 goes back to doyle with this correction before any build.
Forks 2 and 4 are unaffected: fork 2's ruling is about surfacing, fork 4's about
configurability, and neither rests on the trigger premise.
