pi bridge: the log gets read, not just written
Adds the auto-delivered mailbox. Until now the bridge stamped events on the way out and never read the log, so a directed ask reached an agent only if that agent happened to run event_list itself — which in practice meant ALC telling it to. The channel had real cross-machine traffic since 2026-08-18 and no reader. DELIVERY, two points, both fail-silent and both additive (223 insertions, 0 deletions; the feed's agent_settled handler is byte-identical): - session start: one more sections.push() in the existing before_agent_start wake-up injection, beside mempalace_status and diary_read. - mid-session: a second agent_settled handler, poll floored at MEMPALACE_MAILBOX_POLL_MS (default 300000 = 5 min), delivered with pi.sendMessage(deliverAs: "steer"). The cadence is chosen from measured arrival, not taste: 22 events since 2026-08-18, of which ELEVEN landed inside one 5h38m window today. Arrival is bursty and correlates with the agent's own activity, because events arrive when another machine is working the same thread — so agent_settled (activity-coupled) is the right trigger and a wall-clock timer is the wrong one. Tightest observed gap was 2m12s, so a 5-minute floor coalesces a burst into one message instead of delivering five. POLLING IS DECOUPLED FROM DELIVERY, which is the part that keeps this from becoming noise: polling is cheap and frequent, but an item is only announced if it has not been surfaced this session, or was surfaced more than MEMPALACE_MAILBOX_RESURFACE_MS ago (default 1 h). Re-announcing the same ask every five minutes would train the reader to ignore it — the exact failure the status filter was introduced to prevent. The dedup map is in memory on purpose: after a restart it may re-show something already seen, and that is the SAFE failure direction (a resurfacing item is visible noise; a suppressed unanswered ask is silent and permanent). Owed-ness is DERIVED, never read off a field. event_ack appends and status is written once, so a directed `open` matches the mailbox query forever, answered or not — measured on this device, where the raw filter returned 3 asks of which 2 were already answered. A candidate is answered only when one of this device's own events has a strictly higher seq, joins via metadata.ack_of or a shared correlation_id, and carries a terminal status. The seq test is load-bearing: without it one terminal reply suppresses every later ask on that correlation forever, verified against the live thread where a seq-16 reply precedes the seq-17 request it cannot have answered. `*` broadcasts are excluded even though to_agent=<me> matches them, because the protocol says a broadcast owes nobody a reply. Leaving them in would have made this code contradict the skill documenting it, and would have made every machine think it personally owed the same answer. It also gives "don't broadcast an ask" teeth: broadcasting one now demonstrably reaches no owed set. GATE: on when MEMPALACE_PI_DEVICE and MEMPALACE_REMOTE_URL are both set (the same pair as the stamper — an unstamped client has no address to be reached at), off with MEMPALACE_MAILBOX=0. Default-on is deliberate and ALC's call: the problem being fixed is that nobody reads the inbox, and an opt-in fix for a nobody-does-it problem only relocates the forgetting. Inert on a solitary palace. README §2 rewritten in the same commit — it asserted "the bridge is write-only today: there is no mailbox, no poll, no delivery", which this commit falsifies. Shipping the code without the doc edit would have left a record asserting something untrue in the very file documenting the fix for that class of defect. VERIFIED: tsc 5.9.3 --strict, 0 errors, against the real pi types, with the harness mutation-tested first (an injected error on a new line was caught, then restored clean) because this repo has no package.json, no tsconfig and no tsc on PATH — nothing in-repo will re-run this. Owed-set logic extracted verbatim from the implementation and run against the live fixture: 3 candidates -> owed [seq 21] only; demoting the seq-19 reply to seq 5 makes seq 17 owed again (proves the ordering guard is live, not dead code); an open `*` broadcast and a self-authored open ask are both excluded; null correlation_id does NOT join itself (a plain === would have had null === null clear every uncorrelated ask). NOT verified: no live palace call from the implementation, and the queue-vs- interrupt semantics of "steer" are read from docs/extensions.md, not observed.
This commit is contained in:
+39
-9
@@ -301,13 +301,42 @@ identity — `from_agent` / `to_agent` carry the entire distinction. Measured
|
||||
So the stamping above is what makes `to_agent="pi@tor-ms22"` mean anything, and
|
||||
an *unstamped* client addressed as bare `pi` is unreachable.
|
||||
|
||||
**2. The bridge is write-only today.** It stamps events on the way out and never
|
||||
reads the log: there is no mailbox, no poll, no delivery. An event addressed to
|
||||
this machine by name reaches the agent only if the agent runs
|
||||
`mempalace_event_list` itself — which is why that query is a wake-up step in the
|
||||
*consumer* skill (`~/.agents/skills/mempalace/SKILL.md`), and why the protocol
|
||||
norms live there rather than here. **This file documents the mechanism; the
|
||||
skill is normative for behaviour.**
|
||||
**2. The bridge now READS the log too — auto-delivered mailbox.** It derives what
|
||||
this device owes and injects it, so an event addressed to this machine no longer
|
||||
waits for the agent to think of asking. Two delivery points, both fail-silent:
|
||||
|
||||
- **Session start** — one more section in the existing `before_agent_start`
|
||||
wake-up injection, alongside `mempalace_status` and `diary_read`.
|
||||
- **Mid-session** — an `agent_settled` poll, floored at
|
||||
`MEMPALACE_MAILBOX_POLL_MS` (default 300000, i.e. 5 min), delivering via
|
||||
`pi.sendMessage(..., { deliverAs: "steer" })`. Note this **queues, it does not
|
||||
interrupt**: at `agent_settled` the agent is idle, so the message lands at the
|
||||
start of the next turn and spends no LLM call. There is deliberately no
|
||||
`triggerTurn` — waking the model on inbound fleet traffic is a much larger
|
||||
behavioural change than auto-delivery.
|
||||
|
||||
Owed-ness is **derived, never read off a field**, because `event_ack` appends and
|
||||
`status` is written once: a directed `open` event matches the mailbox query
|
||||
*forever*, answered or not. Two calls (`to_agent=<me> status=open`, and
|
||||
`from_agent=<me>`), then a candidate counts as answered only when one of this
|
||||
device's own events has a **strictly higher `seq`**, joins via
|
||||
`metadata.ack_of` or a shared `correlation_id`, *and* carries a terminal status
|
||||
(`applied`/`superseded`/`failed`/`blocked`). The `seq` test is load-bearing:
|
||||
without it one terminal reply suppresses every later ask on that correlation
|
||||
forever. `seq` is replica-local, so `hlc` is the correct key once `mesh_peers`
|
||||
reports actual peers.
|
||||
|
||||
`*` broadcasts are excluded even though `to_agent=<me>` matches them, because the
|
||||
protocol says a broadcast owes nobody a reply — which also means broadcasting an
|
||||
ask demonstrably reaches no owed set, giving that documented anti-pattern teeth.
|
||||
|
||||
Gated on `MEMPALACE_PI_DEVICE` **and** `MEMPALACE_REMOTE_URL` (the same pair as
|
||||
the stamper, since an unstamped client has no address to be reached at), and
|
||||
disabled outright with `MEMPALACE_MAILBOX=0`. Inert on a solitary palace: no
|
||||
calls, no injection. Delivery is the mechanism; the *norms* — what a reply owes,
|
||||
and that only a terminal event closes a thread — remain normative in the
|
||||
*consumer* skill (`~/.agents/skills/mempalace/SKILL.md`). **This file documents
|
||||
the mechanism; the skill is normative for behaviour.**
|
||||
|
||||
**3. Live push is a deployment question, not a code one.** The palace implements
|
||||
an SSE endpoint (`GET /logstream/stream`, `text/event-stream` in
|
||||
@@ -315,8 +344,9 @@ an SSE endpoint (`GET /logstream/stream`, `text/event-stream` in
|
||||
through its reverse proxy — verified 2026-08-26 against
|
||||
`https://mempalace.jordbo.se`, where `/logstream/events`, `/logstream/stream`
|
||||
and `/sync/peers` all return 404 while `/mcp` serves normally. Where that is the
|
||||
case, polling through the existing MCP client is the only available path, and
|
||||
enabling SSE means a proxy route plus an auth decision — not an extension change.
|
||||
case, polling through the existing MCP client is the only available path — which
|
||||
is what the mailbox in §2 does — and enabling SSE means a proxy route plus an
|
||||
auth decision, not an extension change.
|
||||
|
||||
As with stamping, all of this is inert unless `MEMPALACE_REMOTE_URL` points at a
|
||||
shared palace. On a solitary palace the event tools work fine and the log
|
||||
|
||||
Reference in New Issue
Block a user