Reason Network, Beacon and the hive: a technical overview

Two recent projects each solve half of a problem that teams running coding agents now have. Reason Network (Reason, Rust, version 0.12.0) connects the agent sessions on a team's machines into a live directory and delivers messages into running sessions, with no server in between. Beacon (Asymptote Labs, Go, version 1.3.45) records what agents do across more than 20 harnesses in one normalized local log and turns reviewed traces into lessons that later sessions can recall.

Parts 1 and 2 describe each system from its code, with a file and line for every statement, and each ends with what the system does not do and where its documentation and code disagree. Part 3 designs a system that combines the two with Freyja's always-on agent: one daemon per machine, a team context graph, memory that travels as signed lessons, and an always-on member that keeps obligations, prepares work while people are away and decides when to interrupt them. Its last design section covers the work between people that a personal agent cannot see: changes that reach other repositories, handoffs nobody picked up, agents stuck on solved problems, and autonomy earned from a record. A companion piece, hive-vision.html, presents the same design as an illustrated vision with live animations.

All three parts were written on 2026-10-21 against Reason Network at 0.12.0 and Beacon at commit 99c70908. Where a repository's documentation and its code disagree, the code wins and the disagreement is listed.

Contents

Part 1: Reason Network, how it works and where it stops

Reason Network is a Rust daemon, one per computer, that lets coding agents on different teammates' machines see each other and send each other messages without a central server. Lars Talian at Reason wrote it. It started on 2026-10-11 as a commit titled "comms", was renamed tincan the same day, shipped as Reason Network v0.11.0 on 2026-10-20, and was at 0.12.0 later that day (git log; Cargo.toml:3). Of 190 commits, 165 are Lars's. The crate has about 14.9K lines of Rust under src/, about 12K of them outside the inline #[cfg(test)] modules, plus a 143-line opencode plugin and a 71-line pi extension in TypeScript.

Every statement below cites the file and line I read. "DESIGN" means DESIGN.md in the repo. Where DESIGN and the code disagree I follow the code and list the disagreement at the end.

1. The problem it names

DESIGN keeps its case for novelty narrow. Around 15 projects from 2026 let agents message each other, but almost all of them are single-machine, single-person or Claude-only (DESIGN.md:15-16). The gap it targets is "a team directory of live sessions with context (person, machine, agent, repo, branch, busy/idle, what it is doing), so an agent can pick the right recipient instead of broadcasting and hoping. That is the core of this project; messaging is the easy part" (DESIGN.md:23-25).

That framing explains most of the design. The directory is live and derived (presence, git state, transcript tails), not curated. The message path is deliberately thin: one envelope, one recipient, an ack, an outbox. A large share of the code goes into the hard part DESIGN names, "Getting messages into agents" (DESIGN.md:218-220), meaning per-harness injection.

Status as documented: milestones M0 to M3 are built and tested on macOS with two machines over iroh and real Claude Code and Codex sessions; M4 is not started (DESIGN.md:10-11).

2. Team model

team ──< person ──< machine ──< session

DESIGN defines four nouns (DESIGN.md:29-40):

The daemon takes its own person and machine name from the signed roster, not from the config file, once it is in a team (src/daemon/mod.rs:181-185). The directory does the same for teammates: when it lists a peer's sessions it overwrites person, machine and address with what the roster says about that endpoint, ignoring what the peer's presence reported (src/daemon/directory.rs:240-245).

Why a person is not an address. If an agent sends to a bare person name, the resolver refuses and lists that person's live sessions instead: "messages go to agent sessions; alice's are: …" (src/daemon/directory.rs:395-407). The only exception is a pop-up: with notify, an agent may address its own user, which shows a desktop notification on each of that user's online machines and delivers nothing to any session (src/daemon/mail.rs:130-169). A receiving machine accepts a session-less message only if it comes from one of the recipient's own machines (src/daemon/mail.rs:482-489). The code gives no explicit reason. The effect is that every message lands in a conversation that has context, and that a person's attention is reached only through their own agent or their own machine.

3. Addressing and naming

Address Resolves to Source
alice/api-cursor one session src/daemon/directory.rs:382-394
alice/api-cursor@xps that session on machine xps src/daemon/directory.rs:345-351
api-cursor a session, if the name is unique in the team src/daemon/directory.rs:408-409
alice/* every live session of alice's src/daemon/directory.rs:386-388
repo:acme/api, repo:api every live session whose repo equals it or ends in /api src/daemon/directory.rs:374-381
* every live session src/daemon/directory.rs:372-373

When several sessions share a name the resolver prefers the live one; if more than one is live it fails with "ambiguous" and lists person/name@machine options (src/daemon/directory.rs:417-432). A group never includes the sender's own session (src/daemon/directory.rs:352). Resolution can also target offline sessions, both local ones remembered in SQLite and remote ones a peer lists under known, so mail can queue for them (src/daemon/directory.rs:298-331).

Names. A session's default name is <repo-root-or-dir>-<agent>, slugged (src/agent.rs:181-186), with -2, -3 on collision (src/daemon/sessions.rs:312-331). Names are unique across all of one person's machines, because pick_name treats names in use on the person's other machines as taken (src/daemon/sessions.rs:92-101). A session cannot take the person's own name (src/daemon/sessions.rs:112).

Inheritance across restarts. pick_name first reuses the name previously stored for the same session key; failing that it takes the default base name if no live session holds it (src/daemon/sessions.rs:314-329). Because mail is stored by address (person/name), a new session that takes a stale name immediately sees that name's unread mail: registration reads pending(&address) and delivers anything with wake set (src/daemon/sessions.rs:84-89, src/daemon/sessions.rs:26-33). DESIGN states the purpose: "tell the api agent X" survives restarts (DESIGN.md:56-58). Renaming a session moves its unread mail to the new address (src/store.rs:369-378).

Group guard. The daemon refuses a group address from any sender whose address does not end in /terminal: "agents message one session, not a group: pick it with list_team, or ask your user" (src/daemon/mail.rs:126-129). A receiving machine also refuses a teammate's agent's group message, which only an older build would send (src/daemon/mail.rs:518-523). The CLI decides whether a human is at the terminal by checking whether stderr is a TTY; a script must pass --from <you>/terminal explicitly (src/main.rs:417-423). DESIGN acknowledges that the guard does not stop an agent from messaging a person's sessions one by one, which the loop guard and inbound policy are meant to cover (DESIGN.md:316-320).

4. Membership: the signed roster

The roster is a small replicated state in which every entry is signed by a member machine's key. The module header sets the trust assumption in one sentence: "Built for a team of friends who trust each other, not adversarial members" (src/team.rs:1-3).

pub struct Signed<T> { pub body: T, pub by: EndpointId, pub sig: String }          // src/team.rs:18-23

pub struct TeamState {                                                             // src/team.rs:88-103
    pub id: String,                       // 16 random bytes, hex
    pub founder: EndpointId,
    pub meta: Signed<Meta>,               // team name, optional relay, ts
    pub admissions: BTreeMap<String, Signed<Member>>,   // kept forever
    pub members: BTreeMap<String, Signed<Member>>,      // latest entry per machine, LWW
    pub invites: BTreeMap<String, Signed<Invite>>,      // keyed by blake3(secret)
    pub link: Option<Signed<Link>>,       // the reusable team-link secret
    pub attestations: BTreeMap<String, String>,         // endpoint -> sign-in JWS, unsigned by members
}

A signature covers the bytes "tincan/1/{team}/{kind}/" followed by the entry's JSON (src/team.rs:25-30), so an entry cannot be replayed into another team or reinterpreted as another kind.

Admissions. A machine counts as admitted only if its admission chain reaches the founder: the founder's self-signed entry is depth 0 and every other admission must be signed by an already-admitted machine (src/team.rs:262-283). Admissions are never removed, "so who may sign entries never depends on the order changes arrive in" (src/team.rs:84-87). Merging repeats over incoming admissions until nothing new verifies (src/team.rs:407-430).

Members and last-writer-wins. Member entries merge by (ts, sig), so the signature breaks timestamp ties deterministically (src/team.rs:112-115). Merge processes entries in admission-depth order so a person's first machine holds their name before a later machine is checked against it (src/team.rs:433-436). Three rules then apply (src/team.rs:437-459): a machine may sign its own departure but never its own return (the founder's first entry aside); a machine cannot take a name another active machine holds unless one of that person's machines vouches for it or a Reason sign-in proof ties it to the same account (src/team.rs:184-190); and only names made of a-z, 0-9 and -, at most 64 bytes, are accepted (src/team.rs:125-130).

Clock skew. Any entry stamped more than a day in the future is refused (MAX_SKEW_MS = 86_400_000, src/team.rs:109-123). Timestamps are otherwise trusted.

Invites, links and tickets. An invite stores only blake3(secret) (src/team.rs:63-75, src/team.rs:525-527). One-time invites are marked used_by when redeemed; the team link's invite is reusable and works until replaced (src/team.rs:383-386). An invite dies with the machine that made it (src/team.rs:349-351), and invites that expired over a week ago are pruned (src/team.rs:518-522). Removing anyone replaces the team link (src/daemon/teamops.rs:550-551), and a machine the state marks as removed is sent the roster without the link or any invite (src/net.rs:291-299). The link's ticket packs the team id, the secret, the team name, relay hints, up to two private LAN addresses, and the keys of this machine plus up to two others, preferring online ones (src/team.rs:569-609, src/daemon/teamops.rs:413-430). Four-word invite codes (about 41 bits) instead derive a temporary iroh key that the inviting machine listens on (src/invite.rs:1-4); DESIGN explains that a sign-in never joins with a code, because a code cannot show which team answers it (DESIGN.md:137-140).

Departures. remove signs an inactive entry for the target machines, makes a new link, broadcasts, and drops their connections after two seconds so the news arrives first (src/daemon/teamops.rs:518-564, src/net.rs:486-495). leave refuses unless some teammate is online to hear it, unless forced (src/daemon/teamops.rs:566-574). A machine that learns it was removed clears its team state, deletes saved presence, checkpoints the SQLite WAL so the old link is not left on disk, and restarts (src/daemon/teamops.rs:137-145).

Gate. An incoming connection on the team ALPN from a non-member is closed with code 403. A machine that was a member but has been removed first gets the secret-free state, so it learns why; strangers learn nothing (src/net.rs:328-340). Every frame on a live link is rechecked for membership (src/net.rs:709-711).

DESIGN states what the trust assumption costs: a removed machine "can no longer connect, but it is not made cryptographically powerless: this is for friends" (DESIGN.md:124-126). Any member may invite and remove anyone (DESIGN.md:124); there are no roles.

5. Sign-in proofs

A Reason sign-in produces a compact JWS stating that an endpoint id belongs to a Reason account and the name that account uses (src/proof.rs:1-4). Verification is local: RS256 only, never none or HS256 (src/proof.rs:177-180), against public keys compiled into the binary (src/proof.rs:37-41), with issuer https://reasonmachines.com/network, audience reason-network and type reason-network-proof (src/proof.rs:12-14). Proofs over 4 KB, keys under 2048 bits and proofs issued more than a day ahead are refused (src/proof.rs:19-23). The token's fields are sub, name (1 to 40 of a-z0-9-), email, endpoint, iat, exp (src/proof.rs:45-56, src/proof.rs:151-156). Debug builds also trust keys in REASON_NETWORK_TRUST_KEYS; release builds do not (src/proof.rs:101-120).

Proofs ride in attestations, outside the signed entries, so older builds pass them on untouched, and every machine verifies each one itself (src/team.rs:99-102). The code distinguishes two checks (src/proof.rs:123-133):

For the same reason, the comment on KEYS says never to remove a signing key, even a retired one, since a build without it would drop machines every other build keeps (src/proof.rs:28-30).

6. Transport over iroh

DESIGN chose Rust because iroh 1.x (QUIC dialed by public key, hole punching, HTTPS relays, mDNS) and rmcp (the official MCP SDK) both exist there (DESIGN.md:85-86). The machine key lives in ~/.reason/network/machine.key, mode 0600 (src/daemon/mod.rs:161-172).

Binding. Without a team relay, the endpoint uses iroh's N0 preset (n0 DNS address lookup) with Reason's relays and, if lan is on, mDNS (src/net.rs:140-150). With a team relay it uses the Minimal preset and only that relay, and strips relay URLs that LAN neighbours advertise so a stale teammate cannot pull it onto n0's relays (src/net.rs:151-193). DESIGN summarises the result: such a team's machines "never contact n0" (DESIGN.md:108-112). Setting a relay first checks it is reachable within 10 seconds (src/net.rs:162-172).

Reason's relays. The list comes from https://reasonmachines.com/network/relays.json, signed with the proof key under its own type and issuer, at most 64 KiB and 16 relays, refreshed every 6 hours (src/relays.rs:24-38). Each machine homes on the nearest relay, and peers reach it through that home relay, since relays do not talk to each other (src/relays.rs:1-3). The compiled fallback is a single relay, relay-sfo1.compute.reasonmachines.com (src/net.rs:39-41). Dialing adds every configured relay URL to the peer address (src/net.rs:563-569).

Wire protocol. The team protocol uses ALPN tincan/1 and joins use tincan/join/1, both kept from the old name because "never change them" (src/net.rs:31-35). Each connection carries one bidirectional stream of frames: a 4-byte big-endian length, then JSON, with frames over 4 MiB refused (src/net.rs:199-216).

#[serde(tag = "t", rename_all = "snake_case")]                       // src/net.rs:47-55
pub enum Wire {
    Hello { team: String, proto: u32 },
    State { state: Box<TeamState> },
    Presence { presence: Presence },     // { sessions: Vec<SessionInfo>, known: Vec<String>, features: Vec<String> }
    Msg { msg: Message },
    Ack { id: String, state: String, note: String },
}
Frame When it is sent What the receiver does
Hello first, on every new link (src/net.rs:670) drops the link if the team id differs; logs a protocol mismatch (src/net.rs:713-720)
State on connect, and rebroadcast whenever a merge changed something (src/net.rs:671-673, src/daemon/teamops.rs:130-134) merge_state (src/net.rs:721)
Presence on connect, then from the presence loop (src/net.rs:674, src/net.rs:607-622) tidy_presence, then store (src/net.rs:722-726)
Msg per message, plus outbox replays (src/net.rs:675-677, src/net.rs:623-632) receive, then always answers with Ack (src/net.rs:727-734)
Ack in reply to each Msg clears the outbox entry unless the state is retry; wakes the waiting sender (src/net.rs:735-743)

Mesh and dialing. Each daemon dials every active member it is not connected to, every 2 seconds, with per-peer backoff doubling from 2 to 60 seconds and a 20-second connect timeout (src/net.rs:536-584). When both sides dial at once, both keep the connection dialed by the smaller endpoint id; a redial after the old link is more than 5 seconds old replaces it, on the reasoning that the peer lost its end (src/net.rs:644-657, src/net.rs:749-755). DESIGN calls the topology a full mesh, "fine to ~50 machines" (DESIGN.md:164).

Presence cadence. The presence loop wakes every 2 seconds or when a local session comes or goes, rebuilds this machine's presence, and broadcasts it if it changed or if 60 seconds passed. Every 30 seconds it also resends outbox messages older than 20 seconds to connected peers (src/net.rs:586-635). Rebuilding presence is not free: it scans Claude Code's session files, runs (cached) git lookups and tails transcripts for doing (src/daemon/directory.rs:192-210, src/daemon/directory.rs:54-126). Incoming presence is sanitised: at most 200 sessions and 100 known names, names slugged to 64 characters, other fields cut to one printable line (src/net.rs:757-782). By default a session's cwd is cut to its last component before it leaves the machine (src/daemon/directory.rs:194-199).

Sending. send_msg puts a Msg on the link and waits up to 5 seconds for the Ack; on timeout the message simply stays in the outbox (src/net.rs:517-534, src/net.rs:36). The outbox gives up on a message after 7 days (src/store.rs:321-327).

7. The local daemon and its request protocol

 agent ── reason-network mcp ──┐                         ┌──────────────────────────┐
 agent ── reason-network hook ─┼── NDJSON over socket ──▶│ daemon: sessions, roster, │◀── iroh QUIC ──▶ teammates
 you   ── reason-network CLI ──┘   (0600 / named pipe)   │ directory, mail, SQLite   │     (tincan/1)
                                                          └──────────────────────────┘

One daemon runs per state folder, enforced by a lock file (src/daemon/mod.rs:41-54). Clients talk to it with one JSON object per line over ~/.reason/network/network.sock (src/paths.rs:63-80), chmodded to 0600 (src/ipc.rs:60), or a named pipe on Windows (src/paths.rs:81-95). Each request runs on its own task so a long-poll cannot block the connection (src/daemon/mod.rs:257-271). A connection that registered sessions also receives pushed Event::Deliver { key, ids, text } frames, which the client must acknowledge with Delivered (src/proto.rs:270-275).

The request enum is internally tagged by op (src/proto.rs:82-210); dispatch is at src/daemon/mod.rs:278-324.

Request Purpose
Register{session} register or refresh a session; returns name, address, unread count
SetStatus{key, doing?, name?} set doing or rename
Who{me?, filter?, all} the directory; all adds sessions offline within the last day
Send{from, to, body, reply_to?, wake, notify, files} send; from is a session key, or for CLI-replying agents an address or name
Inbox{key, wait_secs, peek} unread mail; long-poll capped at 600 s (src/daemon/mod.rs:39, :289)
Unread{key} count and senders
Ask{key, question, about?} ask_user pop-up
StopWaiting{key} the client cancelled an Inbox or Ask wait
Delivered{key, ids} mark messages read and clear in-flight
Hook(HookCall) a harness hook fired; returns mail to inject
Attach{agent, cwd} map an id-less MCP call (Cursor, opencode) to a hook-known session
CreateTeam, Join, Proof, Invite, Team, Link, Remove, Leave, SetRelay membership and network
Held, Settle{ids, release} list and approve or drop held messages
Shutdown exit; the service manager restarts it

Sessions register by several routes, in priority order: a per-call _meta session id (Codex, opencode V2), environment variables (Claude, Copilot, Grok), hook stdin correlated by PID ancestry, or one session per MCP process (DESIGN.md:205-209). The MCP server must handle many sessions per process because Codex, opencode and Cursor share one MCP process across threads (DESIGN.md:78-79). Hook-only sessions are dropped when their agent's PID dies or after 6 hours without a hook (src/daemon/sessions.rs:9-10, src/daemon/sessions.rs:264-281).

8. Storage and retention

State lives in ~/.reason/network/ (README.md:92). The SQLite database runs in WAL mode with secure_delete on, and its files are chmodded 0600 because it holds the team link (src/store.rs:95-110). Tables (src/store.rs:48-87, with approval and files columns added by migration at src/store.rs:111-119):

Limit or constant Value Source
Message body 64 KB src/daemon/mail.rs:17
Wire frame 4 MiB src/net.rs:35
Ack wait 5 s src/net.rs:36
In-flight before re-showing 90 s src/daemon/mail.rs:23-24
Hook-only session TTL 6 h src/daemon/sessions.rs:9-10
Forget a gone session and its unread mail 24 h src/daemon/sessions.rs:11-12, src/store.rs:397-417
Outbox give-up 7 days src/store.rs:321-323
Files kept after read / while unread 1 day / 7 days src/files.rs:24-26
Expired invites pruned after 7 days src/team.rs:518-522
Inbound rate limit 60 messages/min per peer machine src/daemon/mail.rs:18-19
Loop guard 12 messages per (from, to) per 10 min src/daemon/mail.rs:20-22
Notification throttle / staleness 1 per sending person per 60 s / none over 15 min old src/daemon/notify.rs:12-15
ask_user wait / pop-up lifetime 600 s / 3600 s src/daemon/mod.rs:39, src/daemon/ask.rs:15-16
Presence cadence 2 s check, 60 s floor; outbox replay 30 s src/net.rs:586-635
Reason session poll 15 s, 20 sessions src/daemon/reason.rs:14, :82
Relay list refresh 6 h src/relays.rs:35

Forgetting a session deletes only its unread mail; the person's own CLI mailboxes and held mail are exempt, and nothing is forgotten in the first 6 hours after the daemon starts, while live sessions register again (src/store.rs:397-417, src/daemon/sessions.rs:282-287). I found no code path that deletes a message after it has been read; see section 18.

9. Delivery tiers

Each session advertises a tier so a sender knows when the message will be seen (src/proto.rs:8-20):

Harness Tier Mechanism Source
Claude Code (with the MCP server) push the MCP child posts a {"type":"user"} frame to CLAUDE_CODE_MESSAGING_SOCKET; read between tool calls, starts a turn if idle src/daemon/mail.rs:286, src/mcp.rs:355-365, src/agent.rs:209-229
Claude Code (open before setup, no MCP) push the daemon posts to the socket path in ~/.claude/sessions/<pid>.json; the agent replies via CLI src/daemon/mail.rs:293-298, src/daemon/directory.rs:64-93
Codex push codex queue --thread <id> --message …; hooks inject mid-turn (PostToolUse) and at stop (decision: block) src/daemon/mail.rs:287-289, src/daemon/sessions.rs:18, src/hook.rs:128-135
opencode push plugin: client.session.promptAsync (1.x), ctx.session.prompt (2.x) src/opencode-plugin.ts:81, :124, src/daemon/sessions.rs:20
pi push extension: sendUserMessage, deliverAs: "steer" while busy src/pi-extension.ts:28, src/daemon/sessions.rs:20
Reason (cloud) push reason session send <id> --message …; replies via the CLI src/daemon/mail.rs:290-292, src/daemon/reason.rs:1-3
Cursor turn postToolUse additional_context; stop followup_message src/hook.rs:136-137
Gemini CLI turn AfterTool / BeforeAgent additionalContext; stop block src/hook.rs:132-135
Copilot CLI turn additionalContext; stop block src/hook.rs:135, :138
Grok CLI turn PostToolUse additionalContext; Stop (end_turn) block; subagent hooks ignored src/hook.rs:116-121, :131, :135
DeepSeek Harness, any MCP client pull check_inbox(wait_secs) long-poll plus an unread notice on Reason Network tool results src/mcp.rs:205-240, src/mcp.rs:326-341

The daemon picks a channel per message in deliver (src/daemon/mail.rs:284-301): Claude socket, agent CLI command, direct Claude socket, hooks if the session has ever fired one, else pull. A session currently blocked in check_inbox gets the message there instead of a push (src/daemon/mail.rs:277-279). A message sent with wake: false (send --fyi, src/main.rs:413) is only queued for the next natural pause (src/daemon/mail.rs:274-276), and a mid-turn tool hook injects only wake messages while stop and prompt hooks inject everything (src/daemon/sessions.rs:208-213). DESIGN records that MCP notifications are not a delivery path because "no client passes them to the model (checked Oct 2026)" (DESIGN.md:241).

At-least-once. A message stays unread until whoever showed it to the agent confirms with Delivered (src/daemon/mail.rs:1-5). While a push or hook is outstanding the id sits in in_flight and is hidden from the inbox and other hooks; after 90 seconds unconfirmed it becomes visible again (src/daemon/mail.rs:40-46). The hook command marks messages read only after it has written them to stdout (src/hook.rs:21-25); check_inbox peeks and acknowledges right before returning (src/mcp.rs:219-236). Duplicates from peers are dropped by message id (src/daemon/mail.rs:501-503, src/store.rs:137-142).

Tier downgrade. If the agent-CLI channel fails (in practice codex queue), the daemon logs it, sets push_failed, and changes the session's tier to Turn, with the comment "Hooks (or check_inbox) show it instead; tell senders the truth from now on" (src/daemon/mail.rs:437-444). A re-registration cannot raise it again (src/daemon/sessions.rs:52). Reason is treated differently because it has no hooks to fall back on: failures back off from 15 seconds doubling to 30 minutes, and the 15-second sync pushes again (src/daemon/mail.rs:426-436, src/daemon/reason.rs:134-141). A failed Claude socket post does not downgrade anything; the message stays unread and resurfaces through hooks or the inbox (src/daemon/mail.rs:355-371, src/mcp.rs:359-364).

What the sender learns. Each recipient gets a state and a human-readable note (src/proto.rs:447-453). The states the code actually produces are pushed, queued, delivered, held, refused, retry (receiver busy or over file quota), notified and not notified (src/daemon/mail.rs:189-215, :313-352, :472-531). Notes describe the tier in words, for example "queued: it sees it at its next tool call or when its turn ends" (src/daemon/mail.rs:306-312).

10. What an agent sees: framing and defang

Incoming mail is rendered as peer input, never as the user (src/render.rs:239-284):

<reason-message from="alice/api-cursor" agent="cursor" repo="acme/api" id="01JA…">
Did you change the session token format? My tests started failing on refresh.
</reason-message>
From alice's agent. Reply if useful with send_message to "alice/api-cursor".

Attribute values are escaped so they cannot close the attribute or tag (src/render.rs:300-303). A file list, if any, goes after the closing tag so the body cannot imitate it (src/render.rs:265-266). The trailer says whether the text was typed by a person at a terminal or by an agent, and for CLI-only recipients gives the exact reason-network send … --from … reply command (src/render.rs:269-282).

defang is the main defence against forged frames (src/render.rs:305-331). It replaces any opening bracket, including 19 look-alike Unicode brackets, with [ when the tag word after it spells "reason" under a leetspeak reading (0→o, 3→e, 4/@→a, 5/$→s, other non-letters dropped), contains anything other than printable ASCII, or runs past 64 characters (src/render.rs:335-353). It is applied to message bodies, approval text, list_team output as a whole (because "a teammate's fields side by side (repo@branch) could open a frame that none of them opens alone", src/render.rs:133-137), send results, and tool errors (src/mcp.rs:176, src/mcp.rs:382-385). Peer message metadata is reduced to safe character sets and the id must be a canonical ULID (src/daemon/mail.rs:659-677). The most recent merged PR, #54, is titled "nothing a teammate sends opens a frame where agents see it" (git log).

Framing is the only content control. No code inspects what a message asks for. The skill text and MCP instructions carry the rest (section 15).

11. Trust controls

All of these act on messages arriving from teammates' machines in receive (src/daemon/mail.rs:451-540), in this order:

  1. Speaks for its own person. The sender machine's roster person must prefix msg.from (src/daemon/mail.rs:462-465).
  2. Approval check (section 12).
  3. Inbound policy. accept (default), hold or refuse, with a default and per-person overrides in config.toml (src/config.rs:26-42, src/config.rs:97-99). Refuse bounces with a note (src/daemon/mail.rs:472-475). Hold stores the message with held = 1 and saves no files until the person approves with reason-network held / approve (src/daemon/mail.rs:524-531, src/store.rs:270-311).
  4. Rate limit. At most 60 messages per minute per peer machine; beyond that the receiver answers retry, so the sender keeps the message in its outbox and replays it later. Deferred, not dropped (src/daemon/mail.rs:476-481, src/daemon/mail.rs:542-553, src/net.rs:735-739).
  5. File quota (section 13).
  6. Loop guard. At most 12 messages from one session address to another in any 10-minute window; the 13th is refused with "looks like a loop. Check with your user before continuing" (src/daemon/mail.rs:20-22, :49-64, :74-75, :515-517). Senders ending in /terminal are exempt (src/daemon/mail.rs:50-54). For local recipients the sender's daemon applies the same check (src/daemon/mail.rs:190-192).
  7. Group guard for messages from older builds (src/daemon/mail.rs:518-523).

Notifications. A notify pop-up shows at most once per sending person per minute and never for a message older than 15 minutes, so "a looping agent can't flood the screen" (src/daemon/notify.rs:12-15, :20-46).

Claude Code's own layer. Claude Code has a crossSessionInbound setting; a session running in bypassPermissions holds a stranger's message unless that setting is accept. Reason Network detects this from the transcript and tells the sender held by Claude Code… (src/claude.rs:78-86, DESIGN.md:309-313).

I think the loop guard is the most consequential default in this list, because it is the only control that stops two agents from exchanging messages indefinitely with no human involved. It is also weaker than DESIGN's description: it is a sliding count per direction, not a count of consecutive messages reset by human input (see section 19 and the closing notes).

12. ask_user and signed approvals

ask_user(question, about?) asks the session's own person with a pop-up on that machine and returns the answer as the tool result, "their own decision, not a message from anyone" (src/daemon/ask.rs:1-3). On macOS it is an osascript JavaScript panel, on Windows a PowerShell panel, on Linux a notify-send notification with Approve and Deny actions (src/daemon/ask.rs:378-383, :424-434, :439). One question per session is up at a time; asking the same question again joins the wait, a different one is refused (src/daemon/ask.rs:70-91). The call waits up to 10 minutes; the pop-up stays for an hour, and a later answer arrives as a message from <person>/terminal (src/daemon/ask.rs:93-113, :235-266). With no desktop the call errors so the agent asks in its own session (src/daemon/ask.rs:80-86).

With about set to the id of a teammate's message to this session, the pop-up shows that request first, in the sender's words, clipped to 500 characters, then the agent's question clipped to 400 (src/daemon/ask.rs:50-69). Direction overrides, zero-width characters and tag characters are stripped from what is shown and signed (src/daemon/ask.rs:385-393). When the person answers, the daemon builds an Approval holding what the panel reported it showed, signs it with this machine's key, and sends it to the requesting session from the asking session, or from the person's terminal if that session has gone (src/daemon/ask.rs:166-213).

pub struct Approval {                                    // src/proto.rs:279-291
    pub about: String, pub asker: String, pub approver: String, pub approved: bool,
    pub question: String, pub request: String, pub note: String, pub at: i64,
}

What "verified" means. The receiving machine keeps the approval only if approval_holds: the signer is the machine that delivered it, approver equals that machine's roster person, asker equals the session it arrived at, the text fields total at most 2,000 characters, and the signature verifies (src/daemon/mail.rs:650-657). Otherwise the approval is stripped and the body gains "(It came with an approval that didn't verify.)" (src/daemon/mail.rs:466-471). The agent then sees <reason-approval from=… about=… verified> (src/render.rs:250-260). Which request it answers is the approver machine's word, so the skill tells agents to check that from is the person they asked and about is their request's id (src/daemon/mail.rs:650-652, src/skill.md:41-42).

The project's own caveat. It proves the approver's machine recorded its person's answer. Release builds only run the system's dialog programs, so an agent cannot start the engine with a stand-in that answers (src/daemon/ask.rs:401-408). DESIGN still calls this "a speed bump, not a guarantee: software that controls the approver's machine can still sign (the machine's key is a file there, and it could build its own engine)" (DESIGN.md:280-281).

13. File attachments and confinement

A message can carry up to 10 UTF-8 text files, 256 KB each and 512 KB in total, inside the envelope, so they are as authenticated as the text (src/files.rs:1-4, src/files.rs:15-17). An agent's send_message reads files only through Confine: inside the repo root or cwd, no hidden path component, nothing under ~/.config, ~/.ssh, ~/.aws, ~/Library, %APPDATA%, %LOCALAPPDATA% or the state folder, and nothing at all if the session works in the home folder or above it (src/files.rs:36-80, src/mcp.rs:368-380). The stated reason is that "an agent can be talked into sending what its harness wouldn't let it read" (src/mcp.rs:369-370). A person's send --file has no such limit.

The receiver saves files in a new folder named by a locally generated ULID, never the sender's id, with the folder 0700 and files 0600, never executable, and marked as downloaded via macOS quarantine or the Windows zone identifier (src/files.rs:276-346, src/store.rs:22-30). The paths are listed after the frame with "read them, don't run them" (src/render.rs:286-298). A machine keeps at most 100 MB of teammates' files, 25 MB from any one person, and takes at most 20 MB per person per hour; past that it answers retry (src/files.rs:27-32, src/files.rs:371-399, src/daemon/mail.rs:504-514). For a peer whose presence lacks the files feature, the sender inlines the files as fenced blocks if they fit 64 KB, otherwise refuses for that recipient (src/daemon/mail.rs:679-692).

14. How "doing" is derived

"Agents never report what they are doing" (DESIGN.md:283). doing.rs builds a line of the form title · task in progress, clipped to 120 characters (src/doing.rs:16-49), from data the harness writes anyway:

Transcripts are tailed incrementally: the first read takes the last 2 MiB, later reads only appended bytes, and a file that is one JSON document (Gemini) is reread whole when it changes (src/doing.rs:99-156). Busy and idle come from hook events (prompt/tool mean busy, src/daemon/sessions.rs:202) and, for Claude Code, from ~/.claude/sessions/<pid>.json, which also lists Claude sessions that never loaded the MCP server (src/claude.rs:1-33). Those unmanaged sessions are shown to teammates only if share_unmanaged is on (src/daemon/directory.rs:193, src/config.rs:13-14).

15. MCP tools and behavioural instructions

reason-network mcp is a stdio server and a thin client of the daemon (src/mcp.rs:1).

Tool Arguments Notes
list_team filter?, all? defanged directory; read-only hint (src/mcp.rs:132-149)
send_message to, message, reply_to?, notify?, files? always wake: true (src/mcp.rs:151-178)
ask_user question, about? cancellation leaves the pop-up up (src/mcp.rs:180-203)
check_inbox wait_secs? (max 600) hidden from Claude Code sessions that have the messaging socket, "fewer tokens" (src/mcp.rs:205-262)

The server's instructions are the "How to behave" section of src/skill.md, extracted at build time (src/mcp.rs:30-37). The rules (src/skill.md:30-42):

DESIGN adds that the instructions cover only messaging and change nothing about how an agent works otherwise (DESIGN.md:300-302).

16. Hosted MCP and cloud-agent bridges

Reason cloud sessions. With reason = true, the daemon runs reason session list --limit 20 --json every 15 seconds, registers each unfinished, unarchived session as reason:<id> with tier push, uses its title as doing, delivers with reason session send, and drops sessions that disappear from the list (src/daemon/reason.rs:79-156). These agents reply through the local CLI (src/render.rs:277-279).

Workspace cloud agents. When the Reason CLI installed the binary under ~/.reason/bin (src/main.rs:45-53), the daemon keeps reason network __relay running, restarting it with backoff from 30 seconds up to 10 minutes; it "carries the workspace's cloud agents' calls (Reason's hosted tools) onto the network" (src/daemon/reason.rs:5-6, src/daemon/reason.rs:35-77). The relay's implementation lives in the Reason CLI, not this repo.

Hosted MCP. README tells hosted assistants (Grok Bot, Muse, ChatGPT) to add https://mcp.reasonmachines.com/mcp (README.md:43, DESIGN.md:333-334). Nothing in this repository implements that server, so I cannot say how it authenticates, which machine's daemon it reaches, or what tier those assistants get.

17. Implemented versus planned

Implemented, per the milestone list and confirmed in code (DESIGN.md:345-353): the daemon, socket API, SQLite, the MCP server, CLI, Claude push and passive discovery (M0); iroh transport, signed roster, invites, joins, removal, presence sync, acks and outbox, hold and refuse, rate limit, mDNS, custom relay (M1); Codex, Cursor, Gemini, Copilot and opencode adapters, setup, service install, doctor (M2); release builds and install scripts (M3). Sign-in proofs, Reason's signed relay list, file attachments and the Reason bridges landed after the M3 list in October 2026 (git log).

Planned as M4 and not started (DESIGN.md:10-11, DESIGN.md:354-355):

None of these exists in src/: there is no role field in Member (src/team.rs:54-61), no history or search request in Request (src/proto.rs:84-210), and no edit-path tracking in the hook handler (src/hook.rs:67-77). Amp appears in DESIGN's tier table as push via a plugin (DESIGN.md:233), but the repo ships no Amp plugin and M2 says "Amp: MCP only, by hand" (DESIGN.md:350); in code, Amp is only recognised by its MCP client name (src/agent.rs:64-65), which puts it on the pull tier.

18. What Reason Network does not do

These are gaps in the current code and docs, not criticisms; several are deliberate.

19. Surprising or non-obvious things in the code

Notes: where the lead notes, DESIGN and the code disagree

The code wins in each case.

  1. Size. The notes say about 10K lines in src/. wc -l gives about 14.9K lines of Rust, about 12K outside inline test modules.
  2. Ack states. The notes list stored|delivered|held|refused|retry. stored appears only in a test (src/net.rs:843). The states produced are pushed, queued, delivered, held, refused, retry, notified, not notified. The notes' inbox state appears only in a doc comment (src/proto.rs:450). DESIGN's "later receipts (delivered, read) flow back" (DESIGN.md:170-171) has no implementation; there is one Ack per Msg.
  3. Loop guard. The notes say 12 per pair per 10 minutes, and DESIGN says "a cap on consecutive agent↔agent messages in a thread without any human input" (DESIGN.md:314-315). The code counts per directed (from, to) pair in a sliding window, has no thread concept, and nothing resets on human input (src/daemon/mail.rs:49-64).
  4. Tier downgrade. The notes say push failure falls back to hooks with the tier downgraded. Only the agent-CLI channel (Codex) downgrades to Turn (src/daemon/mail.rs:437-444). Reason backs off and retries (src/daemon/mail.rs:426-436); Claude socket failures do not change the tier.
  5. check_inbox hiding. The notes say it is hidden for push agents. It is hidden only for Claude Code sessions that have a messaging socket (src/mcp.rs:251-269); Codex, opencode, pi and Reason sessions still see it.
  6. Notification throttle. The notes say one per sender per minute. The key is the sending person (src/daemon/notify.rs:24-33).
  7. Amp. The notes list an Amp plugin with push delivery. No Amp plugin exists in the repo; Amp is recognised by MCP client name only (src/agent.rs:64-65, DESIGN.md:350).
  8. Presence and envelope shape in DESIGN. DESIGN describes Presence{machine, seq, sessions[], human_status} and Envelope{id, from, to, reply_to, thread, body, sent_at} (DESIGN.md:166, :169). The code has Presence{sessions, known, features} (src/net.rs:59-67) and a Message with no thread or seq field (src/proto.rs:293-318).
  9. Ticket peers. DESIGN says the link carries "up to six members' keys" (DESIGN.md:132). The code puts this machine plus at most two others in a ticket (src/daemon/teamops.rs:416-418).
  10. Socket path in DESIGN's diagram. The diagram shows ~/.reason/network/sock (DESIGN.md:67); the code uses network.sock, or a short path under /tmp when that is too long (src/paths.rs:63-80). The module comment in src/proto.rs:2 still names an older path.

What I could not verify


Part 2: Beacon, from endpoint telemetry to reviewed memory

Beacon (agent-beacon, Asymptote Labs, MIT) is a Go monorepo. This part describes the checkout at ~/work/services/agent-beacon, HEAD 99c70908 (2026-10-20, tagged v1.3.45). Paths are repo-relative. Two prefixes recur: AO/ is pkg/asymptoteobserve/, the shared module for schema, ids, dedupe, privacy and the rules engine, and CB/ is cli/beacon/. Every file:line below was opened and checked against the code. Anything that lives only in docs, or only on the closed hosted side, is labelled as such.

1. Two products on one log

Beacon makes two pitches, and both run on the same code.

The README now leads with "Self‑improving memory for coding agents" and the line "A problem solved by one agent shouldn't need to be learned from scratch by another." It promises cross-harness history, "knowledge that compounds", and "reviewed knowledge future agents can retrieve through MCP or Agent Skills" (README.md). The problem it names is that what one agent learns in a session vanishes when the session ends, and that the next agent, often in a different harness, starts from scratch.

The other front is endpoint telemetry for local agent runtimes. The contributor guide still opens with "Beacon Endpoint Agent is a local-only endpoint telemetry agent for AI runtimes" and states the posture as "visibility-first endpoint telemetry for local AI agent runtimes, not a hosted policy service or general endpoint protection product" (CLAUDE.md:7, CLAUDE.md:28). The problem here is that security and platform teams cannot see what coding agents do on laptops, in CI and in cloud sandboxes. That front ships SIEM packs, MDM packaging, a CEL rule format with 77 rules, and a fleet installer.

The shift is recent and visible in the dates:

Date Change Source
2026-05-11 First commit git log
2026-05-13 v0.0.1, "local agent harness telemetry", Wazuh rules docs/changelog/cli.mdx:1660
2026-06-13 Open threat-rule format, beacon scan changelog
2026-09-21 v1.3.17: beacon login, Standard vs Metadata-only, local reviewed memory docs/changelog/cli.mdx:411
2026-10-18 Memory over Beacon Cloud MCP docs/changelog/cloud.mdx:30-32
2026-10-19 Beacon Cloud Pro ($20/month); memory and findings MCP tools gated to Pro docs/changelog/cloud.mdx:21-25
2026-10-19 README tagline switched to memory git log -- README.md

So the memory loop is about one month old, and the hosted memory tools are three days old at HEAD. The telemetry material was not removed, only demoted: offline beacon scan is a free feature, and "block or require approval" is an Enterprise line item. CLAUDE.md has not caught up and mentions memory only as a clause about the agent skills. The useful way to read the repo is that the memory product is a consumer of the telemetry substrate, and almost every property of the memory loop (locality, privacy, scoping, provenance) comes from decisions made for telemetry.

2. Shape of the system

There is no Beacon daemon in the capture path. Three kinds of process append to one JSONL file, and everything else reads that file on demand.

 harnesses (20+)                                                     
   | hook exec (stdin JSON)   | OTLP gRPC/HTTP 127.0.0.1:4317/4318   | session stores on disk
   | plugins spawn the same   |                                      |
   | hook binary              |                                      |
   v                          v                                      v
 beacon-hooks              beacon-otelcol (only resident            beacon CLI pollers
 (one short-lived          capture process; launchd/systemd/        (install-time backfill,
  process per event)        SCM/pidfile)                             manual, or --watch)
   |                          |                                      |
   +----------- flock(runtime.jsonl.lock) -> dedupe tail 256 KiB -> rotate 10 MiB x5 -> append
                                      |
                     ~/.beacon/endpoint/logs/runtime.jsonl
                                      |
   +-----------+-----------+----------+-----------+-------------+--------------+
   v           v           v          v           v             v              v
 dashboard   beacon     history.db  beacon      beacon mcp    beacon memory  Vector forwarder
 :8765       traces     (opt-in,    scan        serve         -> memory.db   -> Beacon Cloud
 + lenses    (TUI)      SQLite FTS) (CEL rules) (read-only)   -> SKILL.md    (Standard |
                                                                             Metadata-only)

The architecture doc calls the log "the stable handoff boundary" and keeps its active path fixed "so dashboards and shippers can follow it" (docs/architecture/architecture.mdx:88). Any consumer, including a third-party daemon, can tail the file without asking Beacon for anything.

3. The capture layer

3.1 Capture families

Family Mechanism Harnesses (examples) collection_method
Native hooks Harness execs beacon-hooks --platform <x> <event> with JSON on stdin; process writes and exits Claude Code, Cursor, Qwen, Kiro, Kimi, Muse, Grok, Hermes, Devin, OpenHands, dsh, Antigravity, goose hook
Managed plugins Single-file TS/JS plugins installed into the harness; each spawns beacon-hooks with a 2,000 ms send timeout (plugins/opencode-beacon/src/beacon.ts:8) OpenCode, Cline, Pi, Oh My Pi, Prime, Senpi/omo, OpenClaw plugin
Local OTLP Custom OpenTelemetry Collector beacon-otelcol with a beaconjson exporter that converts logs, traces and metrics into the same event Claude Code, Codex, Gemini CLI, Copilot, Factory, VS Code, Cowork otlp
Pollers beacon endpoint <rt> sync reads the harness's own session store with per-file cursors 14 store readers (Claude, Codex, Cursor SQLite, OpenCode, Cline, Copilot, Factory, fx, Grok, Hermes, OpenClaw, Pi, Prime, dsh) plus Cursor Admin API and Devin v3 API pulls poll
Browser extension (beta) MV3 extension tees claude.ai and chatgpt.com SSE streams and posts OTLP logs to 127.0.0.1:4318 claude_web, chatgpt_web otlp
CI beacon ci exec -- <cmd> starts an ephemeral collector around one command Claude Code, Codex otlp
Cloud sandboxes Hooks inside Claude Code on the web or Cursor cloud write /tmp/beacon/runtime.jsonl; cloudshuttle uploads new lines on Stop Claude web, Cursor cloud; Devin via central API pull hook / poll
SDK @asymptote/sdk emits OTLP traces to a customer collector or hosted Observe, not to the local log agents in code n/a

The plugin-versus-hook split is decided by the --platform flag, not by harness name, because some harnesses report through two paths under one name (AO/provenance.go:98-107). Claude Code is deliberately wired both ways: its OTLP export has no session lifecycle, file activity or subagent events, so hooks fill those in, and the call id lets the writer collapse the overlap.

Two caveats matter for anyone treating Beacon as a live feed. First, pollers are not scheduled by anything in the service layer. I found no launchd or systemd unit that runs a sync (CB/internal/endpoint/service/ holds collector, forwarder, inventory, updater and script-forwarder units only); syncs run at install and connect time as a 30-day bounded backfill, by hand, or under --watch with a one-minute ticker. Harnesses that are poll-only therefore appear in the log only when someone runs a sync. Second, the cloud-sandbox uploader is the single place a hook touches the network, and CLAUDE.md calls it the bounded exception, active only with BEACON_ORIGIN=cloud.

3.2 One log, three writers

The three writers are the hook adapter (cli/beacon-hooks/internal/logging/logging.go), the collector exporter (collector-builder/exporter/beaconjsonexporter/writer.go) and the CLI writer used by pollers and CI (CB/internal/endpoint/writer/writer.go). Each carries a comment telling maintainers to keep the rotation and file-mode contract mirrored with the other two (writer.go:257, logging.go:649, beaconjsonexporter/writer.go:211).

writer.AppendEvent (writer.go:63-129) shows the contract in order:

  1. Attach a derived trace id, sanitize (redact, then cap strings), validate.
  2. Marshal. If over 64 KiB, drop raw and cut message to 1,024 bytes. If still over, compactRetainedContent drops diff, command output, prompt text, gen_ai input and output, and tool arguments and results, and marks the content as held back.
  3. Stamp event.id last, over the bytes about to be written. An event that would exceed the cap with its id is written without one.
  4. appendJSONL (writer.go:258-322): take an exclusive lock on runtime.jsonl.lock, run the duplicate check, reserve backfill budget, rotate if needed, open with O_APPEND, write.

Rotation keeps the live file plus five archives at 10 MiB each (writer.go:22-26), so the log holds roughly 60 MiB. The docs put that at "a day or two of activity on a busy machine" (docs/cli/endpoint-traces.mdx:32). A backfill sweep carries a RetentionGuard that refuses an append if rotation would discard the sweep's own earlier output, and install-time backfill shares a 24 MiB budget so old history cannot evict live data.

There is no IPC between hooks and anything else. A hook resolves its log path from BEACON_ENDPOINT_LOG and three aliases, falls back to the default path only when BEACON_ENDPOINT_MODE is set, and otherwise records nothing (logging.go:613-626).

3.3 Provenance markers

Every event carries two independent provenance fields. harness.collection_method is one of hook | plugin | otlp | poll. event.fidelity is observed when the source named the action and inferred when Beacon derived it (AO/event.go:59-62). CLAUDE.md calls these "the only sanctioned way to express that difference" and forbids per-runtime confidence fields. Approvals show why both fields exist: several runtimes give only a pre-tool notification, and the hook records that as approval.allowed with fidelity=inferred, because "nothing was gated, a tool call was seen". Anything counting real human decisions has to filter on observed.

3.4 Dedupe at write time and read time

Write-time dedupe runs inside the lock, in all three writers, through one function: AO.IsDuplicateEndpointEvent (AO/dedupe.go). It scans the last 256 KiB of the log (dedupe.go:16) and applies only to tool-shaped actions (mcp.tool_invoked, command.executed, file.read, file.modified, tool.invoked, tool.completed, tool.failed). Two lines must first share a key built from action, session, workspace and a per-action target; for files the target is the path only, because Claude's Write reads as a modify in the hook and a create in OTLP. Then (dedupe.go:85-115):

The header records why the function is shared: two implementations of the rule "have already drifted apart here once, silently" (dedupe.go:64). The writer never dedupes on event.id.

Read-time dedupe covers what write time cannot. Token usage events carry no call id, and several runtimes are read twice (live, then by a later sync). preferLiveOverPoll (CB/internal/tokens/tokens.go:758-787) suppresses poll usage at or after the earliest live capture of a session and keeps poll usage from before Beacon was installed.

3.5 Observe-only and fail-open

Every hook path exits 0. The reason is spelled out in logging.go:236-246: on the Claude-shaped hook contract that several runtimes copy, exit 2 is a block that would "refuse the tool call, erase the prompt, or -- on Stop -- keep the agent running", and "A telemetry write that failed must never become a control decision." Write failures are reported on stderr and surfaced by beacon endpoint doctor instead.

Plugins apply the same rule. The OpenClaw plugin refuses to register before_prompt_build and agent_turn_prepare (prompt injection), inbound_claim (can swallow a message) and skill_proposal_evaluate: "Beacon observes. Registering any of them would ask OpenClaw for a power Beacon must not hold" (plugins/openclaw-beacon/src/beacon.js:29-34). Its before_tool_call handler dispatches and returns without awaiting, because OpenClaw registers that hook fail-closed.

The only control path is the policy provider (section 10), and it can only deny.

3.6 Per-harness reply contracts

What a hook prints back is a security property, because on several runtimes an "allow" is a pre-approval that would disarm the user's own prompts. preToolResponse (cli/beacon-hooks/cmd/pre_tool.go:173-260) states the rule ("a telemetry hook never answers 'allow' where the runtime reads that as a pre-approval") and documents each runtime:

Runtime No-opinion reply Reason given in code
Antigravity nothing decision is required and every value is an opinion; {} reads as a deny
Cursor {"permission":"allow"} silence and non-schema output are risky; Cursor staff confirmed allow is not a pre-approval. "If Cursor ever makes hook verdicts authoritative, this branch must be revisited"
Grok Build {} a response with no decision is no objection
Qwen, DeepSeek Harness, Claude-shaped {} permissionDecision: allow would skip the approval prompt
Muse {} its runner rejects unknown keys, so an allow object would fail the hook
OpenHands {} the reply is shown verbatim in the conversation
goose explicit allow object goose classifies a blocking hook's {} as a failed hook; Stop needs the same (cmd/stop.go:29-40)
Kiro, Kimi nothing on stdout stdout is pasted into model context (hookStdoutIsConsumedAsAgentContext, cmd/kiro.go:232)

The same comments record which hook outputs reach the model's context on each runtime (Kiro and Kimi stdout, Claude Code's SessionStart additionalContext, OpenHands' additionalContext key). Beacon uses that knowledge only to stay silent.

4. The canonical event and its ids

The event is a nested JSON object defined once in AO/event.go:642 and aliased by the CLI's schema package. Trimmed:

type Event struct {
    Timestamp     string        `json:"timestamp"`          // RFC3339Nano UTC
    Sequence      uint64        `json:"sequence,omitempty"` // per-writer counter, tiebreak only
    Vendor        string        `json:"vendor"`             // "beacon"
    Product       string        `json:"product"`            // "endpoint-agent"
    SchemaVersion string        `json:"schema_version"`     // "1.0"
    Event         EventInfo     `json:"event"`    // id, kind, action, category, fidelity
    Severity      Severity      `json:"severity"`
    Endpoint      EndpointInfo  `json:"endpoint"` // hostname, os, agent_version
    User          UserInfo      `json:"user,omitempty"`     // OS name, uid
    Harness       HarnessInfo   `json:"harness"`  // name, version, ..., collection_method
    Origin        Origin        `json:"origin,omitempty"`   // local | cloud | ci
    Run           *RunInfo      `json:"run,omitempty"`      // CI/cloud run ids
    Session       *SessionInfo  `json:"session,omitempty"`  // id, working_directory
    Handoff       *HandoffInfo  `json:"handoff,omitempty"`
    Trace         *TraceInfo    `json:"trace,omitempty"`
    Tool, File, Command, MCP, Approval, Policy, Prompt, SystemContext, Content, VCS, GenAI ...
    Model, Repository, Branch, Message string; Raw map; ...
}

Validate (AO/event.go:750-793) requires vendor, product, schema version, event.kind and event.action, severity, endpoint.os and harness.name, and checks the enums for origin, fidelity, collection method and content.retention. Harness names and model ids are canonicalized at write time, not at read time.

Correlation ids:

Id Field Derivation
Session session.id the runtime's own id, resolved per platform; OTLP conversation.id maps here
Turn prompt.id first hit in an alias table (promptId, prompt_id, turn_id, generation_id, ...); unique only within a session; survives Metadata-only
Tool call gen_ai.tool.call.id alias table over tool_use_id, call_id, callId, ...; links call, result, approval and the OTLP twin; also used as trace.span_id
Approval none of its own the tool-call id plus approval.* fields
Event event.id UUIDv5 under a fixed namespace (AO/identity.go:143-196)
Trace trace.id OTLP trace id if present, else session:<harness>:<session.id> (AO/identity.go:230)
Handoff handoff.source_harness, handoff.source_session_id parsed from a marker in the first prompt (section 8)
Commit vcs.ref.head.revision on session.commit_linked git-note attribution (section 9)

The event.id derivation has two modes (identity.go:177-196). When the event has a tool-call id and a resolvable target, the id hashes session, lowercased action, target and call id, so the hook and OTLP reports of one call get one id despite different timestamps and fields. Otherwise it hashes the whole serialized line. A kind prefix (call or content) keeps the two value spaces apart. The comment is careful about what equality means: a content-digest match says "nothing recorded tells these apart", which is weaker than saying it is the same action and, in the comment's words, "not one to drop an event over".

Ordering is by (timestamp, sequence), never by file position. sequence is per writer and restarts with each hook process (event.go:644-654), so "a hook event numbered 1 may well have happened after an exporter event numbered 900".

5. Privacy

Redaction. SanitizeEvent runs in every writer before validation. String values pass through a rule table (labelled secrets, key-name suffixes, Authorization headers, URL credentials, PEM blocks, provider token prefixes) and become [REDACTED], keeping type prefixes such as ghp_[REDACTED] so detection rules still match. Strings are capped at 4,096 bytes, raw strings at 2,048 (AO/privacy.go:14-21). CleanString "redacts before it truncates, so a cut can never leave part of a secret behind" (privacy.go:608). The docs state the limit: "A credential with no label and no recognizable shape, such as a bare random password, is not detected" (docs/security/retention-redaction.mdx:33).

Retention. Beacon keeps full content locally by default. The old --content-retention flag is a deprecated no-op ("Beacon now always captures full content", CB/cmd/ci.go:99-101). The local log therefore holds prompts, command output, diffs and tool results, after redaction and size caps.

Standard versus Metadata-only. These are forwarding modes; capture is the same in both.

Standard Metadata-only
Prompts, responses, reasoning, message, raw sent (redacted, truncated) removed
Tool arguments, results, descriptions, definitions sent removed
Command output, file diffs sent removed
approval.reason, policy.reason, eval explanations sent removed
Ids, prompt.id, timestamps, actions, harness, model, token usage sent kept
command.command, tool.command, file paths, decisions sent kept
content block as captured rewritten to retention=metadata, included=false, redacted=true

Enforcement happens in two places with one field list. The Vector forwarder applies the beacon_runtime_metadata transform (CB/internal/endpoint/asymptote/pack/metadata-transforms.toml:1) on the device, before the disk buffer. The Go function StripToMetadata (AO/metadata_only.go:20-61) mirrors it, and its only non-test caller is the share builder (AO/share.go:339), so a share from a Metadata-only endpoint sends no more than forwarding would. The doc comment says "A test in the forwarding pack holds the two to the same list of fields." Neither applies to the local log, the dashboard, lenses or the memory store, which all see full content.

Metadata-only is a structural projection: it keeps who did what to which file with which command, and drops what anyone said.

6. Local surfaces

Dashboard. beacon endpoint dashboard runs a foreground HTTP server on 127.0.0.1:8765 (CB/internal/endpoint/dashboard/server.go:26). It rejects non-loopback bind addresses and non-loopback Host headers to stop DNS rebinding (RequireLoopbackHost, server.go:443). The docs say it "never mutates endpoint configuration, telemetry, or memory review state" (docs/architecture/architecture.mdx:101). It is not a service.

Lenses. A lens is one HTML file (16 MiB cap) with a JSON manifest (api: "beacon.lens.v1"), loaded in a sandboxed iframe whose CSP includes connect-src 'none' (dashboard/lenses.go:49-50). Its only input is one call, window.beacon.getTrace(), which resolves to a beacon.trace.v1 bundle plus findings and token data. Seven are built in (activity, approvals, files-changed, mcp, security-review, skills, token-usage). The docs concede two limits: the sandbox is best-effort hardening, and "A lens sees one trace. A view across several sessions is not supported yet" (docs/concepts/lenses.mdx:144).

History search. history.db is an SQLite file with FTS5 trigram indexes over events and traces (dashboard/history_store.go:356-357). It keeps traces 90 days and caps the file at 1 GiB (history_store.go:48-51). It is off until the user turns it on: the first interactive beacon traces asks once, traces reindex creates it, and scripts, agents and --json callers are never asked (docs/cli/endpoint-traces.mdx:36-40). Every trace command catches it up with the log first, so it needs no background process; activity that rotates out of the log before any catch-up is lost.

Local MCP. beacon mcp serve speaks MCP 2024-11-05 over stdio or loopback HTTP at 127.0.0.1:8766 (CB/cmd/mcp.go:21). It exposes seven tools (CB/internal/mcpserver/server.go:126): four activity tools (search_activity, summarize_activity, get_activity_event, list_activity_filters) and three memory tools (search_memory, get_memory, get_memory_context). All seven are read-only. An agent connected to the local server can recall memory but cannot file it.

7. The memory loop

7.1 Overview

 runtime.jsonl / history.db
        |  dashboard.ShowTrace (beacon.trace.v1)
        v
 A. SCORE (optional, networked)            B. AUTHOR (local)
    BuildProjection: <=80 events,             agent reads the trace with
    head 40 + tail 40, 1,200-char fields,     `beacon endpoint traces show`
    redacted -> POST Jev or SubQ              and drafts a lesson
    3 yes/no questions -> probabilities       `beacon memory candidates create`
    PromotionDecision -> candidate
    (placeholder body)
            \                                  /
             v                                v
          memory.db: candidates {state: candidate}
                      |  approve (title/body/applicability edits) | reject | supersede
                      v
          memory.db: memories (one project_id, superseded_by)
              |                                        |
          RECALL (pull)                            PROMOTE
          search_memory / get_memory_context       .agents/skills/beacon-<slug>/SKILL.md
          beacon memory list -q                    -> git commit -> teammates

Nothing in this loop runs from hooks. The docs say so directly ("Nothing in this workflow runs from hooks", docs/concepts/cross-harness-memory.mdx:17), and the code agrees: no hook path imports the learning package.

7.2 Project identity and storage

Memory lives in memory.db, an SQLite file next to the log directory, so ~/.beacon/endpoint/memory.db in user mode (CB/internal/learning/store.go:52-61). The docs call it "durable local state, unlike the rebuildable trace index". It has three tables: evaluations, candidates and memories, each with a few denormalized columns and the full record as JSON.

Every record belongs to exactly one project. ProjectID hashes the lowercased remote URL, or the path when there is no remote: "sha256:" + hex(sha256(lower(remote_url || path))) (store.go:193-200). Worktrees resolve to the main checkout's remote. Two consequences follow. The id is deterministic across machines when the remote string is identical, which is the only cross-machine key the memory store has. And an ssh:// clone and an https:// clone of the same repo get different ids, so their memory is silently split.

7.3 Evaluation and the promotion rule

beacon memory evaluations run is the only networked step. It sends a projection of one trace to a System One decisions endpoint: TypeSafe Jev by default (https://api.typesafe.ai/v1/systemone) or SubQ (https://api.subq.ai/v1/systemone), with a 10-second default timeout and a flat cost estimate of $0.00035 per trace (CB/internal/learning/evaluator.go:18-26, :315-317). --dry-run makes no request.

The projection keeps at most 80 events. For longer traces it keeps the first 40, an omitted marker and the last 40, because "Long sessions resolve at the end ... otherwise the evaluator never sees the fix or outcome a reusable lesson depends on" (evaluator.go:240-253). Each text field is cut to 1,200 characters with redaction.

The rubric is three yes/no questions (evaluator.go:72-76):

The score is the plain mean of the three probabilities (evaluator.go:452-461). A trace becomes a candidate only when the evaluation completed, task_success was answered and is at least 0.50, and the mean is at least 0.60 (candidate.go:13, :25, :32-47). The 0.50 precondition came from issue #649: answers of 0.27, 0.86 and 0.69 average 0.6067, which cleared the score threshold for a trace the judge said had failed (candidate.go:19-24).

The evaluator returns probabilities, and the lesson text comes later. A scored candidate gets a guessed kind (keywords like "fix" or "debug" give debugging_pattern, otherwise correction) and a boilerplate body that says, when no rationale came back, "The evaluator returned scores with no rationale, so no lesson text was extracted. Review the source trace before approving." (candidate.go:302). The README's "evaluate what worked, extract useful knowledge" is, in code, a hosted classifier that filters traces, followed by an agent that writes the lesson and a person who approves it.

7.4 The agent-authored path

beacon memory candidates create --trace T --kind K --title ... --body-file - files a candidate with no evaluator (CB/internal/learning/authored.go). Kind, title and body are required. SourceEvaluationID stays empty, "which is how an authored candidate is told apart from a Jev-scored one" (authored.go:12-13). The beacon-memory-distill skill drives this path when the user declines the networked evaluation or has no key. It tells the agent that "the strongest evidence for a lesson is a failed command followed by a different command that succeeded" and to pick three sessions at most (skill guidance, not a CLI limit; the command takes one --trace).

7.5 Records and evidence

The schemas are in AO/learning.go under beacon.learning.v1:

type LearningEvidenceV1 struct {
    TraceID     string   `json:"trace_id"`
    EventIDs    []string `json:"event_ids,omitempty"`
    Summary     string   `json:"summary,omitempty"`
    Redacted    bool     `json:"redacted,omitempty"`
    Truncated   bool     `json:"truncated,omitempty"`
    ContentHash string   `json:"content_hash,omitempty"`
}

type LearningCandidateV1 struct {
    SchemaVersion, ID, MemoryID, State, Kind, Title, Body, Applicability string
    Tags               []string
    Project            LearningProjectV1     // id, path, remote_url, branch
    SourceEvaluationID string                // empty for agent-authored
    Evidence           []LearningEvidenceV1
    CreatedAt, UpdatedAt, ApprovedAt, RejectedAt, SupersededAt string
    SupersededBy, ReviewReason string
}

type LearningMemoryV1 struct {
    SchemaVersion, ID, CandidateID, Kind, Title, Body, Applicability string
    Tags         []string
    Project      LearningProjectV1
    Evidence     []LearningEvidenceV1
    CreatedAt, UpdatedAt, SupersededBy string
}
States (candidate) Kinds (both)
candidate, approved, rejected, superseded (learning.go:11-16) workflow, correction, debugging_pattern, gotcha, convention (learning.go:18-24)

Evidence is by reference: a trace id and event ids, never trace content. Those references resolve only on the machine whose log or history.db still holds the trace. Ids are truncated SHA-256 content hashes (store.go:676-690): a candidate id covers project, kind, title, body and evidence, so filing an identical lesson twice produces one record; a memory id covers project, candidate id, kind and title.

The records have no author field, no reviewer field, no confidence, no usage counter, no TTL and no scope other than the single project.

7.6 The review gate, and how strong it is

ApproveCandidateWithEdits (candidate.go:131-172) is the one place a memory comes into existence. It checks that the kind is valid and the candidate's state is candidate, builds the memory from the reviewer's edits (empty fields fall back to the candidate's values), writes the memory, then flips the candidate to approved with ApprovedAt, MemoryID and a free-text ReviewReason. The candidate keeps the evaluator's original text "so the review history shows what was changed" (candidate.go:100-105). Reject requires the candidate state too. Supersede requires a replacement memory in the same project but does not check the candidate's state, so an already-approved candidate can be superseded, which also stamps superseded_by on its memory.

The review is a convention of the workflow that the code does not check. The approval records the lesson text, a timestamp and a reason string, but not who approved it. The CLI command (CB/cmd/memory.go:498-512) needs no TTY, no confirmation and no reason, and nothing distinguishes a person from an agent running beacon memory candidates approve. If no --body is given, a scored candidate's placeholder body becomes the memory body; the only code reference to the placeholder string is the generator itself. The human-in-the-loop guarantee lives in the distill skill's text: "Never approve a candidate with its placeholder body" and "A candidate the user has not decided on stays a candidate. Silence is not approval." (agent-skills/skills/beacon-memory-distill/SKILL.md:27, :254). The docs describe the loop as "deliberately review-gated" (cross-harness-memory.mdx:32-33), which is true of the intended workflow and not of the Go API.

7.7 Recall

Recall is pull-only. An agent calls get_memory_context or search_memory over local MCP, or the CLI's beacon memory list -q, and reads the results.

Store.ListMemories (store.go:591-651) selects non-superseded memories for the project, ordered by updated_at DESC. When there is a query, it loads the rows and applies matchesText in Go (store.go:737-749): split the query on whitespace, lowercase, and require every term to appear as a substring of id + title + body + kind. Applicability and tags are not searched. I would not call this search in the usual sense: it ANDs substrings over four fields and returns the newest matches first, with no ranking, stemming or embeddings. The recall skill compensates by telling agents to search with "two or three distinctive terms", since a full sentence matches nothing.

Scoping happens before matching (server.go:386-406). An explicit project_id wins. A project path is matched only against projects already present in memory.db and never touches the filesystem; an unknown path "keeps scoping the query to nothing rather than widening it to every project" (store.go:234-241). With neither, the server resolves the project from git in its own working directory, so a stdio server launched by a harness inside the repo scopes itself. search_memory returns at most 20 summaries with bodies cut to 1,200 characters; get_memory_context returns at most five, and its task argument is just an alias for q.

7.8 Promotion to Agent Skills

beacon memory skills install <candidate> renders an approved, non-superseded memory as .agents/skills/beacon-<slug>/SKILL.md in the project (CB/internal/learning/skills.go:19-70, AO/learning_skill.go:11-13). Provenance (beacon_memory_id, beacon_candidate_id, kind, tags) goes under the Agent Skills metadata map "so a generated skill validates and can be published like any other". The file is written 0644 and is not overwritten without --force. A superseded memory does not update an installed skill.

This is the only path by which a lesson reaches a teammate in the open product, and it reaches them through git: someone commits the file and the teammates' harnesses load it. It is also the only path with semantic matching, since the harness compares the skill's description (title plus applicability) against the task. The promote skill warns accordingly: "Each installed skill adds to what every agent considers on every task."

7.9 Cloud memory is a separate store

Beacon Cloud runs the same beacon.learning.v1 workflow over forwarded sessions through the hosted beacon-managed MCP server: beacon_memory_candidate_create, _list, _get, _review, beacon_memory_search, beacon_memory_get and beacon_memory_skill. The tool names and arguments come from docs/deployment/managed-mcp.mdx and the distill skill; the server code is not in this repo. There is no evaluator on the cloud path ("There is no evaluator", managed-mcp.mdx:175): the agent reads a session, drafts, and files only after the user confirms. Cloud cannot write to the repository, so beacon_memory_skill returns content and a path for the agent to write. The renderer is shared, so a skill promoted from either side is byte-identical.

The two stores never sync: "Memory approved through Beacon Cloud stays there; it is not copied into the local store, and local memory is not uploaded" (distill skill, SKILL.md:259). Cloud memory is personal, like the rest of Cloud (section 11).

8. Handoff briefs

beacon handoff list|export|resume moves work between sessions and runtimes on one machine. It reads each runtime's own session store, not the Beacon log, because "the store is what the runtime itself resumes from, and it keeps content that the runtime log truncates" (CB/internal/handoff/session.go). If the store is gone it falls back to the log and says the brief keeps less.

The brief is Markdown capped at 48 KiB (handoff/brief.go:18-23): source session details, first and latest requests and the last agent message (800 runes each), up to 40 changed files, up to 12 recent commands with exit codes, and the last 30 steps at 1,200 runes each, dropping the oldest first to fit. Events pass through SanitizeEvent, and fences are lengthened so quoted content cannot forge structure. A closing section tells the receiving agent to check git status and to confirm its understanding with the user before changing anything. Briefs are written 0600 under ~/.beacon/endpoint/handoffs/.

resume reopens the session natively (claude --resume, codex resume) when the runtime, CLI, session file and directory allow it. Otherwise it starts a new session whose first prompt names the brief by path, never inline, because "a command line is visible to every local user through the process table" (handoff/resume.go:179-185), and ends with the marker [beacon-handoff from=<harness> session=<id>] (AO/handoff.go:25). The hook adapter (cli/beacon-hooks/cmd/handoff_link.go) and 14 poller mappers parse the marker and write a session.handoff event linking the new session to the old one. The doc comment is explicit that the marker is unverified prompt text and not a verified identity, because "anyone can type it" (AO/handoff.go:16-19). Launches never pass flags that disable approvals and unset switches like COPILOT_ALLOW_ALL and HERMES_YOLO_MODE.

Share. beacon share projects a session into the same beacon.trace.v1 bundle that lenses and the evaluator use and uploads it to https://api.beacon.sh (AO/share.go; CB/cmd/share*.go). Before upload it sanitizes every event, rewrites paths relative to the session directory and the home directory to ~, drops the working directory and repo path, and strips credentials from the remote URL. Visibility is direct (default, anyone with the link), private (owner only) or public (listed; needs confirmation) (share.go:55-57). There is no per-user, team or org ACL. Uploads are idempotent by event id, and refresh sends only events after the server's watermark. A committed .beacon/share.json can turn auto-share off for a repo but never on, so "a cloned repository can never publish your sessions".

Git notes. beacon git setup installs a post-commit hook that attributes a commit to sessions by file overlap: it scans the log for sessions with write operations on the commit's changed files, in a window of at least 2 hours and at most 24 hours before the commit plus one minute of slack, linking up to 32 sessions (CB/internal/gitlink/link.go:13-21). The only attribution method is file_overlap (gitlink/events.go:15), and the docs call a link "an inference, not an observation". Links are written as a note under refs/notes/beacon (gitlink/note.go:13), one line per session, beacon:<harness>/<session-id> [share-url], and a session.commit_linked event is written to the log. Share URLs in notes are accepted only for https on allowed hosts, because notes can come from anyone who can push. With --share-notes, remote notes are fetched into refs/notes/beacon-remotes/<remote> (gitlink/share.go:15) and merged with git's cat_sort_uniq strategy, which keeps every line from both sides. This is the one decentralized, server-free team primitive in the product, and it carries identifiers only.

10. Detection rules

Rules are YAML with a CEL match expression over an event e, or a correlation of steps within a time window, scoped to one session (the only scope in spec v1.1, spec/threat-rules/SPEC.md:99). Evaluation cost is capped at 5,000,000 CEL units per expression; exceeding it is an error, never a match (AO/threatrules/cel.go:94). The spec calls the format "Sigma for agent telemetry" and allows the engine that consumes it to be "separate (and closed)".

The corpus has 77 rules across 10 categories: 57 stable and 20 experimental, and all 77 are posture: detect (counted with grep over rules/). Examples include secret-read-then-egress (a credential-file read and a curl/wget/nc egress within 120 seconds, in either order) and indirect-injection-in-tool-result, which matches a derived result_text field carrying both an override phrase and a download-and-execute payload.

Evaluation today is offline. beacon scan reads the log, sorts by (timestamp, sequence) and returns findings: a Go struct with rule id, severity, session and the evidence events (AO/threatrules/findings.go:59-70). The dashboard runs the same scan on demand, and --fail-on high makes it a CI gate. Scan "does not mutate endpoint telemetry" (docs/detections/engine.mdx:115), so findings are not written back to the log, and the open build has no alert channel. Nothing under cli/beacon-hooks imports the rules engine. Because scan reads only what the log still holds, a rule can see roughly 60 MiB of history unless history.db is on.

Metadata-only data keeps command lines, file paths and MCP fields, so command, file and correlation rules still work on it. It removes prompt text, tool results and tool descriptions, so the prompt-injection rules that match those fields are blind there.

The policy provider. The only hook-time control is BEACON_POLICY_PROVIDER, an executable the hook consults on the pre-tool and permission-request phases (AO/policycontract/policycontract.go:30-32). The client "contains no enforcement decision logic of its own", is inert when unset, and is fail-open "on every error path (missing provider, marshal failure, exec failure, timeout, non-zero exit, malformed output)" with a 2-second timeout (cli/beacon-hooks/internal/policy/policy.go:1-31). A deny is rendered in each runtime's own deny shape and logged as policy.decision=deny. The repo ships a reference provider that denies one pattern, and beacon endpoint enforcement enable downloads a signed Enterprise package; the decision logic is not open.

11. Beacon Cloud and its team model

Beacon Cloud is reached through a Vector forwarder that tails the log and posts gzip NDJSON to /v1/ingest/runtime with a per-device bearer key, after the on-device privacy transform. Connecting stages a 30-day backfill. The account model is a beacon.sh sign-in (browser PKCE); CLAUDE.md:30 insists "Signing in alone never starts forwarding" and the install wizard's confirm screen is the consent point.

The team model is thin. Each machine has its own bcn_device_* key. On every request the ingest checks that "the user who approved the device is still a member of the organization" and that the org is enabled, cached for one minute (docs/log-forwarding/asymptote.mdx:42). The tenant is added server-side and the device never asserts it. Beyond that, the product is personal: "You see only your own history. Results are limited to sessions forwarded from your devices" (docs/deployment/managed-mcp.mdx:239). SSO, RBAC and audit logs appear only as Enterprise bullets in the plans table, and no role names exist in the repo. MDM packages do not enroll devices: "there is no headless enrollment for MDM fleets yet; it is a planned follow-up" (docs/mdm/index.mdx:62).

Cloud adds hourly "findings" (security, skill and behavior kinds) over forwarded activity, with user feedback read by the next run. The docs say these are separate from local beacon scan results; whether they reuse the CEL rules is not visible in this repo.

12. Implemented, documented-only, planned

Capability Status Basis
Hooks, plugins, collector, pollers, three writers, dedupe, rotation Implemented code read
Browser extension Implemented, beta; adapters marked experimental code and docs
Provenance markers, deterministic event.id Implemented AO/provenance.go, AO/identity.go
Redaction, Standard/Metadata-only (Vector + Go) Implemented client side AO/privacy.go, AO/metadata_only.go, pack TOML
Dashboard, lenses, history.db, local MCP (read-only) Implemented code read
memory.db, evaluator gate, authored candidates, approve/reject/supersede Implemented CB/internal/learning/*
Skill promotion to .agents/skills Implemented skills.go, learning_skill.go
Handoff list/export/resume, marker linking Implemented CB/internal/handoff, handoff_link.go
Share client, git notes and note sharing Implemented (beacon.sh server closed) AO/share.go, CB/internal/gitlink
beacon scan, 77 rules, CI gate Implemented, offline only cmd/scan.go, rules/
Policy provider client Implemented, inert by default policy.go
Inline enforcement, block or require approval Enterprise, closed; installer stub only cmd/endpoint_enforcement.go, plans
Cloud ingest, findings, memory MCP tools, alerts Documented-only here (closed service) docs/deployment/managed-mcp.mdx, cloud changelog
SSO, RBAC, audit logs, Okta/Slack connectors Documented-only (plan bullets) docs/deployment/plans.mdx
Scheduled poll collection Not present (docs list poll as a capture path) no service unit runs syncs
MDM headless enrollment Planned docs/mdm/index.mdx:62
Cross-session lenses Planned ("not supported yet") docs/concepts/lenses.mdx:144

13. What Beacon does not do today

These are the gaps Part 3 builds on. Each was checked against the code or the docs' own wording.

14. Non-obvious details


Part 3: The hive, a technical design

Parts 1 and 2 describe two systems that each build one piece of what a team of agents needs. Reason Network gives a team's agents a shared present: who is running where, on which branch, doing what, and a way to put a message into a running session. Beacon gives one person's agents a past: a normalized record of what every harness did and a reviewed set of lessons drawn from it. Neither has a future tense, meaning obligations, standing requests and an agent that is still awake when the laptops close. Freyja's always-on platform design and its orchestrator design supply that part.

This part describes one system built from all three, which I call the hive. It is a design, not a description of running code, and every component is mapped to the code it would reuse. Where the design needs a change to either repository, the change is named with the file it touches.

1. Constraints

  1. No required server. Reason Network's mesh needs no central service, and the hive keeps that property. The always-on machine (the anchor) is a roster member like any other, so losing it costs history and proactive help but not messaging between online machines.
  2. The friends assumption stays. The roster module's first line says it is "Built for a team of friends who trust each other, not adversarial members" (src/team.rs:1-3). The hive defends against compromised content (a poisoned issue, a malicious package, a web page an agent read) and leaves a malicious teammate to the social controls a team already has.
  3. Metadata leaves the machine by default, content does not. Presence and the graph use Beacon's Metadata-only projection. Prompts, tool output and diffs leave a machine only when their owner shares a trace or approves a lesson.
  4. One author per proactive message. Any session or watcher may propose something for a person's attention, but only the anchor decides whether it reaches them, and every decision is recorded.
  5. Everything shared is a projection over append-only logs. The graph, project pages and session cards can be dropped and rebuilt, and retraction is a replicated tombstone, not a hope that every copy gets deleted.
  6. Authority is earned. Learned components start in shadow and are promoted on measured agreement, the way Freyja promotes its Jev decision models.
  7. Unattended work stops at drafts. The anchor may do anything inside its own sessions and branches. Anything another person will see, or that changes shared state, waits for a signed approval from the person it affects.
  8. The graph finds the work. Proactive work starts from a record the anchor can check: a contract that changed, two branches that fail when merged, a pull request that is ready with no reviewer, a date that has passed. A model does the work; it is not asked to search for something to do, because models find work far less reliably than they do it. On a subset of Active-SWE, Claude Opus 4.8 resolved 59% of bugs when given the issue report and 26% when it had to find them (arXiv:2608.04682, Table 3).

2. One adapter per harness

Both repositories maintain a per-harness adapter layer. Beacon's observes: native hooks for about 20 harnesses, seven managed plugins, a local OTLP collector and 14 session-store pollers. Reason Network's injects: a Claude Code messaging socket, the Codex queue command, opencode and pi plugins, hook outputs for Cursor, Gemini CLI, Copilot and Grok, and an MCP server for everything else. Where a harness has hooks, both attach at the same points (session start, prompt submit, tool results, stop), and both encode, harness by harness, which hook outputs reach the model. Beacon records that table only to stay silent (cli/beacon-hooks/cmd/pre_tool.go:173-260, hookStdoutIsConsumedAsAgentContext in cmd/kiro.go); turn-tier delivery needs exactly the same table.

The hive merges them into one descriptor per harness:

pub struct HarnessAdapter {
    identity: Identity,  // canonical name, aliases, version probe, session-id and cwd keys
    install:  Install,   // owned-block writer, detector, uninstaller, re-parse check
    observe:  Vec<Source>, // Hook | Plugin | Otlp | Poll, each tagged collection_method + fidelity
    presence: Presence,  // which events update busy/idle/doing/files, and their latency class
    inject:   Inject,    // tiers offered: Push(channel) | Turn(hook points) | Pull(mcp)
    gate:     Gate,      // approval visibility, deny shapes, stdout-is-context flags, no-opinion reply
}
Plane Reason Network today Beacon today
identity agent kind from MCP client name and hook flags (src/agent.rs) NormalizeHarnessName, session-id alias tables (AO/harness.go, AO/identity.go)
install setup.rs writes hooks, plugins and MCP config CB/internal/endpoint/hooks/*, owned blocks with uninstall
observe transcript tails and todo files for doing (src/daemon/directory.rs:54-126) four capture families into runtime.jsonl with provenance
presence rebuilt every 2 seconds from session files and git (src/net.rs:586-635) active-sessions.json presence table; session.* events
inject push, turn and pull tiers (src/daemon/mail.rs, src/hook.rs) none, by design
gate ask_user dialog and signed approval (src/daemon/ask.rs) deny-only policy provider, inert by default (policy.go)

Observation feeds presence. The daemon tails Beacon's log, which the architecture doc calls "the stable handoff boundary", instead of re-parsing transcripts. prompt.submitted marks a turn as started, tool.completed and agent.message keep it busy, session.ended and the idle events end it, todo-shaped tool calls update doing, and file.read and file.modified give the files a session touched. Presence gets the same latency as capture: immediate for hook and plugin harnesses and batch-flush latency for OTLP. Poll-only harnesses update today only when someone runs a sync, so the daemon runs their pollers in Beacon's watch mode (a one-minute ticker), and the descriptor states the resulting minute-scale latency instead of hiding it.

The hook process is the turn-tier vehicle. Beacon already runs a short-lived hook process at every turn boundary on every hooked harness. In the hive, that process writes its event as it does today and then asks the daemon over the local socket whether anything is queued for this session. It waits at most 150 ms (an assumption to tune), and on a timeout or an empty queue it prints the harness's no-opinion reply from Beacon's table, so a slow daemon can never become a control decision.

This is Beacon's own rule: "A telemetry write that failed must never become a control decision" (logging.go:236-246). One hook binary per event replaces the two that would otherwise run.

Injection is visible in the same log. Every delivery writes an event (team.message.injected with the session id, turn id and tier), so the record of what entered a model's context sits next to the record of what the model did. Beacon's security posture depends on never writing into context; the hive keeps that property for everything except team traffic, and makes the exception auditable.

3. The daemon

 hooks / plugins ─┐                      ┌──────────────────────────────────────────┐
 OTLP collector ──┼─► runtime.jsonl ─────►  capture → presence → directory          │
 pollers ─────────┘   (Beacon writers)   │  mailbox ⇄ delivery (push / turn / pull)  │◄─► teammates
 hook "anything for me?" ── socket ──────►  screen (defang, rules, inbound policy)   │    (iroh QUIC)
 CLI / MCP ──────────────── socket ──────►  lessons ⇄ review (ask_user) → sign        │
                                         │  graph projection; obligations (anchor)  │
                                         └──────────────────────────────────────────┘
 stores: runtime.jsonl (Beacon) · network.db (RN sessions, mail, outbox) · lessons.db · graph.db

The process model follows Beacon's analysis of the merge: hooks stay thin and stateless, and state (queues, peers, lessons) is kept in Reason Network's resident daemon. The OTLP collector stays a separate process, since it is the one resident capture process Beacon already runs under launchd, systemd or a service manager.

Presence gains three fields. Reason Network's presence frame today is {sessions, known, features} (src/net.rs:59-67); each session entry gains files (paths touched in the last 30 minutes, at most 32, paths only), turn (busy, idle or waiting_approval, the last from an approval.requested without an answer) and head (the commit the branch points at). files and turn come from Beacon events under the Metadata-only projection, which keeps commands and file paths and drops prompts, outputs and diffs. head comes from one more git rev-parse HEAD beside Reason Network's existing repo and branch lookup (src/git.rs:11-25), since neither project reports it as presence today.

Metadata-only keeps file paths (AO/metadata_only.go:37-40 clears only the diff), so the hive adds a path filter before a files entry leaves the machine. Its first patterns can come from Beacon's credential-access/credential-file-read rule, which already matches .env files, .aws/credentials, id_ed25519 and git credential stores; without the filter, a teammate's directory would show ~/.ssh/id_ed25519 whenever an agent read it.

4. Wire protocol

Reason Network's team protocol is a stream of length-prefixed JSON frames tagged Hello, State, Presence, Msg and Ack (src/net.rs:47-55). The hive adds four:

#[serde(tag = "t", rename_all = "snake_case")]
pub enum Wire {
    Hello { team: String, proto: u32 },
    State { state: Box<TeamState> },
    Presence { presence: Presence },
    Msg { msg: Message },
    Ack { id: String, state: String, note: String },
    // hive additions, sent only to peers whose presence lists the "hive/1" feature
    Entry { entry: Signed<LogEntry> },        // one numbered entry in an (author, scope) log
    Body { hash: String, body: Vec<u8> },     // a lesson body, sent after its entry or on request
    Sync { want: Vec<(String, Scope, u64)> }, // highest seq held per (author, scope), for anti-entropy
}

The feature gate is not optional. An unknown frame type fails to deserialize, and the read loop treats any read error as a disconnect (src/net.rs:681-690), so one hive frame sent to an older build would drop the link. Reason Network already negotiates optional behaviour through the features list in presence (attachments use a files feature, and a sender inlines files for a peer without it, src/daemon/mail.rs:679-692), and the hive frames would be gated on a hive/1 entry in that list. A #[serde(other)] catch-all variant in new builds would make later additions safe without a feature check.

Every signature uses the team id. Roster entries sign over "tincan/1/{team}/{kind}/" with the team id (src/team.rs:25-30; signed with self.id at :304, verified at :421 and :456), but approvals sign over the team's name (src/daemon/ask.rs:201, src/daemon/mail.rs:467), so a rename while an approval is in flight makes it fail verification. Hive signatures use "hive/1/{team_id}/{kind}/", and the approval path should move to the id at the same time.

Store-and-forward

Today a message for an offline machine waits in the sender's outbox for up to seven days (src/store.rs:321-327), so two teammates who are never online together cannot exchange mail. The anchor fixes this by accepting custody: when the recipient is offline and the anchor is online, the sender's daemon hands the message to the anchor, the anchor answers with a new ack state, anchored, and the sender clears its outbox entry. The anchor delivers when the recipient's machine connects and keeps the entry until the recipient acks.

Custody needs a change to how messages are authenticated. A peer message is trusted today because of the connection it arrives on: the receiver checks that the sender address starts with the roster person of the connected machine (src/daemon/mail.rs:462-465). A message relayed by the anchor arrives on the anchor's connection, so that check cannot pass.

Relayed messages therefore travel as Signed<Message>, signed by the originating machine's key with the same envelope as approvals, and the receiver verifies the signature against the roster instead of the connection. The anchor can read relayed mail, which is acceptable for a member the team runs. A team that wants relayed bodies unreadable by the anchor needs an encryption key per machine, for example an X25519 key for HPKE added to each roster entry; the roster's ed25519 keys are signing keys, and using them for encryption would need a separately specified conversion.

5. Lessons

The record

Beacon's approved memory (LearningMemoryV1) has an id, a kind, a title, a body, an applicability string, tags, a project and evidence pointers, and "no author field, no reviewer field, no confidence, no usage counter, no TTL and no scope other than the single project" (Part 2, section 7.5). Reason Network's approval has an approver, a question, the request it answers, a decision, a note and a time, signed by the approver's machine. A hive lesson joins the two:

pub struct LessonBody {
    pub id: String,             // blake3 over author, scope, kind, text, applicability, quote, evidence
    pub kind: LessonKind,        // Convention | Correction | Gotcha | Decision | Routing | Preference
    pub text: String,           // at most 1,200 characters, Beacon's recall body cap
    pub applicability: String,  // when it applies (Beacon's field)
    pub quote: Option<Quote>,   // { speaker, words, at }: required for Preference and Decision
    pub scope: Scope,           // Personal(person) | Project(project_id) | Team
    pub evidence: Vec<Evidence>,// { kind: Event | Commit | Message | Approval, reference, digest }
    pub author: String,         // a session address, e.g. alice/api-claude
    pub supersedes: Option<String>,
    pub at: i64,
}

pub struct Review {             // Reason Network's Approval, generalized
    pub about: String,          // lesson id, or message id for a request approval
    pub asker: String, pub approver: String, pub approved: bool,
    pub question: String, pub request: String, pub note: String, pub at: i64,
    pub scope: Option<Scope>,   // the scope the reviewer approved for sharing
    pub edits: Option<String>,  // the reviewer's replacement text, if any
    pub diff_hash: Option<String>, // prepared work: hash of the exact change approved (section 7)
    pub base: Option<String>,   // prepared work: the commit its tests ran on
}

pub struct Tombstone { pub lesson: String, pub reason: String, pub at: i64 }

pub struct LogEntry {           // what a log stores and its author signs
    pub author: String,         // the originating machine's roster endpoint
    pub scope: Scope,           // one log per (author, scope)
    pub seq: u64,               // gap-free within that log
    pub record: Record,         // Lesson(header + body_hash) | Review | Tombstone
}

Two rules from Freyja's orchestrator design apply at write time. A Preference lesson must quote the person it describes, so "Carol's agent says Bob prefers squash merges" is stored as hearsay with Carol's agent as the speaker and never treated as Bob's preference. And a lesson with no evidence is rejected: cite or omit.

Lifecycle

 draft ──propose──► proposed ──review: approve──► verified ──supersede──► superseded
 (local)            (visible to    │                  │
                     its reviewer) ├─ reject ──► rejected
                                   └─ no answer ──► held ──(morning digest)──► proposed
 any state ──retract──► retracted (tombstone replicates; the body is deleted from every daemon's store)

The states extend Beacon's candidate machine (candidate, approved, rejected, superseded, learning.go:11-16) with held and retracted. Beacon's ApproveCandidateWithEdits (candidate.go:131-172) is the single place a memory comes into existence today, which makes it the place to require a signed Review whose approver is a roster person.

Choosing the reviewer is a graph query. A personal lesson goes to the session's own user. A project lesson goes to whoever owns the files in its evidence: the CODEOWNERS entry if the repo has one, otherwise the person whose sessions and approved decisions touched those files most in the last 30 days. A team-scope lesson needs a reviewer per project it names.

The request travels through ask_user, so the reviewer sees the lesson's text, its evidence summary and the scope requested, and the answer comes back as a signed Review. A request that gets no answer in 24 hours moves to held and reappears in the reviewer's morning digest instead of interrupting again.

Scope and replication

Scope decides which machines receive a lesson. A Personal lesson never leaves its owner's machines. A Project lesson goes to the machines of the project's members, and a Team lesson goes to every member. Project membership needs an explicit record, because inferring it from activity would let anyone who clones a repo receive its lessons. The hive adds Signed<ProjectMember { project, person }> entries to the roster, proposed automatically when someone first runs a session in the repo and confirmed by the project owner through ask_user.

The project id is Beacon's: sha256: followed by the SHA-256 of the lower-cased, slash-normalized remote URL, or of the project path when there is no remote (cli/beacon/internal/learning/store.go:193-199). It needs normalization first (strip .git, map git@host:org/repo to https://host/org/repo), since today an ssh clone and an https clone of the same repo produce different ids and split its memory.

Replication runs over append-only logs, one per author and scope. Each machine numbers the entries it originates in each scope without gaps, peers exchange Sync { want } with the highest sequence they hold for each (author, scope) pair, and the sender replies with the missing entries for scopes the receiver belongs to, then any bodies it lacks. The logs are split by scope because filtering a single log would leave gaps that a receiver could not tell from loss. This is the read-union model Beacon already uses for git notes (refs/notes/beacon plus beacon-remotes/*, merged at read time and never written into a peer's ref), applied to logs instead of a ref.

A lesson's entry holds its header (id, kind, scope, author, evidence pointers, review references) and the hash of its body (text, applicability and quote, hashed together with the lesson id so two lessons never share a body), and bodies are kept in a separate content store keyed by that hash. The author signs the entry, not the body, so a header stays verifiable after its body is deleted, and the log stays append-only while a body can still go. A review is an entry in the reviewer's log, and a receiver treats a lesson as verified once it holds both entries.

A tombstone replicates with the same scope as its lesson. Receivers delete the body from their content store, keep the header and the tombstone so a late copy of the body is refused by its hash, and re-render every projection that cited the lesson. That is the forgetting rule Freyja's orchestrator design uses for personal memory, extended to a team. It removes a lesson from every agent's context and every daemon's store, but not from a copy a person saved outside the daemon, and the retraction prompt should say so.

Recall

Recall uses all three of Reason Network's tiers, with a different trigger for each. Pull is the default, and push is rare and always passes the anchor's gate.

Tier Trigger What the agent receives
Pull the agent calls search_memory Beacon's MCP tools, serving merged results (own, project, team), each tagged with origin, reviewer, age and a verified bit
Turn presence matches a lesson's evidence files, applicability or error text one framed line at the next tool result: <team-memory id="m_8f2" scope="project:acme/api" reviewed-by="bob" verified> and the lesson text
Push a verified correction whose evidence matches a command the session is about to repeat the same frame mid-turn, only after the anchor's gate allows it

Ranking for pull and turn recall combines five signals: scope (narrowest first), text relevance (SQLite FTS5 over text, applicability and tags, which replaces Beacon's AND of substrings), overlap between the lesson's evidence files and the session's current files, an outcome rate, and age. The outcome rate is a smoothed success rate over sessions that cited the lesson, (successes + 1) / (citations + 2), so a new lesson starts at one half and moves with use. Superseded and retracted lessons are never returned. A lesson borrowed from another project is labelled as a hypothesis until it has been cited in a successful session in the new one.

The citation comes from text the agents already write. Beacon's recall skill asks an agent to say "Using Beacon memory memory_…" when it uses a memory; the daemon sees that text in the agent's message events and writes lesson.cited with the session id. The outcome is the Beacon evaluator's task-success score when a session is evaluated, and otherwise a proxy: the session's tests pass after the citation, and no revert of its changes appears in the next day's commits. A lesson cited before a failure, or whose evidence files change in a way that breaks the behaviour it describes, gets lesson.contradicted and returns to its reviewer.

6. The graph

Schema

Node Created from Tense
person, machine roster entries timeless
session presence, session.started present while live, past after session.ended
repo, project normalized remote URL timeless
file file.read, file.modified present while open, past afterward
commit git, session.commit_linked past
message Reason Network mail past
approval, decision Review records; a decision is an approved request with a stated effect past
lesson lessons log past (verified), future (proposed)
obligation decisions, standing requests, promises in messages future
Edge Source
session touched file Beacon file events (path, diff hash)
commit came from session Beacon git notes (session.commit_linked, file-overlap inference within 2 to 24 hours)
commit touched file git
lesson derived from event lesson evidence pointers
review signed by person, about message or lesson Review signature and about
decision produced obligation the decision's stated effect ("remove the grace by 10-28")
obligation waits on event its condition
person owns file CODEOWNERS, or recent approved decisions and sessions

The graph is two SQLite tables, nodes(id, type, tense, attrs, t_start, t_end) and edges(src, dst, type, t, evidence), built incrementally from the logs on each machine and on the anchor. Each machine's graph covers its own full-content events plus the metadata its teammates share. The anchor's graph is the union of what members share, plus lessons and any history members opt into.

Queries

Agents query the graph through the directory tools they already have. list_team gains a filter on files and topics, and three tools are added.

Each who_knows call that leads to a message writes a routing record with the context, the candidates, the choice and, once the exchange ends, the outcome (answered, helpful, redirected, ignored). Freyja's orchestrator design specifies this ledger as the supervision for a learned router.

Obligations

An obligation has an owner, a condition, an action, an optional deadline and a source. Conditions are expressions over events, and Beacon already ships an engine for that: its 77 detection rules are CEL expressions over the same event schema, with correlation within session windows. The anchor evaluates obligation conditions with the same engine as events arrive, for example e.event.action == "vcs.merged" && e.branch == "auth-rotation". When a condition fires, the action becomes a candidate item for the gate in section 7, never a direct interruption.

7. The anchor

The anchor is a member daemon with role=anchor in the roster, which is how Reason Network's design plans it ("just a member daemon on an always-on box", DESIGN.md:197). It has five jobs.

  1. Custody. Mail for offline machines (section 4).
  2. History. Members who opt in stream their metadata to the anchor, along with any messages and traces they choose to archive. The anchor indexes them with FTS5, which Beacon's opt-in history.db already uses locally.
  3. Runtime. Freyja's always-on platform as its design describes it: workflows with triggers and steps, persistent watchers over CI, review queues and deploys, capabilities denied by default, and an audit log for every check.
  4. Gate. The single place where proactive items become interruptions.
  5. Work. Tasks people hand it when they stop for the day and fixes for failures that a test reproduces, run in sessions of its own and stopped at drafts.

The gate

on candidate item c:                         # from a session, a watcher, an obligation or a loop
  p = owner(c)                      # the graph: code owner, obligation owner or standing request
  if floor(c):                      deliver(c, channel = prefs[p].alarm_channel); return   # fixed in code
  if not prefs[p].allows(c):        ledger.silence(c, "preference"); return
  if seen(c.fingerprint, window):   ledger.silence(c, "duplicate of " + prior); return
  u = urgency(c)                    # info | attention | alarm; rules first, a model only when rules disagree
  if u == alarm and prefs[p].alarms: deliver(c, channel = prefs[p].alarm_channel); return
  if quiet(p, now):                 ledger.hold(c, until = quiet_end(p)); return
  if u == attention and budget[p].take(): deliver(c, channel = prefs[p].channel); return
  digest[p].add(c)                  # the morning or evening note

Urgency comes from records the anchor can check: CI state, deadlines in the graph, whether a person or session is blocked. The proposing agent's own wording never raises it, which also means an issue that says URGENT cannot page anyone. Floors run before preferences, so no proposal and no learned preference can lower them; the first floor is that a security finding always reaches the owner of the code. Every branch writes to a ledger with the item, the rule that decided and the time, so "why didn't you tell me" has an answer and so does "why did you tell me". The per-person budget (attention-level items per day) and the preferences are hive additions, kept as each person's own settings and delivered through Freyja's proposed notification fabric, whose design distinguishes info, attention and alarm (ALWAYS-ON-PLATFORM.md §8.8; §2.6 lists native notifications as missing today).

The gate is measured in both directions, because both failures are common. A team of agents produces more candidate items than any person can read, and each agent rates its own item as important. The opposite also happens: Freyja's four-day observation of Muse, an always-on assistant, found that its proactive composer ran 21 times and sent nothing (SLACK-ORCHESTRATOR.md:272). The ledger yields both rates: the share of delivered items a person acted on, and the share of held or silenced items that a later event showed should have been sent (the person asked about it, or the issue it described escalated). A silent gate also looks like a broken one, so each digest lists what the anchor checked and how many items it held, which is the report Freyja's orchestrator design already requires of every background loop (a run ends with an output or with no_change and what was checked, SLACK-ORCHESTRATOR.md §7.4). Before the gate sends anything, it runs in shadow: it writes the digests it would have sent, people mark what they wanted, and it starts sending when the missed-item rate on that record is acceptable to them.

Prepared work

Anthropic's Claude Duet works this way for one person: it "reads, drafts, and prepares work on its own", and before it sends a message, makes a payment, signs a document or deletes anything, it shows the exact action and waits for approval (announcement, October 21, 2026). Freyja's always-on design already runs unattended work as workflow runs with capabilities; the hive adds what a team needs around those runs.

Work reaches the anchor in two ways. A person ending the day hands a session over with Beacon's handoff brief, exactly as they would hand it to a teammate (section 10), or a watcher or obligation starts a task when a failure reproduces under test, for example a dependency pull request that fails the auth suite. The anchor runs each task in a session of its own on a clean checkout, registered in the directory as anchor/<name> like any other session, so teammates' agents can see it and message it.

Tier Actions Rule
Free read the repository and CI logs, run tests, push branches under hive/ no approval
Approved open a pull request, comment where people will see it, roll back a deploy a signed approval from the owner
Outside change CI or repository settings, push to protected branches never granted, so no approval can enable it

The tiers map onto Freyja's capability model (ALWAYS-ON-PLATFORM.md §8.7), which denies by default. Secrets follow its rule (§7.10): injected into the run's environment only, never logged, never written to a transcript, never sent to the model. Each task carries llm.spend limits per run and per day, and a run that exceeds them pauses with an attention notification (§7.5). A task that reaches a decision only a person can make stops and writes the question down instead of working around it.

The result is a prepared item: the diff, the failing test it fixes, the evidence and the base commit, delivered through the gate to the owner the graph names, with three answers: approve, edit or skip. The answer is a signed Review (section 5). Reason Network's Approval, which Review generalizes, records the request text as shown (clipped to 500 characters) and the answer, with nothing that binds it to an exact change and no expiry (Part 1, section 12). For prepared work the Review also carries diff_hash and base, so one approval covers one change; if the base moves before the answer arrives, the anchor re-runs the tests and asks again. An edit is the most informative answer. The owner's version goes in edits, and the anchor's draft, the approved version and the difference between them go to the quiet pass as evidence for a correction lesson. Skips feed the nightly review of each person's preferences.

Cost

The most common regret in Freyja's hosting research was idle token burn: heartbeats that resend a large context every 30 minutes (docs/research/anywhere/vps-deployment-patterns.md:111). The anchor's tick is a deterministic loop over queues, deadlines and obligation conditions, and it costs nothing when every queue is empty. A model runs only when a trigger, a message or a pending item needs a decision, and the hourly and nightly passes run in batches with budgets.

Failure

If the anchor is down, messaging between online machines continues as it does today and offline mail falls back to the sender's outbox. Obligations and watchers pause, and on restart the anchor replays the logs it missed, so a condition that became true while it was down still fires (late, and marked late).

A team can run two anchors, with the roster naming one as each person's gate and the other as standby, and the standby takes over after the first has been unreachable for a set interval. That is failover without fencing: during a partition both anchors can believe they hold the gate. Every item therefore has a stable id and the two anchors replicate their delivery ledgers, so the usual cost of a split is a repeated notification, which both ledgers record. A team that cannot accept that should run one anchor and accept its downtime.

8. Proactivity across the team

Personal proactive agents have converged on one design. xAI's Grok Bot (August 11, 2026), Meta's Muse (September 8), OpenAI's dots (September 29) and Anthropic's Claude Duet (October 21) each give one person an agent with its own computer that reads connected sources, prepares work while its owner is away and asks before anything leaves the draft stage. The research for this report found no product that updates every repository that depends on a changed API, notices that one person is the only reviewer of a module, rescues a stuck agent session (Claude Tag offers a manual !restart), or earns autonomy from a record and revokes it automatically. Each of those works across people, and each needs records the hive already keeps.

This section adds six extensions on top of the graph (section 6), the gate and prepared work (section 7). All six follow constraint 8, all six run in shadow first, and the measures for all six are listed at the end.

Changes

Contract index. The anchor builds a per-repository index nightly from code evidence: exported endpoints, schema fields and public symbols, and the repositories that consume them through imports, dependency manifests and calls to known endpoints. CodeRabbit's automatic repository linking (June 2026) derives edges from the same evidence for review-time impact reports. Every edge keeps its evidence (file and line), so a wrong edge can be traced and removed.

Symbols in presence. A member's daemon adds the exported symbols a working diff touches to its presence (symbols: ["acme/api:token.kid"]), computed locally. The diff stays on the machine, as constraint 3 requires.

Speculative merges. For pushed branches that share a file or a contract edge, the anchor merges them in a scratch checkout, builds the result and runs the affected tests. This is Crystal's method (Brun et al., ESEC/FSE 2011). In the histories of Git, Perl5 and Voldemort, textual conflicts were 67% of all conflicts; the other third merged cleanly and then failed to build or pass tests. Only a failure produces an item, sent to both sessions at their next turn. Unpushed work keeps the file-overlap notice from section 10.

Decision checks. When a Review approves a request with a stated effect, the anchor retrieves the decisions and lessons that share an entity with it (a file, a config key, a token type) and asks a model whether they conflict. A likely conflict becomes an item for every signer involved, showing both records with their signers and dates. GroupMemBench (arXiv:2605.14498) found that group memory fails when a newer statement overwrites a conflicting older one, so the graph keeps both, attributed. Only signed decisions are checked against, so an agent's unsigned choice never acquires a person's authority.

Change sets

A change whose contract edges reach other repositories becomes a change set, run by one durable workflow on the anchor that sleeps between triggers.

pub struct ChangeSet {
    pub decision: String,           // the Review whose effect started it
    pub producer: Change,           // repo, branch, merged commit
    pub consumers: Vec<Change>,     // one per consuming repository, each with its own Review
    pub evidence: Option<Check>,    // e.g. tokens without a kid in the API's logs, per day
    pub closes_when: String,        // CEL over events: every change deployed and the evidence check passing
    pub reopens_when: String,       // CEL: the evidence reappears
}

pub struct Change { pub repo: String, pub branch: String, pub owner: String, pub review: Option<String>, pub deployed: Option<i64> }

For each consumer the anchor applies a deterministic transform when one exists (a codemod or an OpenRewrite recipe) and uses the model only for the remainder, which is Moderne's documented approach to multi-repository changes. It verifies with that repository's own build and tests against the producer's merged commit, pushes under hive/, and asks the owner through the gate. Pi Durable, released with Pi 1.0 on October 1, 2026, shows the runtime shape a week-long change set needs: checkpointed tasks, sleep until a time or event, and exactly-once submissions.

A change set closes on observation. Cleric's account of verifying its own fixes found two failure modes that apply here: "merged to main" was mistaken for "rolled out", and a verifier's confidence grew across recurrences with no new evidence. So closes_when requires a deploy event and a passing evidence check, never an agent's report, and reopens_when reopens the set if the evidence returns, which is how Claude Security handles the findings it tracks.

Obligation sources

Dangling edges. Each tick, the anchor queries for work in a ready state with no next actor: - a pull request that passed CI more than a set time ago with no reviewer; - a session that ended with unchecked plan steps and no handoff; - an ask_user question past its deadline; - an obligation whose owner is away (calendar or presence); - a decision with no implementing commit.

Each becomes a candidate item for the next owner, with the review brief or handoff attached.

Promises. The quiet pass on each member's daemon proposes commitment sentences from that member's own sessions as obligations (owner, condition, action, a pointer to the source event). The content stays local until the owner accepts or skips the proposal in the morning digest, and skips are labels for the extractor. Agents record promises to each other with a directory tool, owe(to, condition, action), so those need no extraction.

Absences. The anchor computes reviewer concentration per path, meaning the people who approved changes there in the last 90 days, and checks it against calendars. A path with one reviewer who is about to be away produces a handoff-brief request to that person's agent and a prepared change to the code-owners file.

Lifetimes. Every obligation and standing request has an expiry and lapses unless renewed, and the digest lists the ones about to lapse. Claude Tag's channel follows end after seven days of thread silence, and Claude Code's recurring tasks expire after seven days; both are precedents for expiry by default.

Rescue

Signatures. Each member's daemon computes stuck signatures from its own events and publishes them as metadata. Starting settings: - the same command hash and exit code three times within 15 minutes, with no file.modified in between; - a run of turns without file changes; - a spend rate well above the session's median.

Matching. A failure signature is the normalized command, the failing test and the error class, hashed locally. The anchor matches it against: - lessons whose evidence carries the same signature; - sessions whose events show the signature followed by a passing run; - live sessions touching the failing files (who_knows).

Delivery. A match goes to the stuck agent as a turn-tier message with the pointer. The person hears only past a threshold they set. No one else hears.

Implicit standing requests. A who_knows(error) query or an ask_user about a failure records the asker against its signature. When a merged commit makes the signature pass, the asker gets an info item.

Breakpoints

The gate's attention branch waits for a breakpoint, up to a limit per urgency:

  if u == attention and budget[p].take():
      b = next_breakpoint(p, limit = max_hold[u])   # commit, test run finished, session idle 60 s, PR opened
      deliver_at(c, b, channel = prefs[p].channel); return

Breakpoints come from presence. Iqbal and Bailey (CHI 2008) found that notifications deferred to task breakpoints caused less frustration and faster reactions than immediate delivery, at a mean deferral of 88.6 seconds; the limit per urgency is Horvitz's bounded deferral (UM 2005). A false breakpoint costs more than a missed one, so detection favors precision.

Per-source shares. Each watcher and each proposing session gets its own share of a person's budget, so one noisy source cannot spend all of it.

Review requests draw from the same budget, prepared items included. In a Harvard study of 718 firms using Jellyfish data, the average time to review a pull request rose 49% with AI coding tools, and the share needing changes doubled. When several items wait, the digest orders them by how much each unblocks: the number of sessions, obligations and branches in the graph that wait on it.

Preparation in idle time

Candidates are needs the graph can predict: unfinished plans in the day's sessions, requested reviews, scheduled operations such as a migration, and recurring failures.

Scoring. Each candidate scores P(used) × value − cost. P(used) is estimated per kind and per person from a use log, and kinds with low use rates are dropped. Sleep-time compute (Lin et al., arXiv:2504.13171) cut answer-time compute about fivefold at equal accuracy on its benchmarks, and the gain grew with how predictable the query was from the context.

Execution follows PASTE's rules for speculative tool calls (arXiv:2603.18897): - results stay outside shared state until used; - side effects run only as a dry run or against a copy, such as a migration run against a snapshot; - jobs are preemptible and yield to real work; - the night has a spend limit.

Output is session cards with evidence attached, review briefs and rehearsal reports. Each is read on demand or attached to an item the gate is already sending.

Standing approvals

pub struct StandingApproval {
    pub id: String,
    pub kind: String,               // "dep-bump-patch"
    pub repo: String,
    pub perimeter: Perimeter,       // paths, change types, required checks: all checked mechanically
    pub earned_from: Vec<String>,   // the Reviews that earned it
    pub sponsor: String,            // signs, renews, and is told before expiry
    pub expires: i64,
    pub uses: Vec<String>,          // each use, also listed in the sponsor's digest
    pub suspended: Option<String>,  // revert, edit request, out-of-perimeter change, failed post-merge check
    pub sig: Signature,
}

Proposal. A kind becomes eligible when its record reaches a threshold the team sets: approvals as prepared, with none edited or reverted. The anchor then proposes the approval to the code's owner.

Use and suspension. Each use is listed in the sponsor's digest. Any suspension reason in the struct suspends the approval on its own, and revoking it is one command.

Floors. A standing approval cannot cover security findings, CI or repository settings, or a change that removes a check.

Why the perimeter is mechanical. Anthropic's account of its auto-mode classifier gives the reason: its misses on real overeager actions came mostly from finding approval-shaped evidence without checking that the consent covered the action's blast radius. A perimeter of paths, change types and required checks leaves nothing to interpret.

Who can change it. Only roster people can create, renew or revoke a standing approval, and no capability tier lets an anchor session write one. That answers Rootly's question of whether the AI can alter the policy controlling its own action.

Precedents. Each covers one piece: - Renovate's automerge rules: a class of updates plus passing checks. - Entra Agent ID access packages: expiry plus a sponsor who must renew. - Cursor's Bugbot learned rules: promotion and demotion from how people react.

Measures

Area Measure Ledger
Changes conflicts and contract breaks first seen at merge or later; time from a producer merge to every consumer updated speculative-merge results, CI on main, change sets
Obligations time from ready to the next owner acting; obligations past due dangling-edge queries, the obligation ledger
Knowledge minutes agents spend stuck on failures with a known fix; time from a fix to the askers being told stuck signatures, rescue records
Attention interruptions and review requests per person per day, against the weeks before the hive the gate ledger
Compute share of prepared work used; cost per used preparation the use log, spend records
Authority reverts of actions taken under standing approvals; time from revocation to effect standing approvals, git

9. Learning

Loop Runs Inputs Outputs Who checks
Route and ask per question, seconds graph, presence messages, answers, routing records the recipient; a person via ask_user when a decision is needed
Quiet pass hourly, on the anchor or each machine the hour's sessions (Metadata-only on the anchor, full content locally) proposed lessons, project-page updates each lesson's reviewer
Dream nightly, on the anchor raw events (shared metadata, plus traces members archive there), lessons, ledgers reconciled lessons, a short team posture, proposed routing rules, skill changes and changes to each person's interrupt preferences (from what they acted on, edited or skipped) people, in the morning digest
Training weekly ledgers, trajectories, lesson outcomes datasets, then router and gate checkpoints shadow and advise metrics

The quiet pass needs content, and content stays on its owner's machine. The pass therefore runs in two places: on each member's daemon over that member's full events, where it can draft lessons with evidence, and on the anchor over the shared metadata, where it can update project pages and notice patterns across people (two sessions hitting the same failing test). The evaluator Beacon calls today is a hosted service, but its options already accept a provider, endpoint, key and model (EvaluatorOptions), so a team can point it at a model it runs itself, on the anchor or locally.

The nightly pass works from raw events and lessons, never from the previous night's summary. CliffCompaction (Nguyen et al., arXiv:2609.26779) is the reason: each pass should work from original content instead of summarizing a summary.

Training data comes from four ledgers the hive writes anyway: routing records (context, candidates, choice, outcome), gate decisions with their later outcomes, lesson citations with session outcomes, and Freyja's trajectory export, whose current version 5 keeps exact tool arguments and results (src/shared/sessionExport.ts:276). Converters to ATIF and ShareGPT exist but accept only version 3 exports (scripts/convert-to-atif.py:307, scripts/convert-to-sharegpt.py:139), so updating them is part of the build. Preference pairs are still a planned phase in TRAJECTORY-TRAINING.md (Phase 4, pair construction). A team supplies the raw material for them naturally: when two agents attempt the same task, an outcome such as a merged change or a passing test picks the chosen trajectory without a separate labeling step.

A learned component earns authority in the three stages Freyja uses for its Jev decision models. In shadow it predicts and is scored against what happened. In advise its prediction is shown to the model that decides. In gate it handles the cases where it is confident and passes the rest on. Promotion waits for data: in Kumar and Saminathan's study of task- and session-level routing (arXiv:2608.14641), three of four production routers gave the same answer whatever the input, and a fixed mid-tier baseline matched them.

10. Collaboration flows

Asking the right session

alice/api-claude                 graph / directory                bob/api-codex            bob (person)
      │ who_knows("src/auth.rs", "401 invalid signature")                │                         │
      │──────────────────────────────►│                                  │                         │
      │◄── bob/api-codex: live, editing src/auth.rs, its session wrote c7f2                        │
      │ send_message(to=bob/api-codex, "...401 since your rotation...")  │                         │
      │──────────────────────────── push (codex queue) ─────────────────►│                         │
      │◄────────────── reply: "since c7f2 the verifier requires kid" ────│                         │
      │                                                                  │ ask_user(about=msg id)  │
      │                                                                  │────────────────────────►│
      │                                                                  │◄── approve + note ──────│
      │◄──── Signed<Approval> (sent by bob's daemon, not by bob's agent) ┤                         │
      │ graph: decision{approver: bob} + obligation{remove grace by 10-28, owner: bob}             │

Routing is the new step at the start. The transport after it exists in Reason Network today: the push into Codex, the framed reply, ask_user with the requester's words shown first, and the Signed<Approval> that the daemon sends on its own (Part 1, section 12). The decision and the obligation at the end are hive additions, the graph's record of what the approval meant; in the hive's record format the approval becomes a Review.

Two sessions on one file

When file.modified events for the same normalized repo and path arrive from two live sessions within an interval (30 minutes, to tune), each daemon queues a turn-tier notice for its own session naming the other session and the file. Reason Network planned this warning as M4 work, sourced from PostToolUse edit paths (DESIGN.md:355); Beacon's events add a diff hash, which shows whether two edits are identical; overlapping lines would need hunk ranges, which each daemon can share as line numbers without the text. The warning goes to the agents, not the people, since the agents can coordinate in seconds and usually should.

Handing a session to someone else

Beacon's handoff brief is Markdown of at most 48 KiB, redacted, with safety lines at the top. It fits inside a Reason Network message as an attachment (up to 10 files of 256 KB each and 512 KB in total, Part 1, section 13), which is authenticated with the envelope and saved on the receiving side as a non-executable file. The receiving daemon starts the new session with Beacon's resume prompt, including its handoff marker, and the sending daemon signs the marker. Beacon treats that marker today as unverified prompt text, not a verified identity; with a signature, Beacon's session.handoff link between the two sessions becomes verified provenance across people.

Starting a session in a project

A new session gets a session card at the earliest hook its harness lets the daemon add context to. Claude Code's SessionStart hook accepts additionalContext; Reason Network skips injection where a hook cannot add context (prompt-submit in Cursor, Copilot and Grok, and Grok's session start, src/hook.rs:107-113), and those sessions get the card at their next supported turn hook or through pull.

The card is built from the project page: conventions and gotchas ranked for the files the session opened, live sessions working nearby, recent decisions with their approvers, and obligations that touch those files. Freyja's orchestrator design uses session cards for the same reason the Delegation Danger Band paper (Hu and Ramachandran, arXiv:2610.00041) gives: selective handoff beat both full inheritance of context and a fresh start.

11. Trust and privacy

A hive connects agents across people, which turns every channel into a path for injected instructions. A teammate's message, a replicated lesson and a shared skill all end up in some agent's context. The ClawHavoc campaign showed what a public channel without review looks like: in February 2026, 341 of the 2,857 skills on ClawHub (11.9%) were found to be malicious. A team's channels are smaller and reviewed, and the defenses below assume a teammate's agent can be talked into forwarding hostile text.

Inbound screening. Every message passes Reason Network's framing and defang, which turns any bracket that could open a frame into [, including look-alike Unicode brackets and spellings like r3as0n. It then passes Beacon's prompt-injection rules, which today run only after the fact in beacon scan and which the daemon would run live on the message text before delivery (prompt-injection/ignore-previous-instructions at medium severity, prompt-injection/exfiltrate-context-secrets at high, and the rest of the family). A match holds the message and asks the recipient's user through ask_user whether to deliver it.

Outbound screening. The daemon records every team message it sends as an event. Beacon's secret-read-then-egress correlation matches a credential-file read and a curl, wget or nc command in either order within 120 seconds (rules/context-exfiltration/secret-read-then-egress.rule.yaml:20-35); the hive adds that send event as a fourth kind of egress. When the secret read comes first, the daemon holds the message for its user before it leaves; when the message comes first, the match can only arrive after the send, so it becomes a finding that the anchor escalates.

The /terminal exemption. Both the group guard and the loop guard exempt any sender whose address ends in /terminal, and the CLI accepts --from <you>/terminal from any process (src/daemon/mail.rs:50-54, :127; src/main.rs:417-423), so an agent with shell access can pose as the terminal. Agents run as the same OS user, so a token file would not help. The fix with the same strength as approvals is to confirm group sends and loop-guard overrides through the dialog engine ask_user already uses: "Send this to all four sessions in acme/api?" An agent cannot answer that dialog without controlling the machine, which is the same speed bump Reason Network accepts for approvals.

The anchor's own work. Overnight tasks read issues, pull requests and CI logs that strangers can write. The anchor's sessions get the same inbound screening as messages, its tokens follow Freyja's secret rule, and its capability tiers mean the most an injected instruction can produce is a draft branch that a person has to approve.

Lessons as a delivery channel. A poisoned lesson is worse than a poisoned message because it is retrieved again and again. Lessons therefore require a signed review by a roster person, arrive labelled with origin, reviewer and verification status, pass the same inbound screening, and can be retracted from every daemon at once. Skills generated from lessons go through git review like any other change, which keeps Beacon's layering: mesh-delivered memory is the fast, scoped tier, and committed skills are the slow, reviewed one.

Privacy. Presence and the shared graph use the Metadata-only projection, and file paths pass the new path filter from section 3. History on the anchor is opt-in per person. Beacon's auto-share rule applies in the hive unchanged: a repository's committed settings can turn sharing off for everyone but can turn it on only for people who enabled it themselves (Part 2), so cloning a repo never publishes anyone's sessions. One existing behaviour deserves a decision: verified sign-in proofs include the person's email, and the full proof replicates to every member machine (Part 1, section 19). The hive should strip the email from proofs before replication or state the exposure in the join flow.

12. Build order

Each milestone is useful before the next one exists.

# Milestone Reuses New Measure
1 One adapter, one daemon Beacon capture and hooks; Reason Network daemon, mesh, tiers presence from events, hook-as-turn-tier, files in presence, overlap notices cross-agent questions per week; overlaps caught before merge
2 The anchor as a member Reason Network mesh and roster; the anchor role its design plans (DESIGN.md:197); Freyja's always-on platform design role=anchor roster field, custody with Signed<Message>, opt-in history, FTS search, the notification fabric mail delivered across non-overlapping online windows
3 Signed lessons Beacon learning store and MCP tools; Reason Network approvals LessonBody, Review, Tombstone, project membership, hive/1 frames lessons cited per session; retraction and correction rates
4 Graph and project pages Beacon git notes and file events; Reason Network directory graph tables, who_knows, owed, session cards, CEL obligations answer rate and time for routed questions; redirect rate
5 The gate and the loops Freyja's quiet pass, dream and tick designs ledgers, budgets, preferences, digests, a shadow period for the gate acted-on rate for delivered items; missed-item rate for held ones
6 Prepared work Freyja runs, capabilities, spend limits and secrets; Beacon handoff briefs; Reason Network approvals anchor sessions, the hive/ branch prefix, capability tiers, diff-hash and base-commit fields on approvals, the edit-to-lesson path share approved as prepared, edited or skipped; re-check rate
7 Work across the team the graph, the gate and prepared work; Beacon's CEL engine; Freyja workflows contract index, symbols in presence, speculative merges, decision checks, ChangeSet, obligation sources, stuck signatures, breakpoint delivery, the preparation scheduler, StandingApproval the six measures in section 8
8 Training in shadow Freyja trajectory export; Jev promotion stages routing and gate datasets, shadow scoring agreement with outcomes before any promotion

13. Open questions