docs: RFC 003 §7.11/§7.12 — delivered is not read, and the mailbox carries obligations not news
Both measured 2026-08-26 by peer devices, and the second one measured on me: the operator had to point at an event id before I read a report that had been addressed to me for two hours. The mailbox was working correctly the whole time. §7.12 is the structural one. Candidates are drawn with status="open", so an event carrying a TERMINAL status is not a mailbox candidate at all, whoever it is addressed to. A task.reply written to a named device to share a finding is therefore delivered to nobody, ever — and neither is any event.ack. So the most natural inter-device message, "here is something you should know", is exactly the shape that gets no delivery. Two things that do work: address it as a directed status="open" ask (correct when a response is actually wanted), or accept it as pull-only and pair it with a drawer, which the peer's search will surface. A terminal report plus an expectation of attention does not. §7.11 is the peer's finding, and it explains the other half of why that report sat unread: delivery is deliverAs "steer" with deliberately no triggerTurn, and the poll fires on agent_settled — i.e. when the agent is IDLE, with no inference running. The text is queued for the next turn, so the human is the trigger. Measured on another device: a delivery landed at ~20:50Z and sat visibly unreacted-to until the operator asked "do I have to nudge you?". Not a defect; the no-triggerTurn decision was deliberate and stands. But the delivered text explains how to CLOSE an ask and never says when it will be SEEN, so the one reader who needs that fact — the human watching the window — is the one not told. Recorded with the three fixes that do not wake a model, as decision 8. §7.4 is upgraded from inferred to measured, on two devices independently, by a two-arm control: a to_agent='*' event with status='open' survives the raw candidate filter and therefore reaches the exclusion branch, which no previously written broadcast (all non-open) ever did. Raw query returned it, derived owed set did not, delivery named only the directed arm. Two arms differing only in to_agent is what makes it evidence rather than an absence. And it carries a correction of my own record, in place and dated: I had filed that this branch COULD NOT be exercised, because every broadcast in the log is status='ready' and the protocol forbids writing an open one. Wrong in an instructive way — the protocol forbids it as PRODUCTION TRAFFIC, which does not forbid planting one as a labelled, self-closed control. Where a rule appears to block a measurement, check whether it blocks the use or the test.
This commit is contained in:
@@ -279,6 +279,10 @@ Measured on real data: the positive-control pair carries `seq` 26/27 and `hlc` `
|
||||
|
||||
Two consequences: **broadcasting an ask reaches no owed set at all** (the "don't broadcast an ask" anti-pattern is mechanically enforced, not merely advised), and to reach a whole fleet with something actionable you must write **one directed event per device**, sharing a `correlation_id` so the thread stays joinable. Separately, an event written with **no** `to_agent` matches no `to_agent=` query ever and is addressed to nobody.
|
||||
|
||||
**✅ Measured 2026-08-26, on two devices independently.** This was inferred from source until a two-arm control settled it. A `to_agent='*'` event with `status='open'` — the anti-pattern, planted deliberately — survives the raw candidate filter and so reaches the exclusion branch, which every previously written broadcast (all non-open) never did. Result: the raw query returned it, the derived owed set did not, and the delivery named only the directed arm of the pair. Confirmed on a second machine that had merely *received* the broadcast rather than planted it. Two arms differing only in `to_agent`, same poll and same fake sender, is what makes it evidence rather than an absence: "delivered exactly one of two" cannot be explained by a mailbox that delivers everything or nothing.
|
||||
|
||||
> ⚠️ **Correction to an earlier record.** A note filed the same day claimed this branch *could not* be exercised, reasoning that every broadcast in the log carries `status='ready'` and the protocol forbids writing an open broadcast. The reasoning was wrong in a specific and instructive way: the protocol forbids it *as production traffic*, which does not forbid planting one as a labelled, self-closed control. Where a rule blocks a measurement, check whether it blocks the *use* or the *test* before recording the branch as untestable.
|
||||
|
||||
### 7.5 `since_event_id` raises on an unknown id
|
||||
|
||||
An unresolvable cursor raises `ValueError` rather than returning empty (`logstream.py:966-970`) — stronger than the tool description promises, and benign on one replica. On a second replica, a resuming watcher whose cursor has not yet replicated will **hard-fail instead of waiting**.
|
||||
@@ -309,6 +313,22 @@ The complete GET route table is `/healthz`, `/statusz`, `/logstream/stream`, `/s
|
||||
|
||||
WAL. **Measured 2026-08-26** on the primary: `logstream.sqlite3` mtime three days old while `logstream.sqlite3-wal` was 865 KiB and seconds old. An operator checking whether coordination is live must look at the `-wal` file, or query.
|
||||
|
||||
### 7.11 Delivered is not read: the human is the trigger
|
||||
|
||||
The mid-session poll delivers with `deliverAs: "steer"` and **deliberately no `triggerTurn`** — waking a model on inbound fleet traffic is a much larger behavioural change than auto-delivery, and was not approved. The poll fires on `agent_settled`, which means the agent is *idle*: nothing is running to react. So the delivered text is queued and read at the top of the **next turn**, whenever a human happens to start one.
|
||||
|
||||
**Measured 2026-08-26:** a delivery landed in a session window at ~20:50Z and sat there, visibly unreacted-to, until the operator asked "do I have to nudge you for you to read it?" — which is precisely the question this design produces. The delay is not the agent choosing to ignore its mailbox; between the poll and the next turn there is no inference at all.
|
||||
|
||||
The consequence is a **UX gap, not a defect**: from the outside, an inbound ask plus an idle agent reads as neglect. The delivered text explains how to *close* an ask but says nothing about *when* it will be seen — so the one reader who needs that fact, the human watching the window, is the one not told. Fixes that do not wake a model: say the queued-until-next-turn semantics in the delivered text itself; notify at poll time (`ctx.ui.notify` / desktop toast) so the human knows a nudge is worthwhile; or an opt-in `MEMPALACE_MAILBOX_TRIGGER=1`, default off, for anyone who does want a turn started.
|
||||
|
||||
### 7.12 The mailbox is an obligation channel, so a report addressed to you is never delivered
|
||||
|
||||
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.
|
||||
|
||||
**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.
|
||||
|
||||
---
|
||||
|
||||
## 8. Phasing
|
||||
@@ -338,6 +358,8 @@ Replication exists in code — `version_vector()`, `list_ops()`, `apply_remote_e
|
||||
5. **Should `GET /logstream/events` exist?** A read-only HTTP tail would let non-MCP consumers (dashboards, CI) follow a stream without an MCP client. Today they must hold SSE or speak MCP.
|
||||
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, and does it ever deserve a turn?** (§7.11) The no-`triggerTurn` decision stands; the open part is the cheap half — stating the queued-until-next-turn semantics in the delivered text, and whether to notify the human at poll time so a nudge is known to be worthwhile.
|
||||
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.1 Retention — decided direction (2026-08-26)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user