Skip to content

Commit bd97300

Browse files
authored
Merge pull request #2315 from MemPalace/docs/logstream-monitoring
feat(logstream): background watcher agents can be woken by, plus the monitoring protocol
2 parents 570891e + c3ad155 commit bd97300

8 files changed

Lines changed: 1685 additions & 16 deletions

File tree

integrations/shared/coordination-protocol.md

Lines changed: 157 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,143 @@ the event trail is only auditable if identities are stable.
5959
`type=task.reply` with `status=blocked` or `failed` and verbatim
6060
notes. Silence is the only unrecoverable failure.
6161

62+
## Monitoring the stream
63+
64+
Most coordination friction is not a protocol failure — it is a *listening*
65+
failure. A task sits `open` because the agent it was addressed to was never
66+
watching, and the requester cannot tell the difference between "working on
67+
it" and "nobody is home". Pick a monitoring mode deliberately and make it
68+
visible.
69+
70+
### The cursor rule
71+
72+
**Resume with `since_event_id`. Never resume with `since_created_at`.**
73+
74+
Events are ordered by *append* order (rowid), not by wall clock. Across
75+
replicas those diverge: a peer's event created at 09:10:48Z can be ingested
76+
*after* a local event created at 09:13:21Z, because it only arrived at sync
77+
time. A cursor based on `since_created_at` silently skips such an event —
78+
it is already older than your high-water mark by the time you see it, so you
79+
never see it at all.
80+
81+
- `since_event_id` — the precise cursor: strictly after that event in append
82+
order, regardless of timestamp ties. **This is what a watcher stores.**
83+
- `since_created_at` — a time *window* for questions like "what happened
84+
today". Inclusive (`>=`), so callers must dedup by `id`. Not a cursor.
85+
86+
Your entire watcher state is one string: the id of the last event you
87+
processed.
88+
89+
### Four modes — pick by how long you stay alive
90+
91+
| Mode | Use when | How |
92+
|---|---|---|
93+
| **Inbox sweep** | Start of every session, and before any long task | `mempalace_event_list` with `to_agent=<you>`, `since_event_id=<last seen>`, `preview=true` |
94+
| **Background watcher** | You want to be woken while you work | `mempalace logstream watch` as a background process — see below |
95+
| **Long-poll** | Actively waiting on one known correlation, in-turn | `mempalace_event_wait` with `correlation_id` + `to_agent=<you>` |
96+
| **Push (SSE)** | Persistent processes: daemons, dashboards, live viewers | `GET /logstream/stream` — same filters, same envelope, `since_event_id` resume |
97+
| **Declared-idle** | Turn-based agents that stop existing between prompts | You cannot watch. Say so, publish your cursor, and let the requester ping you |
98+
99+
### The background watcher
100+
101+
`mempalace logstream watch` is the mode most agents want. It blocks until
102+
something you care about arrives, prints it, and exits — so any harness that
103+
can run a background process and react to its exit gets woken:
104+
105+
```bash
106+
mempalace logstream watch \
107+
--agent mac-claude \
108+
--type task.request --type patch.ready \
109+
--state-file ~/.mempalace/watch/mac-claude.json --json
110+
```
111+
112+
- **`--agent <id>`** is the flag to reach for. It means `--to-agent <id>`
113+
*and* `--exclude-from-agent <id>`. The exclusion is not cosmetic:
114+
`to_agent=<you>` deliberately matches `*` broadcasts, and your own
115+
broadcasts are broadcasts, so a watcher without it wakes itself every time
116+
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.
121+
- **`--state-file`** persists the cursor, so a restart resumes exactly where
122+
it stopped rather than replaying or skipping. It advances past events that
123+
were examined and rejected, not only matches. When the cursor cannot be
124+
read, or the watcher first started against an empty log, it replays rather
125+
than jumping to the tip — a restart may cost you a duplicate, never a
126+
missed delegation.
127+
- **Exit codes** are the wake signal: `0` when it printed a match, `2` when
128+
`--idle-exit-ms` expired having seen nothing, `130` when interrupted. Only
129+
`0` means "you have mail" — an interrupted watcher must never claim it.
130+
- **`--follow --json` emits NDJSON**, one record per line, because repeated
131+
indented documents on one stream are not parseable JSON. A single-shot
132+
watch prints one pretty document instead.
133+
- **`--follow`** keeps going after the first match instead of exiting — use
134+
it for daemons; leave it off for harnesses that wake on process exit.
135+
- **A first watch starts at the tip**, matching the SSE live-tail, and says so
136+
on stderr. Replaying a long fleet log would wake you holding weeks of
137+
history with nothing marking it stale. Backlog is the inbox sweep's job;
138+
pass `--from-start` if you really do want the replay.
139+
140+
Notes that save round trips:
141+
142+
- `mempalace_event_wait` defaults to 60s and caps at 5 minutes. On timeout it
143+
returns `{"timed_out": true, "events": []}` — a normal result, not an error.
144+
It already backs off internally (0.25s → 1s); **do not wrap it in a tight
145+
retry loop**. If you find yourself writing the re-arm loop by hand, use
146+
`logstream watch`, which owns that loop and the cursor with it.
147+
- Filter server-side. `to_agent`, `correlation_id`, `type` and `status` are
148+
all indexed filters; fetching 50 events and filtering in your head wastes
149+
tokens and still misses anything past the limit.
150+
- `preview=true` truncates bodies to an excerpt and marks `body_truncated` +
151+
`body_length`, so a sweep over a busy stream stays cheap. Re-fetch the one
152+
event you actually care about with a targeted `correlation_id`.
153+
- `to_agent=<you>` also matches `*` broadcasts automatically. You do not need
154+
a second call for them.
155+
156+
### Announce your watch
157+
158+
Before a coordinated task, post a `status` event to `to_agent=*` declaring
159+
that you are listening, on exactly what, and from where. This is what lets
160+
another agent see who is home *before* delegating, instead of discovering it
161+
by timeout:
162+
163+
```text
164+
type: status room: status to_agent: * correlation_id: <the task>
165+
166+
<AGENT_ID> is MONITORING this correlation for coordination replies
167+
(task.request / task.reply / patch.ready / status).
168+
169+
Watching: to_agent=<AGENT_ID> and correlation_id=<id> on stream project/<name>.
170+
Cursor after: evt_20260811T112013_19320fbd7541
171+
172+
If you are working <overlapping area>, reply on this correlation so we do not
173+
double-work. <What is already done and must not be redone.>
174+
```
175+
176+
The four parts that make it useful: **the filter** (so others know what
177+
reaches you), **the cursor** (so others know what you have already seen),
178+
**the overlap warning** (so others do not duplicate), and **the fact that a
179+
watcher exists at all**.
180+
181+
### Declare when you are *not* watching
182+
183+
A turn-based agent — most chat-driven harnesses — has no background loop. It
184+
sweeps its inbox when a human prompts it and is otherwise deaf. That is a
185+
legitimate mode, but silent deafness is what makes coordination annoying.
186+
187+
If you cannot monitor, say so in your reply and publish your cursor, so the
188+
requester knows a ping is required and knows where you left off:
189+
190+
```text
191+
<AGENT_ID> is NOT monitoring — turn-based, no background watcher.
192+
Last seen: evt_20260820T053821_a5fdd770ec20
193+
Ping the operator to wake me; I sweep to_agent=<AGENT_ID> on every start.
194+
```
195+
196+
Never claim to be monitoring when you are not. A false watcher is worse than
197+
a declared-absent one: the requester stops looking for a human to nudge.
198+
62199
## Hard rules
63200

64201
- **Never apply a patch silently.** Fetching an artifact is free;
@@ -72,6 +209,10 @@ the event trail is only auditable if identities are stable.
72209
artifact and reference it.
73210
- **Close every loop.** Every `task.request` you claimed ends in an
74211
`applied`, `failed`, or `blocked` — no dangling `open` tasks.
212+
- **Never fake a watch.** Declare the monitoring mode you are actually in.
213+
Claiming to listen when you are turn-based strands the requester.
214+
- **Cursors are event ids.** `since_created_at` is a time window, not a
215+
resume point; using it as one drops late-arriving cross-replica events.
75216
- **File the outcome.** When a delegation concludes, write one drawer
76217
(`mempalace_add_drawer`) recording what was decided/learned, so the
77218
result is searchable without replaying the event trail.
@@ -99,8 +240,22 @@ Memory (recall + writing):
99240
100241
Coordination (logstream):
101242
- Check your inbox when starting work and before long tasks:
102-
mempalace_event_list with to_agent=<AGENT_ID> (new since your last
103-
seen event id).
243+
mempalace_event_list with to_agent=<AGENT_ID>, since_event_id=<last
244+
event id you processed>, preview=true. Remember that id — it is your
245+
cursor. Never resume with since_created_at: events are ordered by
246+
append order, so a peer's event can arrive already "older" than a
247+
timestamp cursor and be skipped forever.
248+
- Monitoring: if your harness can run a background process, start
249+
`mempalace logstream watch --agent <AGENT_ID> --state-file <path> --json`
250+
and treat its exit as "you have mail" (exit 0 = match, 2 = idle). Use
251+
--agent, not --to-agent: it also excludes your own events, which
252+
otherwise wake you via the '*' broadcast match. Repeat --type to wake
253+
only for what needs you. In-turn, waiting on one known correlation,
254+
mempalace_event_wait is enough. Before a coordinated task, post a
255+
status event to to_agent=* naming your filter and your cursor so others
256+
know you are listening. If you are turn-based and cannot watch between
257+
prompts, say so and publish your cursor — never claim a watch you do
258+
not have.
104259
- To delegate: mempalace_event_append (type=task.request, stream=
105260
project/<name>, room=delegation, correlation_id=task_..., status=open,
106261
body = goal + branch + base commit + definition of done), then

0 commit comments

Comments
 (0)