From b2b50afcc115d07646f0e4b6000478ab803cc7fa Mon Sep 17 00:00:00 2001 From: Joakim Persson Date: Thu, 27 Aug 2026 17:28:21 +0200 Subject: [PATCH] rfc-003: propose a news surface that needs no cursor, and say why widening the owed set cannot work MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- docs/rfc-003-coordination-log.md | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/docs/rfc-003-coordination-log.md b/docs/rfc-003-coordination-log.md index f77bff0..6371729 100644 --- a/docs/rfc-003-coordination-log.md +++ b/docs/rfc-003-coordination-log.md @@ -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@` 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-.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