rfc-003: propose a news surface that needs no cursor, and say why widening the owed set cannot work

Decision 9 (news vs obligations) gets a proposed direction in a new §9.2, following
how §9.1 promoted the retention decision.

The load-bearing part is the negative result. The tempting fix — let terminal
directed events into the owed set so a report addressed to a device reaches it —
breaks the derivation's fixed point. Owed-ness is "directed at me, not mine, and
not joined by a later terminal event of mine", so the asserting shape (open) and
the clearing shape (terminal) have to be disjoint. Make terminal events owed and a
reply becomes owed by its requester, whose closure is itself directed + terminal
and therefore owed by the original author: every closure mints a fresh obligation
and the loop never terminates. status="open" is not editorial taste about tone, it
is what makes owed-ness terminate. Worth writing down before someone "fixes" it.

The proposal itself avoids the cost that sinks the naive version. "Since your last
session" implies a per-device read cursor, and container-local state is exactly
what --force-recreate erases. No new state is needed: the device's own last
authored event is already a cursor, it lives in the shared log, and it is
comparable across replicas for the same reason the owed-set join now uses hlc
(§7.3). Named the non-obvious constraint too — the anchor must be computed PER
STREAM, because a global one lets a chatty stream advance past unread news in a
quiet one, and that failure is silent.

Also tightened §7.12: candidacy requires exactly `open`, not merely "non-terminal",
so a `claimed` announcement is as undelivered as a finished report. Claiming still
earns its keep on handoff-prone work (it is what distinguishes "nobody started"
from "someone started and the container died"), but its audience is a log reader,
not the requester — and it does not quiet the claimer's own mailbox either, which
is what the state machine's "open --> claimed does NOT clear" already implies.
This commit is contained in:
2026-08-27 17:28:21 +02:00
parent de59571966
commit b2b50afcc1
+24 -1
View File
@@ -338,6 +338,8 @@ Still deliberately **not** done: `MEMPALACE_MAILBOX_TRIGGER=1` (opt-in, default
Candidates are drawn with `status="open"` (§3.3), so an event carrying a **terminal** status is not a mailbox candidate at all — no matter who it is addressed to. A `task.reply` sent to a named device to share a finding is therefore delivered to nobody, ever, and neither is any `event.ack`. It sits in the log until someone reads the log.
Read the filter precisely: candidacy requires **exactly `open`**, not merely "non-terminal". So a `claimed` announcement is equally undelivered — announcing that you have started work reaches the requester's mailbox no more than your finished report does. Claiming is still worth doing on long or handoff-prone work, because it puts *pickup* in the log for whoever later asks "did anyone start this before that container died?", but its audience is a log reader, not the requester. Note also that claiming does not quiet **your own** mailbox: the original ask stays owed until a terminal event of yours joins it (§3.3), which is exactly what the state machine means by `open --> claimed` *does NOT clear*.
**Measured 2026-08-26:** a device wrote a detailed report addressed to `pi@<peer>` with `status="applied"`, on a thread whose ask belonged to a third party. The recipient never saw it and only read it when the operator pointed at it by id — with the mailbox working exactly as specified throughout.
This is the shape of the channel, and it is worth stating because the natural inter-device message — *"here is something you should know"* — is exactly the shape that gets no delivery. Two ways to make news reach a peer: address it as a **directed `status="open"` ask** so it enters the owed set and gets closed when acted on (correct when a response is genuinely wanted), or accept that it is **pull-only** and pair it with a palace drawer, which the peer's search *will* surface later. What does not work is a terminal-status report and an expectation of attention.
@@ -372,7 +374,7 @@ Replication exists in code — `version_vector()`, `list_ops()`, `apply_remote_e
6. **Undocumented limits.** The 64 KiB `metadata` cap and the lowercase-`type` regex are enforced server-side but absent from the tool schemas (§3.1). Document, or relax.
7. **Agent-name registry.** Nothing prevents two devices sharing one `from_agent` (§7.7). A warning at append time would be cheap.
8. ~~**Does an arriving ask deserve a notification?**~~ **Done 2026-08-26** — delivery note plus `MEMPALACE_MAILBOX_NOTIFY` (§7.11). What stays open is the narrower question: should `desktop` be the *default* rather than opt-in, and does an arriving ask ever deserve a turn (`MEMPALACE_MAILBOX_TRIGGER`)?
9. **Is there a delivery path for news rather than obligations?** (§7.12) Today the only delivered shape is a directed open ask. Options: leave it pull-only and rely on the paired drawer, or give informational events a distinct low-priority surface at wake-up ("3 reports since your last session") separate from the owed set.
9. **Is there a delivery path for news rather than obligations?** (§7.12) Today the only delivered shape is a directed open ask. Options: leave it pull-only and rely on the paired drawer, or give informational events a distinct low-priority surface at wake-up ("3 reports since your last session") separate from the owed set. **Proposed direction in §9.2** — including why the obvious fix (widening the owed set) is not available.
### 9.1 Retention — decided direction (2026-08-26)
@@ -391,6 +393,27 @@ Three constraints any implementation has to respect:
Open sub-questions: the window length (a quarter is the obvious first guess); whether cold storage is a sibling `logstream-archive-<period>.sqlite3` or plain files on disk; and whether archives are queryable through the same tools behind an explicit opt-in flag, or simply left as files for a human to open when a question reaches back that far.
### 9.2 A news surface, without new state — proposed direction (2026-08-27)
Decision 9 above is **proposed in direction**: news should get its own low-priority surface at wake-up, and the owed set should not be touched.
**Why the obvious fix is not available.** The tempting change is to let terminal directed events into the owed set, so a report addressed to a device reaches it. That breaks the derivation's fixed point. Owed-ness is defined (§3.3) as *directed at me, not written by me, and not joined by a later terminal event of mine* — so the asserting shape (`open`) and the clearing shape (terminal) must be disjoint. Make terminal events owed, and: a reply becomes owed by its requester; the requester clears it by writing a terminal event joined to it; that event is directed, terminal, and therefore owed by the original author; and every closure mints a fresh obligation. The loop never terminates. You could exempt `task.reply` and `event.ack` by `type`, but that is the same filter re-entered through a different door, with more surface to get wrong. **`status="open"` is not an arbitrary editorial choice about tone; it is what makes owed-ness terminate.**
**The proposal.** A second, clearly-separated section at wake-up — "N reports since your last session" — listing directed non-`open` events newer than the reader's own last activity, count-capped, bodies not inlined.
The cost that sinks the naive version is state: "since your last session" implies a per-device read cursor, and a cursor in container-local storage is precisely what a `--force-recreate` erases (the palace exists because container state does not survive). No new state is needed, because **the device's own last authored event is already a cursor**: surface directed events whose `hlc` is greater than the newest `hlc` among events written by this device. That anchor lives in the shared log, survives recreate, and is comparable across replicas for the same reason the owed-set join now uses `hlc` (§7.3).
Properties worth stating before anyone implements it:
- **Per stream, not global.** Take the anchor as the newest `hlc` of this device's events *in that stream*. A global anchor lets a chatty stream advance past unread news in a quiet one — the failure is silent, so this is not a refinement to leave for later.
- **A device that has never written sees everything.** Bounded by the count cap, and arguably correct on first boot; it is the same shape as a new joiner reading recent history.
- **A busy device gets a narrow window.** Correct by construction: you saw the log when you last wrote to it.
- **There is no read receipt, so repeats are possible** until the anchor advances. Acceptable only because this surface is a summary, not an interruption: count and one-line subjects, never bodies.
- **It must not resurface.** Owed items re-announce (`MEMPALACE_MAILBOX_RESURFACE_MS`); news must not, or it becomes nagging without obligation.
- **Opt-in first**, following the notification precedent (§7.11): inert unless explicitly enabled, and never counted in the owed total.
**What this does not fix.** It is still not push (§7.12 and the 404 on `/logstream/stream` for this deployment), still queued into the next turn rather than waking anyone (§7.11), and still useless to a device that never starts a session. For anything that must survive indefinitely, the paired **drawer** remains the durable channel; this surface only shortens the delay before a peer notices something already written.
---
## 10. Evidence index