Repository navigation
Specs events
This spec covers the event half of a bidirectional team send: the signed event record and its
log, the bus that appends and serves it, the routing policy that says who receives which kind, and
the message's life after a send's outcome (status, thread, reply). The message record itself, the
history line and the admission rules are the messaging spec's; this spec borrows them unchanged.
- event — a signed record in the project's event log, appended by the bus when a durable record changes; it says what happened, when, to which record, and nothing else.
-
event log —
events.jsonlin the project's state folder, sequence-numbered, rotated. -
bus — the process that owns the event log:
team bus, beside the broker, over.agents/bus.sock. - cursor — a subscriber's position in the log: a sequence number.
- gap — the bus's answer when a cursor is older than the oldest event the log keeps.
-
kind — an event's closed-world word (
message.arrived,seat.idle, …). - privileged kind — a kind no seat may subscribe itself to and only the owner routes.
-
status — a message's derived state after the send's outcome:
sent,delivered,read,replied,held. -
thread — the first message's id; a reply carries it and
in_reply_to. - subscription — a seat's registration on the bus: its agent id, its pane, its cursor.
Lives in <project state folder>/events.jsonl (beside messages.jsonl), written by the bus alone;
one JSON line per event, a payload and its signature (Ed25519, the checkout's message key,
domain team.event.v1\n). The log mints monotonically increasing sequence numbers (seq: 1, 2, …), writes atomically, and rotates: retained files events.jsonl, .1, .2, at most 100 MB in
all; retention drops declare themselves with one signed team.event-log-dropped record naming the
dropped sequence range. Fields (src/bus/log.ts):
| Field | Type | Required | Default | Since | Meaning |
|---|---|---|---|---|---|
v |
1 |
yes | — | 0.5.1 | the record's version |
seq |
number | yes | minted | 0.5.1 | the log's sequence number, in order |
ts |
string (ISO-8601) | yes | — | 0.5.1 | when the event was appended |
kind |
event kind | yes | — | 0.5.1 | what happened (the kinds table) |
session |
string | yes | — | 0.5.1 | the team file's session |
record_id |
string | yes | — | 0.5.1 | the durable record the event is derived from |
from |
string | no | absent | 0.5.1 | the sender, on message events |
to |
string | no | absent | 0.5.1 | the recipient, on message events |
seat |
string | no | absent | 0.5.1 | the seat, on seat events |
subject |
string | no | absent | 0.5.1 | the subject seat, where a table row names one |
thread |
string | no | absent | 0.5.1 | the thread the record belongs to |
via |
string | no | absent | 0.5.1 | the leg a delivery went by |
reason |
string | no | absent | 0.5.1 | a short reason (retention on a retention drop) |
prompt |
string | no | absent | 0.5.1 | the prompt's kind on seat.at-prompt
|
rule |
string | no | absent | 0.5.1 | the rule named, on a refusal |
dropped_oldest_seq |
number | no | absent | 0.5.1 | on team.event-log-dropped: the oldest sequence retention has dropped |
dropped_newest_seq |
number | no | absent | 0.5.1 | on team.event-log-dropped: the newest sequence the pass dropped |
An event carries no body and no instruction.
The message record is the messaging spec's; at the tip it carries from, from_id, to, to_id,
rule, thread and in_reply_to beside the v0.5.0 fields, and its status is derived, never
edited, from its receipts in the event log's order, moving forward only (src/commands/messages.ts).
The status words:
| Status | Meaning |
|---|---|
sent |
the record exists and a transport took it |
delivered |
the transport confirmed the seat's program has it (the inbox fetched it, the drop watcher receipted it, or the typed outcome was delivered) |
read |
the seat's own tool showed the body |
replied |
a record on the thread answers it |
held |
the record waits: a dialog, unsent text in the box, or an unreachable seat; the side and reason named |
held is one word on two records: an attempt's outcome, and the message's status while no later
attempt has. not-submitted, timeout, unreachable and refused stay outcomes of an attempt,
never statuses.
Closed-world (src/bus/kinds.ts, EVENT_KINDS): thirty kinds at the pinned ref, each cited to
its line. Default recipients: "lead" is the seat whose role holds leads; "operator" is the team
file's operator seat; "sender" and "subject" are the records' own fields; the routing policy's
defaults are the table.
| Kind | Raised when | Default recipients | Source (src/bus/kinds.ts at 8acb470b) |
|---|---|---|---|
message.arrived |
a record is written for a seat | the recipient (this is the delivery) | line 57 |
message.delivered |
a receipt is appended | the sender | line 58 |
message.read |
a receipt is appended | the sender | line 59 |
message.replied |
a receipt is appended | the sender | line 60 |
message.held |
a record waits with a reason | the sender, and the lead after held-threshold
|
line 61 |
seat.working |
the seat state changes to working
|
nobody: the bus and the watch read it; a lead that wants it adds itself | line 63 |
seat.idle |
the seat state changes to idle
|
nobody, as seat.working
|
line 64 |
seat.idle-timeout |
a seat has been idle past idle-threshold with no --wait open on it and no lease held |
the lead | line 65 |
seat.blocked |
the state reads blocked, with the prompt's kind |
the lead and the operator | line 66 |
seat.at-prompt |
the seat waits at a prompt, with the prompt's kind | the lead and the operator; the owner for a permission, trust, login or vendor prompt | line 67 |
seat.unreachable |
the seat cannot be reached | the lead and the operator; the owner for the lead's own seat | line 68 |
seat.done |
the process left its pane | the lead and the operator; the owner for the lead's own seat | line 69 |
work.claimed |
a lease is taken | the lead; the task's author where the record names one | line 71 |
work.done |
a lease is finished | the lead; the task's author where the record names one | line 72 |
work.released |
a lease is let go | the lead; the task's author where the record names one | line 73 |
work.gate |
a gate stamp is written, green or red | the lead and the branch's author; a red on the integration tip to every seat holding merge on it, and to the operator |
line 74 |
work.suite |
a suite report is written, green or red | as work.gate
|
line 75 |
team.seat-added |
the roster changes | the lead, and the owner | line 77 |
team.seat-removed |
the roster changes | the lead, and the owner | line 78 |
team.seat-reloaded |
the roster changes | the lead, and the owner | line 79 |
team.grant-issued |
a grant record is written | the lead, the subject seat, and the owner | line 80 |
team.grant-revoked |
a grant record is revoked | the lead, the subject seat, and the owner | line 81 |
team.grant-expired |
a grant record passes its expiry | the lead, the subject seat, and the owner | line 82 |
team.policy-changed |
a policy value is set at any level | the lead, the subject seat, and the owner | line 83 |
team.policy-refused |
a routing change is refused, naming the rule, the requester and the kind | the owner, the implementation's default; the accepted amendment names the lead and the owner (§7) | line 84 |
release.ready |
the lead's own words to the owner (team raise): a release is ready |
the owner | line 87 |
decision.needed |
the lead's own words to the owner (team raise): a decision is needed |
the owner | line 88 |
record.invalid |
a record failed its check | the owner | line 90 |
security.refused |
a guarded act was refused | the owner | line 91 |
team.event-log-dropped |
retention dropped a sequence range | declared in the log itself | line 94 |
The prompt kinds on seat.at-prompt whose default routing includes the owner: permission,
trust, login, vendor notice, rate limit (OWNER_PROMPT_KINDS).
team bus
Runs the event bus for this clone over .agents/bus.sock, appending signed event records to the
project's event log and reconciling durable message and seat records. Only the owner runs it, from
a terminal outside the pane manager. Ctrl-C stops it. Exit codes: bus.started (0), bus.stopped
(0), bus.refused (1), bus.configuration / bus.usage (2).
The bus's two operations on the socket: subscribe (a seat by registry id, its pane, a cursor — a
sequence number) and publish (a runtime tool handing it a record that changed). Each subscriber
gets a bounded buffer; one that was away reads from its cursor. When records between the cursor and
the oldest retained sequence are missing, the answer is gap with oldest_seq, and the subscriber
resyncs from the underlying records before following on.
team reply <id> [<text>] [--close] [--json]
team reply <id> --close
Replies to the sender of the message record <id>, or closes its thread. A reply is a signed
record in the sender's folder, on the same thread; it is admitted while the thread is open.
--close ends the thread, and a later reply opens a new one that cites the closed thread's last
record. Exit codes: reply.replied (0), reply.refused (1), reply.configuration /
reply.usage (2).
The launch installs an MCP server in a Claude seat's session (src/mcp/inbox.ts) that holds a
subscription on the bus. A message or event for the seat arrives as a notice on the next tool
round — a count and a cursor, one line per tool round however many arrived, never one line per
event. The seat's own fetch is the delivered receipt; reading a body with the inbox tool is the
read receipt, shown as a message from the seat named on the record after the signature check; the
reply tool writes the reply record. Nothing is typed into the pane.
DELIVERY_LEGS at the tip: native, mcp-inbox, typed, drop. The fallback order
(src/settings/inherit.ts): mcp-inbox, then drop, then typed.
-
mcp-inboxtakes a send only while the seat's own inbox server is listening, and writes the message to that inbox without typing; otherwise it is skipped asno live inbox listener. -
dropwrites the same 0600 file and types nothing at all: a receiver that watches its folder — the inbox server where its CLI has an MCP front, the launch's start-of-turn read where it has a hook — takes the message there, and a seat that watches nothing never walks this leg. -
typedstays the last fallback. -
nativeis not in the default: a level that names it is the sender choosing its own cross-session channel and recording the delivery itself; it is the one leg a declared word may add.
- The bus alone appends. An event is appended by the bus when a durable record changes: a message's status, a seat's state, a piece of work, the roster, a grant, a policy. (Proof: the bus log tests at the tip.)
- The record is the truth; the event is derived. A subscriber applies events in sequence and drops one it has seen, so a duplicate or a reordered delivery changes no state; whoever doubts an event reads the record it names.
-
A stale cursor is a
gap, never silent loss. Whencursor + 1 < oldestSeq, the bus answersgapwitholdest_seq; the subscriber resyncs from the records (its messages folder, the seat states, the leases, the file) before following on. A retention drop declares itself with a signedteam.event-log-droppedrecord naming the range. (src/bus/log.ts,readEventsFromCursor.) -
The reconcile pass re-raises. At the bus's start and every
reconcile-interval, the bus lists the durable records with no event (a message record with noarrived, a receipt with no event, a seat state newer than its last event) and raises them: a tool that crashed between its record and its publish loses time, never the event. (src/bus/reconcile.ts.) -
Routing is a policy.
event-routing, in the policy catalogue's form. Scope: entity, project, team, seat. Values: a map from event kind to recipients, each a position (lead,operator,sender,subject,owner) or a seat by registry id. Default: the kinds table. Set by: the owner at any scope; a holder named by grant at project or team for non-privileged kinds; a seat for itself only by adding itself, never to a privileged kind. (src/bus/routing.ts,src/policy/events.ts.) -
No self-subscription to a privileged kind. The built-in privileged kinds are
seat.at-prompt,team.grant-issued,team.grant-revoked,team.grant-expired,team.policy-changed,team.policy-refused,record.invalidandsecurity.refused(DEFAULT_PRIVILEGED_KINDS); the owner may grow the set throughprivileged-kinds, and no level below the owner may remove any of them or self-subscribe to one. The default recipients of a privileged kind stand at every level. A seat that is a default recipient is not self-subscribing: the ban is on the seat's own act. -
A refusal is visible. The bus refuses the write, names the rule
privileged-self-subscriptionwith the requester and the kind, appends a refusal record to the operational log, and raisesteam.policy-refusedto the owner — the implementation's default at the pinned ref (src/policy/events.ts, used bysrc/bus/routing.ts). The accepted amendment names the lead and the owner; §7 keeps both positions for reconciliation. (PRIVILEGED_SELF_SUBSCRIPTION_RULE,src/bus/refusals.ts.) -
Tighter is additive for recipients, shorter-only for thresholds. A level may add recipients
among its own seats and remove none a level above set;
held-threshold,unreachable-thresholdandidle-thresholdtighten shorter only. The event log's retention has a default (DEFAULT_EVENT_RETENTION_SECONDS, derived from the thresholds' retention days). -
The owner is reached by the runtime, never through a seat. What the policy routes to
owneris delivered on the machine as a notification and in the owner inbox, with no seat in the path; a connector is one more delivery beside those, never the only one and never a gate. (src/bus/owner.ts.) -
Admission is checked when each record is written, replies included. The thread adds one
rule,
reply-on-open-thread: a reply to the sender of a record on an open thread is admitted while the thread is open, neither seat has been removed, and no revocation or deny applies to the pair; the rule is named on the record, and the owner may switch it off per team. A thread admits nothing by itself. - No typing into a pane a human types in. A typed line is refused for the owner's own seat and for any pane the owner launched by hand; the leg is recorded unavailable with the reason and the next leg runs.
-
Retry without blindness. A held record is retried when the seat next subscribes or its
state turns to
workingoridle(inbox), at the watcher's next pass (drop), or only onseat.idle(typed); several held records for one seat go as one wake line naming them all, or one per idle turn in order. A fall-through happens only on a definite failure before the send went through, never on a missing acknowledgement after one. A duplicate id is dropped by the receiver; a doubled receipt is dropped by its sequence. - An event is not a message. It carries no body and instructs nobody; the inbox verifies the signature before it shows a body, and an event that wakes a seat says something waits, not what to do. A subscription is not a capability.
-
event-kinds (registries/event-kinds.md): the kinds table of §3 (thirty kinds), plus the
prompt kinds
permission,trust,login,vendor notice,rate limitand the built-in privileged kinds of rule 6 — all fromsrc/bus/kinds.tsat the tip. -
delivery-legs (registries/delivery-legs.md):
drop| the record written whole, nothing typed, the seat told by watching its folder | built at int/0.5.1 8acb470b, unreleased |src/settings/inherit.ts(DELIVERY_LEGS) | needs no bus. -
policy-names (registries/policy-names.md):
event-routing(enforced by the bus),privileged-kinds(owner-sovereign, grows only). -
exit-codes (registries/exit-codes.json's rows):
bus.started,bus.stopped,bus.refused,bus.configuration,bus.usage;reply.replied,reply.refused,reply.configuration,reply.usage.
-
The build holds thirty kinds; the accepted amendment's table as written names twenty-five. The
amendment's prose counts twenty-four and its §4.3 table adds
team.policy-refused, so the table names twenty-five. The build at int/0.5.1 8acb470b adds five beyond that table —release.ready(line 87),decision.needed(line 88),record.invalid(line 90),security.refused(line 91) andteam.event-log-dropped(line 94) — or six against the prose's twenty-four, countingteam.policy-refused(line 84). The count to carry is thirty; the spec's §3 lists them all, each with its source line. -
team.policy-refused's audience. The build's default is the owner alone (src/policy/events.tsat int/0.5.1 8acb470b;src/bus/routing.tsuses it for the refusal record), while the accepted amendment names the lead and the owner. Both positions are kept here: the code defines Today, and the amendment's lead-and-owner requirement stands for reconciliation. - Owner question 5 is settled (OD-20261018-05, under the owner's delegation, revisable by the
owner): at once, only a blocker or a decision that needs the owner, a release ready for his go,
and a security refusal or a forged or invalid record; the rest batched in the lead's summary. The
same word settles which kinds
privileged-kindsgrows to. - The status words are derived in the messages command;
thread-ttlandthread-capare named by the text, and their defaults are the build's tuning. - The watch is a subscriber first and a poller second; which of its reads stay polls (the screen, the machine, the pane manager's own word) is the build's choice, measured before claimed.
- Which CLIs take a drop watcher is measured in the build before the leg's default order is claimed for them.
- Draft (the writer, 2026-10-18): lifted from the send-events amendment's v2 draft; read at int/0.5.1 (8acb470b).
- Folded (the writer, 2026-10-19): owner question 5 recorded as settled by OD-20261018-05 (the blocker or decision, the release ready, the security or invalid record at once; the rest batched), matching the amendment's v2 fold.
-
Fix round 1 (the writer, 2026-10-18, on the architect's line): §3's kinds table is one row per
kind, all thirty, each with its source line in
src/bus/kinds.tsat the pinned ref; §7 opens with the amendment/build count;dropis "built at int/0.5.1 8acb470b, unreleased" in the delivery-legs entry. -
Count resolved (the writer, 2026-10-19): the §7 marker reconciled from
src/bus/kinds.tsat int/0.5.1 8acb470b — thirty kinds; five beyond the accepted table as written (twenty-five), six against its prose (twenty-four). -
Fold after the pre-round reviews (the writer, 2026-10-19):
team.policy-refusednames the build's owner-only default in §3 and rule 7, with the accepted amendment's lead-and-owner audience kept separately for reconciliation (§7).
Generated from docs/specs/events.md at v0.5.2 (1d1224a4) — edit the repository, not this page.