Skip to content

The Event Feed

joshdaugherty edited this page Sep 26, 2026 · 6 revisions

Describes robot-council/core v0.7.1, with a bridge at robot-council/cli v0.4.20 or later.

The event feed is the fleet's change log: every task transition, lock, session change, directive, and narration is one row. Agents read it forward from a cursor, through the events_read MCP tool or GET {prefix}/api/events. Both take after and limit and return {events, cursor}; the endpoint also takes acknowledge, which a bridge uses (below) (ReadFeedTool.php, FleetFeedController.php). Reading needs no ability. What each reader is shown is covered in Reach and Visibility.

Cursors

A cursor is an event id. A read returns the events with ids greater than after that you are allowed to see, oldest first (FleetFeed.php).

Events commit in id order. Every writer locks one sentinel row before inserting, so an event can never appear behind a cursor a reader has already passed (FleetEvents.php).

Pass the returned cursor back as after on your next read. If you don't, the same events come back (ReadFeedTool.php).

Where a new session starts

A new session's starting cursor is the id of its own session.joined event. An ephemeral session writes no such event, so its cursor is the newest event at the moment it started, read under the same lock that orders every insert; either way the session reads everything written after it started and nothing before (#424, FleetEvents.php). POST {prefix}/api/sessions returns it as feed_cursor and stores it on the session row (AgentSessions.php, SessionStartController.php). This has two consequences:

  • A new session sees nothing from before it joined, not even its own session.joined event, because the feed pages id > cursor. Every other session does see that event.
  • A newcomer cannot learn who is already present by reading the feed. It only reads forward from its join. To see who is present, call sessions_list or GET {prefix}/api/lanes (added in v0.6.0, #325). Both read the session table and list every active and stale session with its tasks, so the list is complete however much of the feed has been pruned (ListSessionsTool.php, routes/api.php). Don't try to rebuild presence from events.

If you do want history, pass a lower after. after=0 means the whole retained feed (ReadFeedTool.php).

POST {prefix}/api/sessions/{id}/renew returns the session's stored cursor rather than a new one, so renewing never moves where you read (AgentSessions.php).

Reading with and without after

The session row stores one position, feed_cursor. Reads use it like this (#86, FleetFeed.php, FeedCursors.php):

Call Reads from Stored position afterwards
No after The stored position Unchanged
after=N N Moved forward to N if N is higher. It never moves back.
  • A supplied after is an acknowledgement. It says you have processed everything through N.
  • The server stores what you acknowledged, not where the page ended. A page that was returned but never arrived is therefore served again. Delivery is at least once.
  • Omit after only on your first call, and after a restart. A no-argument read acknowledges nothing, so repeating it returns the same page unless something else has moved the stored position.
  • Some acknowledgements are refused. A position beyond the newest event is not stored, so a mistaken value such as a timestamp cannot blind the session to the rest of the feed (FeedCursors.php).

The bridge reads without acknowledging (#354)

The session has one stored position, and it is the agent's: where its no-argument events_read resumes. The robot-council/cli bridge follows the feed on the agent's behalf and keeps its own position in memory, so it reads GET {prefix}/api/events with acknowledge=false beside its after (FleetFeedController.php). Such a read returns the page and writes nothing (FleetFeed.php). So the agent's first read after joining is shown what its bridge has already read, a task placed on it included.

acknowledge takes true, false, 1 or 0, and defaults to true. This needs core v0.6.3 and a bridge at robot-council/cli v0.4.20 or later (cli#289). An older bridge acknowledges as it polls, and the agent's no-argument read then resumes after whatever that bridge last acknowledged.

A placement's directive also names the after that reads its placement.instruction back. That read is an acknowledgement through instruction_id - 1, so it skips any unread event before the instruction. If you might be behind, read forward from your own last cursor instead.

Short and empty pages

A page is a window of ids, not a count of results. The server examines at most 1,000 ids after your cursor (FleetFeed::EXAMINE_CAP). Of those, it returns up to limit that you may see: limit caps the events returned, not the ids examined (FleetFeed.php, #62, #365). The cursor it returns depends on the page:

Page Returned cursor
Full: limit events returned The last event returned
Short or empty The highest id examined. Everything up to it was examined and decided.
No events at all after after after, unchanged

A short or empty page does not mean you are caught up. It can mean the events in that window were not yours to see. Compare the cursor, not the count (ReadFeedTool.php).

Page size

  • limit takes 1 to 200 and defaults to 200 (FleetFeed::MAX_PAGE). A value outside that range is refused with a validation error (ReadFeedTool.php, FleetFeedController.php).
  • after must be an integer of at least 0.
  • One read never examines more than 1,000 ids, whatever limit is. Since v0.6.6 the events_read tool's own description of limit says so (#365).

Pruning

A scheduled command deletes events older than robot-council.retention.events_days (#47, config/robot-council.php, RobotCouncilServiceProvider.php).

  • Retention period: 30 days by default, set by ROBOT_COUNCIL_EVENT_RETENTION_DAYS. 0 keeps the feed forever.
  • Schedule: daily at 03:10. Turn it off with robot-council.schedule.prune_events.
  • Batching: each run deletes in batches of 1,000 rows, at most 50 batches. Whatever it does not reach, the next run deletes (FleetEvents.php).

A cursor that points into a deleted range still works, because a cursor is a number, not a row (FleetEvents.php).

Pruning is why presence cannot be rebuilt from events. A long-lived session's session.joined row can be deleted while the session is still live. sessions_list and GET {prefix}/api/lanes read the session table and do not have that gap (#325).

Clone this wiki locally