---
name: v0370-commune-drop-detect
description: "2026-09-06 SHIPPED v0.37.0 (#25) and then FIELD-FAILED it: ingest is 2.8-5.8s, not >15s, so the drop is gone before the next hook looks. Carries the I-BUILT-ON-AN-UNMEASURED-NUMBER lesson and the event-vs-state trigger rule."
metadata: 
  node_type: memory
  type: project
  originSessionId: df34fdab-4fe5-4743-9b09-62292e3ec385
  modified: 2026-09-06T07:04:59.661Z
---

**2026-09-06 — claude-spt v0.37.0 SHIPPED, `claude-spt-bs#25` DONE — AND THEN FAILED ITS FIELD TEST.**
Follow-up filed the same hour. Read the next section before reusing anything here.

## ⭐ THE LESSON: I BUILT ON THE ONE NUMBER I NEVER MEASURED

The operator said core's commune ingest takes ">15 seconds". Every design decision hung off it:
the io funnel was rejected as unreadable from a hook, a ResidentService was investigated for a day,
and the whole file-stat design exists *because* a drop was believed to sit on disk for 15+ seconds.
I even wrote the dependency down as the design's single point of failure, in the call site and in
the requirement title, phrased so it could be falsified. **I never spent the two minutes to falsify
it.**

MEASURED, three times, right after shipping: **2.8s / 3.8s / 5.8s.** The gap between consecutive
hooks includes the agent's own thinking time, so the drop is typically gone before the next hook
looks. Two real field drops (`cp` and `>`): no boundary, and `state/commune-seen/` stayed EMPTY —
no leg ever *examined* them. The binary is correct (a synthetic payload detects, logs and arms
exactly as designed); the window it needs does not exist.

**Naming a load-bearing assumption is not the same as testing it.** Writing "this holds only while
X" earns nothing if X is someone's estimate and the check is two minutes of work. The cheapest
measurement in the whole project decided its outcome, and I did it after the release instead of
before the design. Sibling of [[signature-match-is-not-root-cause]]: a plausible mechanism, adopted
without the one observation that could have refuted it.

**What it makes true instead:** after ingest, the `COMMUNE` io frame is the ONLY surviving evidence
— the file is gone. And `api io-events --session-id <sid>` is authenticated by the sid every hook
already carries, so a hook CAN read the funnel. The ">15s" figure is the only reason that looked
impossible. The operator's original instinct was right, for a sharper reason than either of us had.

**v0.37.0 is a strict improvement that stops short**, not a regression: the instrument-keyed
predicate is gone, the three legs and arms-once stamp are tested, and a drop written mid-work with a
tool call following inside ~3s does fire. The handoff case — commune as the turn's final act — does
not. Not reverted; v0.38.0 adds the funnel leg.

## The original defect (still correctly diagnosed)

Operator report: agents increasingly write their across-commune with Bash, and no boundary fired.

## The defect, and why it is a familiar shape

`is_commune_write` was `tool == "Write" && file_path.ends_with("<id>-commune.md")` — the trigger
inspected the **tool call's arguments**, so it was keyed on the *instrument*. A commune written with
a Bash heredoc, `cat >`, `Edit` or python fired nothing: the commune ingested, the context rebuilt,
and the session it was meant to end just kept running. Silent at both ends.

spt-core's model was always the opposite — a commune is a **file-drop**, "any harness that can write
a file can commune", `[session].commune_dir` being the declared seam. **The drop is the contract;
the tool is not.** Same family as [[list-vs-predicate-assertions]]: a rule keyed on the instances
that existed the day it was written stays green until the case nobody enumerated arrives.

## ⭐ THE RULE: trading an EVENT trigger for a STATE trigger re-fires it

A Write tool call is an *event* — observed once, and being-observed-once is a property of the event,
not of anything you wrote. A drop file is a *state*: it persists until core ingests it (>15s, many
tool calls). **Every state-shaped trigger re-fires unless something remembers.** Without a stamp the
detector arms on every hook for the whole window: a second wake-ARM self-send and two post-clear
sequences, which *blend* rather than fail loudly ([[boundary-rename-v0141]]).

`state/commune-seen/<eid>.stamp` records the **last EXAMINED** identity (`<mtime_ms>:<len>`), not the
last armed one — a marker-less commune is the common case and would otherwise be re-read (20-100KB)
on every tool call while never arming. Both halves are tested, including a **stat-not-read** test:
a detector that is correct but re-reads is a regression nothing downstream would ever notice.

## ⭐⭐ THE PLAN WAS WRONG ABOUT ITS OWN CLEANUP STEP

The JIT plan said "clear the stamp at `SessionStart(clear)`". That is a **clear loop**: post-clear,
an un-ingested drop plus a cleared stamp re-arms immediately. Caught while wiring it, not by a test.
The stamp now clears only on the drop's *absence*. A plan's cleanup step deserves the same "what
happens on the next tick" reading as its main path.

## ⭐⭐⭐ Two builders for one file

`write_commune_file` built the path with `PathBuf::join` while the detector built a string — so the
writer and the reader could disagree about a separator and the stamp would never match the drop the
adapter itself had just written. Collapsed to one builder. Also: the Recorder's `write_spill` was
modelled as a log entry that left **no readable file**, so the shortcut's own stamping could never be
exercised by any test. A mock that cannot show the artifact cannot fail when the code misreads it.

## The alternatives, rejected with reasons (they do not survive in the outcome)

- **io funnel `COMMUNE` kind** — the *right* predicate, and `api.md` names "wake-marker-class
  constructs over commune and IO events" as its motivating case. But it fires only when **ingest
  completes** (>15s, operator-measured), when no hook is running; it is authenticated; and its
  payload caps at 16KB with `truncated`/`digest_seq`, which our communes exceed.
- **A `[service]` ResidentService** — the only process alive between turns, but `$defs/Service` says
  it has "no perch, no identity, and no address" while `api io-events` needs `--session-id` or a
  capability `--token`. **Filed as `spt-bs-releases#275`** (hand-filed via `gh`: no alchemy instance
  here is bound to that repo, so the mint never ran — deployah typed it `addition`). Filed
  **NOT BLOCKING**, and said so in the body: filing a request you have already routed around, without
  saying so, misprices it for the next person.

**THE ACCEPTED RISK THAT WAS ALREADY REALITY:** the call-site comment says this holds only while
core's ingest is slower than a hook cycle, and guesses that only *sub-second* ingest would break it.
Both halves were wrong in the same direction — the true window is 2.8-5.8s, and a hook cycle is
routinely longer than that. The wording made a live failure sound like a remote hypothetical.

## Process notes worth keeping

- **deployah pointed me at `docs/MANIFEST.md` in the spt-core repo** as the authoritative `[service]`
  prose. Declined — never reading that tree is the whole constraint. His having to reach for an
  internal file to answer a builder's question *is* the measurement that justified his docs port
  (`spt-bs-releases#274`, v0.67.1). See [[node-local-book-gate]].
- **`manifest.schema.json` is a documentation surface**, not just a validator: it carried the entire
  `[service]` contract in prose while the published book had none of it.
- The **leak guard** caught a private ticket ref in a `hook.rs` comment. Its own test printed the
  LEAK header while `grep -i leak` dropped the next line — the one naming the file. Widened to `-A2`.
  See [[mirror-guard-scope-and-excludes]], [[msys-grep-instrument-traps]].
- A `\n` inside a python heredoc collapsed to a real newline again ([[v0340-nowsignal-pretool-agent-output]]);
  used `chr(92)`. Two Bash heredocs also died on quoting — write the script with the Write tool.

## State

520 tests, gate PASS, `traceable-reqs check` exit 0. `REQ-COMMUNE-DROP-DETECT` +
`REQ-HAZARD-COMMUNE-DROP-REARM` (KNOWN-HAZARDS 9.1), `REQ-DIST-CHECKPOINT-COMMUNE` amended (its
detection half superseded). **Seven mutations, all caught, each by its own test and only its own.**
Floor unchanged 0.66.0; skeleton unchanged 0.1.18 (brief text ships in `adapter.spt`). Published,
notes verified at the consumer, mirrored (`3e0b8e9`), installed here — and then field-failed, see top.
All of that ceremony was clean; none of it was the thing that mattered.
