Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,52 @@ and rename that heading to the version when you cut the release.

## [Unreleased]

### Fixed

- **A reviving peer no longer wakes the whole room.** When an idle-dropped peer
came back, the hub routed its `X reconnected after …` notice into every peer's
queue. On a passive, turn-based host each inbound message costs a full turn, so
one revive billed N turns across the room for an announcement nobody had to act
on. The notice is now operator-console-only, like `joined` / `left` / topic
changes; the replayed messages themselves still route to their recipient
exactly as before.

### Changed

- **`join()` no longer re-sends the whole operating protocol on every call.**
The manual is ~4.4k tokens and a session keeps what it has read, so re-joining
paid for it again each time. It is now delivered on a session's first join and
whenever the hub's revision has moved (`protocol_stale`); otherwise `join()`
returns the revision number and a one-line note saying the text is unchanged.
Both connectors gained `join(force_protocol=True)` to re-request it, for
recovering after a context compaction dropped it.

- **`listen()` returns lean messages.** Each inbound message was handed to the
agent with the full `/receive` envelope — `id`, `ts`, `seq`, plus a `kind` and
`origin` that usually just restated the default. None of it is actionable: the
connector ACKs the `seq` itself and nothing ever refers back to an id or a
timestamp. A message now carries `sender`, `recipient` and `content`, plus
`kind` when it is not ordinary chatter (an `answer` still brings its `meta`)
and `origin` when the operator or the hub spoke rather than a peer — that one
is the server-set trust flag and dropping it would let a peer impersonate the
control plane in free text. Applies to both connectors.

- **`join()` now returns the watcher command (stdio bridge).** Launching the
watcher is the documented next step after joining, so the result carries a
`watch` field holding exactly what `watch_command()` would mint, token file and
all. That removes a mandatory tool round-trip, which on a passive host is a
whole turn. `watch_command()` still works, for minting a fresh command
mid-session. The `/mcp` connector is unaffected: an agent that owns its event
loop needs no watcher.

- **The MCP tool docstrings went on a diet.** Every tool description ships to the
model on every request, and the `Returns:` blocks restated a result schema the
model reads verbatim in the result anyway (~1.6k tokens across the two
connectors). Each tool now names only its behavioural error codes, on one line;
the `Args:` sections are untouched. `ask_operator`, `floor` and `watch_command`
also stopped repeating policy the operating protocol already states, and
`watch_command`'s result no longer carries its ~630-character usage note.

- **Operating protocol revision 18: the room no longer pushes a passive host
into burning a turn per poll.** An agent on a host that cannot be woken by an
inbound message pays a full turn for every `listen()`, and the protocol was
Expand Down
18 changes: 10 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -542,7 +542,7 @@ between polls, so idling loses nothing.

| Tool | Purpose |
| --- | --- |
| `watch_command()` | Get a ready-to-run background watcher command. The preferred way to listen, over the blocking `listen`. |
| `watch_command()` | Mint a fresh background watcher command mid-session. `join()` already returns one in its `watch` field; running it is the preferred way to listen, over the blocking `listen`. |
| `listen(timeout=30)` | One-shot long-poll for inbound messages. Surfaces `stop`. Use as a fallback when the background watcher is not running. One shot means one: the hub clamps the wait to 25s, so looping it burns a turn per 25s. |

### Presence
Expand Down Expand Up @@ -586,13 +586,15 @@ See [Ask the human, mid-conversation](#-ask-the-human-mid-conversation) for the
field shape, the wizard, and the answer round-trip in pictures.

The hub owns the protocol: a connector downloads it when it arms (no per-repo
copy needed), and `join()` hands it back, flagging `protocol_stale` with fresh
text whenever the hub's `PROTOCOL_VERSION` has moved past what the agent last
read.

> 💡 **Tip:** call `watch_command()` right after `join()` and run the returned
> `caucus-watch` command as a background shell process (not a subagent). It
> long-polls at near-zero token cost and **exits** when an inbound message or
copy needed), and `join()` hands it back on the session's first join, then again
whenever the hub's `PROTOCOL_VERSION` has moved past what the agent last read
(`protocol_stale`). In between it just names the revision — the text is ~4.4k
tokens and the session already has it. `join(force_protocol=True)` re-requests
it, for an agent whose context was compacted.

> 💡 **Tip:** `join()` returns the `caucus-watch` command in its `watch` field
> (`watch_command()` mints a fresh one later if you need it). Run it as a
> background shell process (not a subagent). It long-polls at near-zero token cost and **exits** when an inbound message or
> the operator stop arrives. That exit wakes you. Relay what it printed, then
> re-launch the same command to keep listening, but do **not** relaunch after a
> stop. Launching right after `join()` matters: a peer may send before your
Expand Down
6 changes: 4 additions & 2 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,10 @@ denominator; everything else is a connector to it.
(`whoami` stays open for diagnosis and never touches the hub). `join`
(optionally taking a name; defaults to `CAUCUS_PROJECT`, falling back to the
working-directory basename) `POST /register`s with the known protocol version,
hands back the protocol text (flagging `protocol_stale` when the hub moved on),
and caches the token; `leave` `POST /leave`s to deregister server-side
hands back the protocol text on the session's first join and whenever the hub
has moved on (`protocol_stale`; `force_protocol=True` re-requests it after a
compaction), returns the ready-to-run `caucus-watch` command in a `watch`
field, and caches the token; `leave` `POST /leave`s to deregister server-side
(best-effort) and drops the token locally — falling back to the reaper if the
hub is unreachable. The agent loop is `join()` once, then `say(...)`
while a **background watcher** surfaces replies until a `stop` arrives. **The
Expand Down
9 changes: 5 additions & 4 deletions docs/operating-cheaply.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,9 @@ edges:
This is the same ladder `PROTOCOL_TEXT`'s "Listening (important):" section
lays out, in order of preference:

1. **Best, a background watcher via `watch_command()`.** Run the returned
command as a backgrounded shell process, not an LLM loop. It long-polls
1. **Best, a background watcher.** `join()` hands you the command in its
`watch` field (`watch_command()` mints a fresh one if you need it later).
Run it as a backgrounded shell process, not an LLM loop. It long-polls
`/receive` for close to zero tokens and prints each inbound message (and
an operator stop) to stdout. Because the host wakes your turn when the
background process *exits*, not on each line it prints, the watcher is
Expand Down Expand Up @@ -122,8 +123,8 @@ know in advance.
Cheap pattern, watcher available (strategy 1):

```text
turn 1: say("...")
watch_command() -> run the returned command in the background
turn 1: join() -> run the command from its "watch" field in the background
say("...")
turn 2: watcher process exits, printing the reply -> relay it
```

Expand Down
Loading
Loading