Skip to content

Commit 334c60e

Browse files
authored
Merge pull request #2352 from MemPalace/docs/watch-reply-filter
docs(coordination): wake for replies, and keep announcements silent
2 parents dfba59b + 63581bd commit 334c60e

5 files changed

Lines changed: 42 additions & 15 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ Large palaces get fast and stay small: both storage backends lost their palace-w
1212

1313
### Features
1414

15-
- **`mempalace logstream watch` — a background watcher an agent can be woken by.** `logstream wait` is a primitive, not a watcher: it caps at five minutes and reports a timeout, so every caller ended up writing the same re-arm loop and each one had to remember to carry its cursor forward. Most did not, and coordinated tasks stalled on nobody listening rather than on the work. `watch` owns the loop and the cursor, and exits on a match so any harness that can background a process and react to its exit gets woken — `0` when it printed a match, `2` on `--idle-exit-ms`, `130` when interrupted; only `0` means "you have mail". Two filters a watcher needs are things `list_events` cannot express, because its SQL is single-valued and positive-only: repeating a flag means "or" (`--type task.request --type patch.ready` wakes for either and stays quiet for everything else), and `--agent <id>` expands to `--to-agent <id> --exclude-from-agent <id>`. That exclusion is not cosmetic — `to_agent=<you>` deliberately also matches `*` broadcasts, and your own broadcasts are broadcasts, so a watcher without it wakes itself every time it posts a status. `--state-file` persists the cursor so a restart resumes exactly, advancing past events that were examined and rejected rather than only matches; a cursorless first run starts at the tip like the SSE live-tail rather than replaying weeks of fleet history it cannot tell is stale, and says so on stderr. `--follow` keeps the process alive past the first match and emits NDJSON, since repeated indented documents on one stream are not parseable JSON. The invariant throughout is that a restart may cost a duplicate, never a missed delegation. (#2315)
15+
- **`mempalace logstream watch` — a background watcher an agent can be woken by.** `logstream wait` is a primitive, not a watcher: it caps at five minutes and reports a timeout, so every caller ended up writing the same re-arm loop and each one had to remember to carry its cursor forward. Most did not, and coordinated tasks stalled on nobody listening rather than on the work. `watch` owns the loop and the cursor, and exits on a match so any harness that can background a process and react to its exit gets woken — `0` when it printed a match, `2` on `--idle-exit-ms`, `130` when interrupted; only `0` means "you have mail". Two filters a watcher needs are things `list_events` cannot express, because its SQL is single-valued and positive-only: repeating a flag means "or" (`--type task.request --type task.reply --type patch.ready` wakes for any of them and stays quiet for everything else), and `--agent <id>` expands to `--to-agent <id> --exclude-from-agent <id>`. That exclusion is not cosmetic — `to_agent=<you>` deliberately also matches `*` broadcasts, and your own broadcasts are broadcasts, so a watcher without it wakes itself every time it posts a status. `--state-file` persists the cursor so a restart resumes exactly, advancing past events that were examined and rejected rather than only matches; a cursorless first run starts at the tip like the SSE live-tail rather than replaying weeks of fleet history it cannot tell is stale, and says so on stderr. `--follow` keeps the process alive past the first match and emits NDJSON, since repeated indented documents on one stream are not parseable JSON. The invariant throughout is that a restart may cost a duplicate, never a missed delegation. (#2315)
1616
- **The monitoring protocol is documented, including the cursor rule that costs real events.** Events are ordered by append order (`ORDER BY rowid`), not wall clock, so a peer's event is appended whenever it syncs and can already be older than a timestamp high-water mark — resuming with `since_created_at` therefore drops late-arriving cross-replica events permanently. Measured on a live fleet: a windows-origin event created `09:10:48Z` was ingested *after* a mac-origin event created `09:13:21Z`, in a single fifty-event window. `list_events`' docstring already said `since_event_id` was "the precise cursor … regardless of timestamp ties"; it just never reached an agent. That rule now appears in the `mempalace_event_list` and `mempalace_event_wait` tool descriptions at the point of use, both `since_created_at` parameters are marked "NOT a resume cursor", and `coordination-protocol.md` gained a monitoring section covering the modes, the announce-your-watch convention, and declaring when you are *not* watching — a false watcher is worse than a declared-absent one, because the requester stops looking for a human to nudge. The system-prompt snippet every agent copies was updated with it. (#2315)
1717

1818
### Bug Fixes

integrations/shared/coordination-protocol.md

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ can run a background process and react to its exit gets woken:
105105
```bash
106106
mempalace logstream watch \
107107
--agent mac-claude \
108-
--type task.request --type patch.ready \
108+
--type task.request --type task.reply --type patch.ready \
109109
--state-file ~/.mempalace/watch/mac-claude.json --json
110110
```
111111

@@ -114,10 +114,14 @@ mempalace logstream watch \
114114
`to_agent=<you>` deliberately matches `*` broadcasts, and your own
115115
broadcasts are broadcasts, so a watcher without it wakes itself every time
116116
it posts a status.
117-
- **Repeat a filter to mean "or"**`--type task.request --type patch.ready`
118-
wakes for either and stays silent for everything else. This is how you get
119-
"or nothing": narrow to the event types that actually require you, and
120-
routine status traffic stops waking you.
117+
- **Repeat a filter to mean "or"** — `--type task.request --type task.reply
118+
--type patch.ready` wakes for any of them and stays silent for everything
119+
else. This is how you get "or nothing": narrow to the event types that
120+
actually require you, and routine status traffic stops waking you. If you
121+
ever delegate, `task.reply` belongs in the filter: a worker reporting
122+
`blocked` or `failed` sends exactly that, and a watcher that rejects it
123+
advances its durable cursor past it silently — the delegation then sits
124+
unanswered until a manual sweep.
121125
- **`--state-file`** persists the cursor, so a restart resumes exactly where
122126
it stopped rather than replaying or skipping. It advances past events that
123127
were examined and rejected, not only matches. When the cursor cannot be
@@ -164,7 +168,7 @@ by timeout:
164168
type: status room: status to_agent: * correlation_id: <the task>
165169
166170
<AGENT_ID> is MONITORING this correlation for coordination replies
167-
(task.request / task.reply / patch.ready / status).
171+
(task.request / task.reply / patch.ready).
168172
169173
Watching: to_agent=<AGENT_ID> and correlation_id=<id> on stream project/<name>.
170174
Cursor after: evt_20260811T112013_19320fbd7541
@@ -178,6 +182,18 @@ reaches you), **the cursor** (so others know what you have already seen),
178182
**the overlap warning** (so others do not duplicate), and **the fact that a
179183
watcher exists at all**.
180184

185+
Two hygiene rules keep announcements from becoming noise. Announce in a
186+
`status` type — which the recommended inbox filter above (`task.request` /
187+
`task.reply` / `patch.ready`) sleeps through — so the announcement lands in
188+
everyone's next sweep without burning a wake-up. Keep `status` out of your
189+
advertised wake filter for the same reason: a fleet whose watchers wake on
190+
`status` wakes on every announcement;
191+
an announcement typed as `task.reply` wakes every watching window, and
192+
self-exclusion only protects an agent from its own events, not from six
193+
peers announcing back. And announce once per session or when the filter
194+
changes — never on every re-arm, or a fleet of re-arming watchers wakes
195+
itself in a loop.
196+
181197
### Declare when you are *not* watching
182198

183199
A turn-based agent — most chat-driven harnesses — has no background loop. It
@@ -250,7 +266,8 @@ Coordination (logstream):
250266
and treat its exit as "you have mail" (exit 0 = match, 2 = idle). Use
251267
--agent, not --to-agent: it also excludes your own events, which
252268
otherwise wake you via the '*' broadcast match. Repeat --type to wake
253-
only for what needs you. In-turn, waiting on one known correlation,
269+
only for what needs you; if you delegate, include task.reply — blocked
270+
and failed arrive as replies. In-turn, waiting on one known correlation,
254271
mempalace_event_wait is enough. Before a coordinated task, post a
255272
status event to to_agent=* naming your filter and your cursor so others
256273
know you are listening. If you are turn-based and cannot watch between

website/concepts/agent-logstream.md

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ can treat process exit as "you have mail":
163163
```bash
164164
mempalace logstream watch \
165165
--agent mac-claude \
166-
--type task.request --type patch.ready \
166+
--type task.request --type task.reply --type patch.ready \
167167
--state-file ~/.mempalace/watch/mac-claude.json --json
168168
```
169169

@@ -172,7 +172,9 @@ exclusion matters more than it looks: `to_agent=<you>` also matches `*`
172172
broadcasts, and your own broadcasts are broadcasts, so a watcher without it
173173
wakes itself on every status it posts. Repeating a filter means "or", which is
174174
how you narrow to the events that genuinely require you and stop being woken by
175-
routine traffic. Exit is `0` on a match and `2` on `--idle-exit-ms`, matching
175+
routine traffic — but keep `task.reply` in the set if you ever delegate:
176+
`blocked` and `failed` arrive as replies, and a watcher that rejects one
177+
advances its cursor past it silently. Exit is `0` on a match and `2` on `--idle-exit-ms`, matching
176178
`wait`'s timeout convention; `--follow` keeps the process alive past the first
177179
match for daemons. A cursorless first run starts at the tip, like the SSE
178180
live-tail, and says so on stderr — replaying a long fleet log would wake a new
@@ -193,7 +195,10 @@ post a `status` event to `to_agent=*` when you begin monitoring a correlation,
193195
naming four things: the filter you are watching, the cursor you have reached,
194196
the work that must not be duplicated, and — implicitly — the fact that someone
195197
is home. Agents deciding whether to delegate can then check instead of
196-
guessing.
198+
guessing. Announce in a `status` type the fleet's watchers sleep
199+
through, once per session and again when your filter changes — an
200+
announcement typed as something watchers wake
201+
on wakes every window, every time anyone re-arms.
197202

198203
The inverse is equally important. If your harness is turn-based and stops
199204
existing between prompts, declare that rather than staying quiet: publish your

website/guide/shared-brain.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,8 @@ Coordination (logstream):
141141
and treat its exit as "you have mail" (exit 0 = match, 2 = idle). Use
142142
--agent, not --to-agent: it also excludes your own events, which
143143
otherwise wake you via the '*' broadcast match. Repeat --type to wake
144-
only for what needs you. In-turn, waiting on one known correlation,
144+
only for what needs you; if you delegate, include task.reply — blocked
145+
and failed arrive as replies. In-turn, waiting on one known correlation,
145146
mempalace_event_wait is enough. Before a coordinated task, post a
146147
status event to to_agent=* naming your filter and your cursor so others
147148
know you are listening. If you are turn-based and cannot watch between
@@ -322,7 +323,11 @@ How the loop plugs into a harness:
322323
Two etiquette rules close the loop. **Announce your watch**: before a
323324
coordinated task, post a `status` event to `to_agent=*` naming the filter you
324325
watch and the cursor you have reached, so others delegate to an agent they
325-
know is home. And **resume by event id, never by timestamp**: events append
326+
know is home. Keep the announcement in a `status` type — one the fleet's
327+
watchers sleep through — and announce once per session (again if your
328+
filter changes), not on every re-arm:
329+
an announcement typed as something watchers wake on wakes every window, and
330+
self-exclusion only guards each agent against its own events. And **resume by event id, never by timestamp**: events append
326331
in arrival order, so a peer's event can sync in already "older" than a
327332
wall-clock high-water mark — `since_created_at` as a resume cursor drops it
328333
permanently, `since_event_id` never does.

website/reference/cli.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,7 @@ mempalace logstream wait --correlation-id task_123 --type patch.ready \
202202
mempalace logstream ack evt_... --from-agent mac --status applied
203203

204204
# Background watcher: blocks, wakes on what needs you, exits 0 on a match
205-
mempalace logstream watch --agent mac --type task.request --type patch.ready \
205+
mempalace logstream watch --agent mac --type task.request --type task.reply --type patch.ready \
206206
--state-file ~/.mempalace/watch/mac.json --json
207207
```
208208

@@ -211,7 +211,7 @@ mempalace logstream watch --agent mac --type task.request --type patch.ready \
211211
| `append` | Append an immutable event (`--type`, `--stream`, `--room`, `--from-agent` required; `--body`/`--body-file`, `--artifact-id` repeatable) |
212212
| `list` | List events, oldest first (all routing fields as filters, `--since-event-id`, `--limit`) |
213213
| `wait` | Long-poll until a match or timeout (`--timeout-ms`, max 300000; exits `2` on timeout) |
214-
| `watch` | Background watcher: re-arms past the `wait` cap, carries the cursor, and exits `0` on a match / `2` on `--idle-exit-ms`. `--agent ID` is shorthand for `--to-agent ID --exclude-from-agent ID` so your own `*` broadcasts never wake you. Filters repeat to mean "or"; `--state-file` resumes exactly; `--follow` stays alive past the first match; a cursorless first run starts at the tip (`--from-start` to replay); exits `130` if interrupted; `--follow --json` emits NDJSON |
214+
| `watch` | Background watcher: re-arms past the `wait` cap, carries the cursor, and exits `0` on a match / `2` on `--idle-exit-ms`. `--agent ID` is shorthand for `--to-agent ID --exclude-from-agent ID` so your own `*` broadcasts never wake you. Filters repeat to mean "or"; `--state-file` resumes exactly; `--follow` stays alive past the first match; a cursorless first run starts at the tip (`--from-start` to replay); exits `130` if interrupted; `--follow --json` emits NDJSON — one batch envelope per line (`{"events": [...], "count": N, "cursor": ...}`), not one event per line |
215215
| `ack` | Append an `event.ack` for an event (`--from-agent` required, `--status`, `--body`) |
216216
| `sync` | Pull missing events/artifacts from peer replicas (`--peer URL --token T`, or all peers in `peers.json`) |
217217

0 commit comments

Comments
 (0)