docs(coord): measure the debt, and record why no second mechanism was built

WP5 asked for one of two candidate mechanisms, chosen by measurement. The
measurement chose neither.

coord-sweep.sh already IS the bulk-ack for pure notices, and it has never
run: no _sweep.log at the default path, and none anywhere under ~/.claude
(stated that way because --log can override the default, so an absent
default-path log alone would not prove it). Its own dry-run agrees exactly
with an independent classification of the same inboxes - 0 at 30 days, 7 at
14, 13 at 7 - so the gap is invocation, not mechanism.

The broadcast class converges on its own: reading records a broadcast as
seen, so a mailbox clears its backlog on its next session. 263 of 884 pairs
are unread, but 34 of those belong to two mailboxes no session can hold -
one a documented retired --to address, one with no checkout anywhere under
/Users/ktg (known-positive control: llm-ingestion-okf resolves). A TTL would
close those rather than reduce them.

What remains is running coord-sweep --write unattended, which closes mail in
51 other repos' inboxes unread. That is a decision on another repo's behalf
and a policy constant of the same class as the STATE.md line limit, so it is
left to the operator rather than shipped. No version bump: nothing here
changes behaviour.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-09-03 20:48:37 +02:00
commit a2019d44f7

View file

@ -0,0 +1,103 @@
# Coordination debt: what does not converge, and why building a second
# mechanism would have been wrong
Measured 2026-09-03 against the live mailbox, for order
`20260902T113745Z-1254925290-from-.claude` (WP5). Every number below was
produced by a command, and every negative result carries the control that
proves the query could have found something.
The order offered two candidate mechanisms and said to choose by measurement,
not taste: a broadcast TTL, or a bulk-ack for pure notices. The measurement
chose neither. One of them is already built and has never been run; the other
addresses the class that converges on its own.
## Denominators
| population | count |
|---|---|
| mailbox directories under the coord root | 55 |
| of those, holding an `inbox/` | 52 |
| pending directed messages across all inboxes | 27 |
| broadcasts in `_broadcast/inbox/` | 17 |
| (mailbox x broadcast) delivery pairs | 884 |
The three directories with no `inbox/` are named rather than silently dropped:
`jobbsok`, `mediemon`, `medieovervaaking`. 55 - 3 = 52 is the reconciliation,
stated because an unreconciled pair of denominators in one report is the same
positive-looking null this engine refuses everywhere else.
## The two classes behave in opposite directions
**Directed messages do not converge.** They are re-injected at every session
start until a session closes them by hand. Of the 27 pending, 4 owe a reply and
23 are pure notices (`reply-expected: no`). By age:
| class | <7d | 7-13d | 14-29d | >=30d |
|---|---|---|---|---|
| owes a reply | 0 | 3 | 1 | 0 |
| pure notice | 10 | 6 | 7 | 0 |
**Broadcasts converge on their own.** Reading one records it as seen, so a
mailbox clears its whole backlog on its next session. 263 of the 884 pairs are
unread (29.8%), and the distribution shows the self-clearing: 10 mailboxes are
fully current, 26 sit at exactly 6 unread (the newest announcements), and the
tail is short.
## The floor under the broadcast number, which strengthens the case
Two mailboxes hold all 17 broadcasts unread, and neither can ever read them:
- `ktg-plugin-marketplace` is a **retired `--to` address**. It is a polyrepo
directory, not a git repo, so `basename(git toplevel)` can never resolve to
it and no session can hold that identity. This is already documented as
engine behaviour; the 17 unread are its permanent consequence.
- `llm-ingestion-guard` has no checkout anywhere under `/Users/ktg`
(`find -maxdepth 4`, with `llm-ingestion-okf` as the known-positive control
proving the query finds a real one) and no `.origin`. It also holds the
single oldest pending notice, 24 days.
So **34 of 263 unread pairs (12.9%) are a permanent floor no TTL would reduce
to zero** - it would close them, but it would be closing announcements for
mailboxes that were never going to read anything. `.origin` absence alone is
NOT a proxy for unholdable: `repos` also lacks one, yet sits at 2 unread of 17,
which is only possible if something reads it.
## Why no second mechanism was built
`coord-sweep.sh` already **is** the bulk-ack for pure notices: machine-wide,
one mechanically decidable class (`reply-expected: no`, older than a grace
window), dry-run by default, closing through `coord-done.sh`, logging sender
and subject for every closure. Building the order's second candidate would have
been a second copy of a shipped policy - the defect class this repo names
repeatedly.
Its own dry-run reports what it would close today, and the figures agree
exactly with the independent classification above:
--days 30 -> 0 messages
--days 14 -> 7 messages
--days 7 -> 13 messages
**The gap is invocation, not mechanism.** No `_sweep.log` exists at the default
path, and no sweep log exists anywhere under `~/.claude` (the one `*sweep*` hit
is an unrelated plugin file) - stated that way because `--log` can override the
default, so an absent default-path log alone would not prove it never ran.
## What remains, and why it is not this repo's call
Making the notice class converge without opening each repo means running
`coord-sweep.sh --write` unattended. That closes mail in 51 other repos'
inboxes, unread, and the script's own design says so in as many words: a notice
to a repo left unopened for the whole window is closed unread, and the log is
the only thing standing between that and a silent disappearance.
Deciding that on another repo's behalf is the one anti-pattern with no
exception clause, and the grace window is a policy constant of the same class
as the STATE.md line limit, which was an operator decision both times it moved.
Dry-run is the default precisely because this is the script that destroys
pending state; flipping that to a schedule is the operator's act, not a
plugin's.
The messages that owe a reply are untouched by any of this, at any age, with
any flag. That is not a gap to close later - it is the rule that keeps a
procedural duty from becoming a substantive one.