CLI reference¶
The synapse command exposes the following subcommands.
For end-to-end examples that combine these commands with existing agent tools, see the Integration demos.
Start-up is proportional to the command you run: the CLI imports only the
module family that owns the requested subcommand, so frequent short calls
(synapse who, synapse send, wake loops) skip the import cost of the rest
of the surface. synapse --help, --version, and unknown commands still load
everything, since they need the whole command table.
| Command | What it does |
|---|---|
synapse hub |
Run the coordination hub. |
synapse commands |
List every subcommand by stability tier, or inspect a measured profile with --profile first-use|core|adapters|governance|labs|all and optional --json. |
synapse completions |
Print a static tab-completion script for bash, zsh, or fish, generated from the installed CLI. |
synapse demo |
Run the real Claude/Codex claim → conflict refusal → mutation denial → handoff → verified receipt path and write JSON plus a static dashboard (--output DIR selects the destination). |
synapse benchmark |
Benchmark the installed package (event store, relay encoding, live hub round-trips) and print a scorecard with honest host context; --compare BASELINE.json gates the run against a saved scorecard, exit 1 on regression; --trend STORE.db accumulates runs and renders per-metric sparkline trends (--ascii for a printable-ASCII trend block); --alert gates the run statistically against its own same-context history, exit 1 on drift. |
synapse quickstart-coding |
Create a coding-fleet workspace, run the live overlapping-claim refusal demo, and print a success marker. |
synapse fleet-init |
Empty machine to working fleet in one command: doctor (--fix), persistent workspace scaffold, provider-seat probe, demo smoke, and a printed next-steps plan. |
synapse new coding-fleet |
Scaffold a runnable two-agent coding demo workspace. |
synapse health |
Probe the hub; exit 0 if reachable, 1 if not (wired as a container healthcheck). |
synapse worker |
Run a model worker that answers on the channel. |
synapse worker-session |
Run a provider command with SYN_PROJECT/SYN_IDENTITY set and a waiter armed around it. |
synapse team |
Launch a hub plus one or two local workers in one shot. |
synapse mcp |
Serve identity-safe coordination tools over stdio, including bounded local-feed inbox and live status; an omitted --name resolves from an agreeing environment or <git-project>/mcp (see MCP server). |
synapse mcp-tools / synapse mcp-call |
List and call allowlisted tools on an external MCP server (outbound). Stable taxonomy codes distinguish invalid config (exit 2), deny-by-default access refusal (exit 3), and tool failure (exit 1). |
synapse sandbox |
Validate a capability manifest and pre-flight or run a .wasm tool against it (validate/test/run). |
synapse adapters |
Detect coding tools and wire them to the hub with a claim-aware adapter (list/install/uninstall). |
synapse a2a-card |
Print an Agent2Agent Agent Card projected from the live capability manifest. |
synapse a2a-conformance |
Print the local Agent2Agent conformance matrix. |
synapse a2a-serve |
Run the stdlib HTTP+JSON Agent2Agent bridge; an optional default-off gRPC subset has a separate security boundary described below. |
synapse a2a-client |
Outbound A2A HTTP(S) client: Agent Card discovery, message:send, and GET task against a second peer. |
synapse a2a-interop-trace |
Run an independent stdlib HTTP(S) client against a live a2a-serve bridge and emit a discovery + task-lifecycle interop receipt. |
synapse channel |
Manage private-channel membership and member-visible history; pair with synapse send --channel. |
synapse encrypt-key |
Generate and check at-rest encryption key files (needs the encryption extra to encrypt). |
synapse sqlcipher |
SQLCipher page-encryption ops for the hub event store: rekey via PRAGMA rekey, migrate plaintext → encrypted. |
synapse agent-tmux |
Wake an existing terminal-agent tmux session (Codex, Kimi, …) with a fixed safe prompt. |
synapse codex-tmux |
Codex-defaulted alias of agent-tmux. |
synapse dashboard |
Serve a loopback-only read-only live cockpit (fleet graph, board, claims, risk guidance, stream, receipts) over hub snapshots, plus /snapshot.json; --feeds-db adds durable store feeds including /postmortem.json?task=ID and /receipts.json, and --observed-peer HUB=URI adds advisory peer-hub rows. |
synapse route-task |
Recommend agents for a board task using local capability signals. |
synapse resource-bids |
Rank live resource offers for a board task without reserving capacity. |
synapse memory-recall |
Recall matching durable memory records from a local event store. |
synapse send |
Connect, send one message, optionally await replies, and exit. |
synapse wait |
Block until a message addressed to you arrives, then exit (a wake trigger). |
synapse listen |
Connect and stream channel messages until interrupted. |
synapse arm |
Keep a waiter armed, or use arm install --identity NAME [--start] to write a permanent Linux systemd user waiter with mailbox replay and Restart=always. |
synapse relay |
Decode and print a lite relay log a hub mirrored to a file. |
synapse ingest |
Stream durable event-store records since a sequence cursor. |
synapse event-query |
Query a hub SQLite event store for temporal task and coordination history. |
synapse multihub |
Observe/follow a peer log read-only, or explicitly recover a stopped local watcher's durable quarantine (see Multi-hub sync). |
synapse participant |
Probe or drive Participant Fabric providers: list reports each driver's readiness, ask runs one turn, exchange and convene run multi-party deliberations, costs reports per-session spend and telemetry from a hub event store. |
synapse deliberate |
Conclude a council into a sealed, verifiable export package (conclude/verify). |
synapse federation |
Exchange, import, list, and revoke operator-confirmed peer-domain bundles (offer/fetch/import/list/revoke); fetch displays fingerprints and never imports. |
synapse compact |
Apply event-store retention and optionally write an HTML archive report. |
synapse postmortem |
Build a replayable task postmortem from a hub SQLite event store. |
synapse debug |
Fork a task's reconstructed state at a sequence point (read-only what-if). |
synapse reproduce |
Fingerprint a task's authoritative history into a deterministic digest. |
synapse causality |
Trace coordination causes, effects, or counterfactuals over the event log — federated across hubs with --peer; contention weighs overlapping live claims and advises who yields; otel exports the graph as OpenTelemetry spans; health flags orphaned claims, dangling dependencies, and stale claims. |
synapse merkle |
Commit the event log to a Merkle root, prove event inclusion, and generate the receipt-signing keypair (keygen). |
synapse reliability |
Build evidence-only reliability memory from a hub SQLite event store. |
synapse trust-graph |
Query the evidence trust graph (receipts, stale claims, conflicts) as text, JSON, or Graphviz DOT. |
synapse accounting |
Record and report opt-in model cost/token usage from a hub SQLite event store. |
synapse fleet-scorecard |
Compose causality spans, opt-in accounting, live-claim contention, reliability findings, and optional benchmark history into an owner-only JSON bundle or a two-signal OTLP/HTTP collector push. |
synapse approval |
Request, decide, and replay human-in-the-loop approval gates from a hub SQLite event store. |
synapse ttl-advice |
Build read-only lease TTL advice from a hub SQLite event store. |
synapse auto-action |
Introspect the opt-in auto-action reactor and manage the durable armed policy the orchestration loop reads (show/arm/disarm/clear). |
synapse board |
Print the shared task/progress blackboard. |
synapse supervisor |
Run an LLM-free supervisor that re-offers stalled tasks. |
synapse manifest |
Print the capability manifest of advertised agents. |
synapse directory |
Print a read-only capability directory from live agent cards (discovery only). |
synapse who |
List the agents currently online and hub-authoritative mailbox pending counts, optionally for one project or this identity with --me. The full-roster view shows the 20 largest positive mailboxes plus total identities/messages; --all-mailbox-pending (alias --all) expands every retained positive identity. --observed-peer HUB=URI appends advisory observed@HUB peer rows. |
synapse status |
Print a one-line hub summary (online agents, active claims, this identity's mailbox pending count) for shell prompts and tmux status bars, the counts as JSON with --json, or a refreshing operator dashboard with --watch; exit non-zero when the hub is down; --observed-peer HUB=URI appends advisory peer counters. |
synapse state |
Print active claims and their checkpoints (a resume view); --observed-peer HUB=URI appends advisory peer claims marked observed@HUB. |
synapse dead-letters |
Print directed messages the hub delivered to no consume-live recipient — no socket, or only stale sockets without a recent reaction/live waiter — worst first with the syn inbox --as NAME drain remedy. |
synapse approvals |
Print the relays awaiting a second operator under the two-person quorum — the pending set of the per-hub approval ledger (enforced but otherwise invisible), oldest first, naming each pending action and its first requester. Rides in the same state snapshot the dashboard and cockpit read. |
synapse doctor |
Check common coordination misconfigs plus this identity's hub mailbox pending count; exit non-zero on a failure. --fix auto-repairs a down default local hub or missing waiter by installing and starting the user services; --json emits the verdicts for CI health gates. |
synapse init |
Print or install the local user services (hub, waiter, presence) as systemd units. |
synapse install-shell-hook |
Install auto-arming shell integration into Bash, Zsh, and Fish (idempotent, guarded block). |
synapse shell-hook |
Print the shell code that auto-arms terminals and wraps agent commands, for manual sourcing. |
synapse git-init |
Persist the local claim identity/URI/token-file path, install post-commit/post-merge auto-release hooks, and write a .synapse/ conventions guide. |
synapse git-claim |
Claim work scoped to the current git branch (see Git-native claims). |
synapse git-claim-check --staged |
Read the real staged index and fail unless one exact identity owns editable claims covering every path on this worktree and branch. |
synapse git-hook |
Install post-commit/post-merge hooks that auto-release a commit's claims. |
synapse git-release |
Release the claims whose paths a commit or merge just touched. |
synapse conflicts |
Predict cross-branch merge conflicts between overlapping claims; exit non-zero on a hit. |
synapse cross-repo |
Scan a directory of repositories into a dependency graph (manifests/CODEOWNERS as edges), flag provably conflicting version pins, and join live claims onto it; with --repo, exit 1 when a connected repository holds a live claim; --suggest-resolution names each conflict's odd-one-out declaration; --watch --notify-cmd runs a sink command on coordination-fact transitions. |
synapse verify-release |
Run declared verification commands and write an observed release receipt JSON; --merkle-db commits the coordination log's Merkle root into it, --signing-key attests it with the hub key. |
synapse policy-check |
Evaluate a release receipt against a policy file; advisory by default, --enforce to gate, --trusted-signing-key verifies the commitment's hub signature and emits a closed evidence verdict. |
synapse identity |
Inventory and audit declared agent identities for enforcement-rollout blockers. |
synapse acl |
Shadow-mode (non-blocking) deny-by-default ACL evaluation of candidate accesses. |
synapse role |
Grant, revoke, and list role-claim authorisations (grant/revoke/list). |
synapse lock |
Hold a lease while running a command, to serialise it across agents. |
synapse release |
Manually drop a claim you own (e.g. an --auto-release-on manual claim). |
synapse task |
Declare and update the shared task plan. |
synapse workflow |
Validate and compile a declarative workflow into blackboard tasks (validate/compile/plan/run); contention weighs overlapping live claims involving the workflow's tasks. |
First 60 seconds¶
The installed CLI has a source-checkout-free validation path:
python -m pip install synapse-channel
synapse doctor
synapse demo --output ./synapse-golden-demo
This is the measured first-use profile: three concepts, three shell commands,
no optional extras, and no implicitly started persistent service. Inspect that
contract, or the complete surface, without activating anything:
synapse commands --profile first-use
synapse commands --profile first-use --json
synapse commands
The unfiltered command prints all five stability tiers. A profile is a machine-readable discovery and package boundary, not a global feature toggle; advanced commands stay available and activate only through their documented extra plus explicit command or flag.
synapse doctor reports local wiring issues, including identity, hub exposure,
root-filesystem pressure, hub reachability, the current identity's waiter, and
directed messages nobody reads: one reader often answers to several names
(a terminal identity, an agent identity, a role name like
project/coordinator), and a message addressed to a name whose inbox no
cursor drains and whose waiter is absent lands durably in the feed while
waking no one — the human ends up relaying, the exact failure the bus exists
to remove. The doctor names such addresses with their message counts and the
remedy (syn inbox --as NAME, repeatable; a standing set goes in the
comma-separated $SYN_ALIASES, and each name advances its own cursor so
draining a role never consumes another reader's delta). The primary syn inbox
read is exact-identity scoped too: it uses the full resolved identity and an
identity-specific cursor, so one terminal cannot display or consume another
terminal's directed mail. syn inbox --project-wide is the explicit opt-in to
the broader project feed, while syn inbox --name PROJ/name reads one exact
address. A bare --as PROJ remains an explicit project-wide alias. On
a fresh machine, a missing hub or waiter can be a warning before services are
installed. synapse doctor --fix repairs the safely repairable findings: when the
default local hub does not answer or the waiter is missing, it installs and starts
the local hub, presence, and wake services, then re-runs the checks so the exit
code reflects the repaired state. Findings the services cannot repair — identity,
exposure, disk pressure, or any non-default hub — are reported with a remedy but
never touched. synapse doctor --json emits every verdict plus the overall
health as one JSON document for CI health gates (it refuses the mutating and
checklist flags so stdout stays a single document); synapse status --json
does the same for the status counts, sized for monitoring scripts.
synapse doctor --notify-cmd CMD additionally pipes any warn/fail findings
to the sink command's stdin — one line each, remedy attached, hub URI in
SYNAPSE_DOCTOR_URI — turning the diagnostics into a proactive alert: a
healthy run sends nothing, under --fix the sink sees the state after the
repair, and it composes with --json (stdout stays one document). Same
contract as cross-repo --notify-cmd: split without a shell (wrap in
sh -c '…' for pipes), best-effort, a failing sink never changes the exit
code.
Federated deployments can opt into peer checks without inferring ambient trust
state: repeat --federation-peer PEER=URI for the hubs to probe, add
--federation-cursor PEER=SEQ for the local consumed cursor when known, and
optionally pass --federation-store PATH to inspect imported bundle expiry and
revocation state. The peer probe uses the multi-hub log request path and reports
reachability, cursor lag (log_end_seq - cursor when the peer supports it),
measured clock skew from the peer welcome timestamp, and TLS certificate expiry
warnings. --federation-token TOKEN is sent only on these peer probes;
--federation-skew-warn-seconds and --federation-cert-warn-days tune the
warning thresholds. Add --federation-path PEER=MODE to declare the network
shape for certificate-pinned federation checks. Supported modes are
direct-mtls, tls-passthrough, tailnet, and tls-terminating-proxy; the
terminating-proxy mode fails because the remote peer pins the proxy certificate
and hub-side client certificates do not reach the hub.
synapse status --watch refreshes the line every --interval seconds (default
2) as an operator dashboard: each refresh opens its own probe connection so a
hub restart shows as an honest offline line, a TTY rewrites the line in place
while piped output appends one line per refresh, and --json --watch streams
one JSON object per line (NDJSON). --count N stops after N refreshes;
Ctrl-C is the normal way to stop an unbounded watch and exits 0. synapse
demo starts an ephemeral local hub and disposable Git repository, connects
Claude and Codex, enforces separate claims, refuses both a deliberate claim
conflict and the corresponding unsafe mutation, transfers authority by atomic
handoff, runs observed checks, and releases with a supported receipt. It
succeeds when it prints:
success: coordination demo completed
The command writes golden-demo.json and golden-demo-dashboard.html. Use
synapse demo --output ./synapse-golden-demo to choose the artifact directory;
without it, the command creates a temporary directory and prints both paths.
For post-release local fleet restarts, synapse doctor --redeploy-checklist
prints package, service, roster, durable-state, and git-hook checks. It does not
restart services by itself, and restart commands are withheld by default. The
default checklist shows the installed executable, exact live hub PID, live
roster, SQLite event log, and claim-aware hooks. Only after fresh owner
disruption authority may an operator add
--redeploy-authorize-restart-pid PID. That opt-in renders, but does not run, a
command that rechecks the exact PID inside the fail-fast
${XDG_RUNTIME_DIR}/synapse-channel-redeploy.lock host-local custody lock before
restarting the hub, presence, and wake-listener units together. The lock remains
held while the hub itself is unavailable. Never reuse a PID authorization from
an earlier process or session.
The local-hub dogfooding gate is mandatory after every new release tag: install the exact published artifact immediately, run the one authorised locked restart, and verify version, service health, roster/waiter reconnect, durable replay, and hook wiring. This release-specific hub adoption does not grant authority to close unrelated running applications.
synapse quickstart-coding creates a temporary workspace, runs the packaged
two-agent coding demo, removes the temporary workspace after success, and is
successful when it prints:
success: coding fleet demo completed
For a generated workspace with editable files and a runnable overlapping-claim refusal scenario:
synapse new coding-fleet ./demo-fleet
cd ./demo-fleet
python run_demo.py
The generated demo succeeds when it prints success: coding fleet demo completed.
synapse fleet-init [path] is the one-command version of the whole first-run
sequence — empty machine to working fleet:
synapse fleet-init # doctor, ./synapse-fleet scaffold, seat probe, demo smoke
synapse fleet-init --fix # let the doctor stage repair the default hub and waiter
synapse fleet-init my-fleet --seat claude --seat codex # plan exactly these seats
It runs the real doctor (a failing report is a printed remedy, not an abort),
scaffolds a persistent workspace (refused when non-empty unless --force),
probes every registered provider CLI without taking a turn, runs the packaged
live overlapping-claim refusal demo (--no-smoke to skip), and prints a
next-steps plan — waiter arming, per-provider worker-session seat commands,
git-init, dashboard — with the workspace's project name filled in. It starts
no daemon and adds nothing the bundled commands do not already do.
Fastest safe trial path¶
Use one self-contained path before changing a real checkout:
python -m pip install synapse-channel
synapse doctor
synapse demo --output ./synapse-golden-demo
The demo starts and stops its own local hub, uses a disposable committed Git
repository, proves separate claims and overlapping-claim refusal, denies a
mutation before handoff, permits it after handoff, and writes an observed
verification receipt plus a static dashboard. It needs no persistent hub,
provider CLI, Git hook, MCP host, or A2A bridge. The same exact three-command
block is regression-bound across the README, quick start, and this reference,
and its synapse demo command is exercised as a real subprocess.
After that proof passes, use synapse fleet-init --fix to prepare a persistent
local workspace, hub, and waiter, then run synapse git-init --name
trial-agent inside the real repository before an agent edits it. Optional A2A
interoperability is a follow-on in the A2A bridge guide; it is not a
prerequisite for first coordination value.
For the strict hub profile, supply every control that paranoid mode enforces:
synapse hub --paranoid \
--db ~/synapse/hub.db \
--token-file ~/.config/synapse/token \
--message-auth-key-file ~/.config/synapse/message-auth.keys \
--require-message-auth \
--acl-policy ~/.config/synapse/acl.json \
--require-acl \
--tls-certfile ~/.config/synapse/tls/server.crt \
--tls-keyfile ~/.config/synapse/tls/server.key
Paranoid hub mode refuses partial configuration: it requires a shared-secret token, durable event log, per-message authentication, ACL enforcement, and native TLS. It also requires a metrics bearer token when metrics are enabled, disables metrics query tokens and the insecure off-loopback override, and prints the hardening hooks that remain outside the profile. Keep token and HMAC entries in owner-only files; see Paranoid mode before exposing a hub.
The loopback-only --metrics-query-token-ok compatibility flag is deprecated,
warns when parsed, and is scheduled for removal in 0.101.0. Send metrics tokens
in the Authorization: Bearer header instead; URL credentials can leak into
logs, shell/browser histories, and proxy records.
Recovery: picking up after a restart¶
Nothing is lost when a terminal or session goes down — the feed, the plan, and the event log are durable. On return, catch up everything for your repo regardless of the instance id you now run as:
synapse relay ./feed.ndjson --project quantum --cursor ./quantum.cursor # missed messages
synapse board # the current plan
synapse state --owner quantum # your claims + resume checkpoints
synapse who --project quantum # who is live now
synapse dashboard --port 8765 # local read-only HTML/JSON view
A lapsed claim keeps its checkpoint, so re-claiming the task resumes from it rather than restarting.
synapse who, synapse status, synapse state, and synapse dashboard can add
an opt-in observed peer layer with repeatable --observed-peer HUB=URI. The peer
URI is pulled through the same multi-hub event-log request path as
synapse multihub follow, then folded locally and marked observed@HUB wherever
it appears. Observed rows are advisory: they never grant local claims, never
change the local hub's roster, and peer failures render as unreachable peer rows.
Use --observed-token for secured peer hubs, --observed-pin HUB=sha256:<hex>
to pin a self-signed wss:// peer's certificate (a pin naming an unfetched hub
is refused), and --observed-timeout to bound
each peer pull. When a peer welcome frame carries a usable timestamp,
observed-peer output also carries local-minus-peer clock skew: who prints
skew=+/-Ns, status includes the largest absolute skew, and JSON uses
clock_skew_seconds / observed_max_clock_skew_seconds.
synapse dashboard binds to 127.0.0.1 by default and reads roster, state,
board, and manifest snapshots from the live hub. The state snapshot carries
dead_letters — directed chats that reached no live connection, per target
with counts, so a blackhole shows up on the page instead of being discovered
by a human relaying messages. A hub started with a
dead_letter_escalation_threshold turns that passive visibility into an active
signal: when a target's undelivered count reaches the threshold, and each further
multiple of it, the hub broadcasts a one-line dead_letter_escalation notice to
every connected socket and journals an audit event, so a growing blackhole is
surfaced without polling. It never re-delivers a message — the ledger keeps
counts and names, not bodies — so escalation points a human or an orchestrator at
the blackhole rather than silently re-sending. The default of 0 disables it,
leaving the ledger's visibility unchanged. It serves / for the browser
view and /snapshot.json for local tooling. The snapshot also includes a derived
fleet section for live agents, -rx waiters, missing waiters, active and stale
claims, a task-dependency graph from blackboard task edges, branch-conflict candidates
from live git-scoped claims, ready and blocked board tasks, release receipt
notes, and optional A2A task counts. It also carries the live hub's pinning tag —
hub_version (the package version) and config_epoch (a fingerprint of the hub's
configuration posture) — so a cockpit can badge which hub build and configuration
it is watching and notice a deploy or a config drift; the hub's own /health
endpoint reports the same two values. Pass --a2a-state-file <path> to
summarise a persisted synapse a2a-serve --state-file <path> store in that
section. The task-dependency graph is read-only and does not mutate the
blackboard. Dashboard branch conflicts use the same declared-claim metadata as
synapse conflicts; they do not run git or apply --check-diff refinement. Use
--allow-non-loopback only behind trusted local network controls because the
page exposes agent names, claim scopes, branch names, and task text. Pass
--dashboard-token <token> to require Authorization: Bearer <token> on /
and /snapshot.json; when --allow-non-loopback exposes the dashboard and no
token is supplied, Synapse generates and prints a startup token. Add
--observed-peer HUB=URI to include peer-hub rows in the browser and
/snapshot.json; those rows stay advisory and are labelled observed@HUB.
The snapshot's risk section also enriches at most 20 ready tasks with at most
three explainable route-task candidates and three resource-bids candidates
per task. These are the same deterministic local scorers as the CLI, remain
advisory-only, and never claim work, assign owners, reserve capacity, or grant
execution authority.
Dashboard browser principals¶
For more than one browser identity, use --dashboard-access-file FILE instead
of the compatibility --dashboard-token. The version-one policy maps a token
file to a display principal, role, and (for write-capable principals) a distinct
hub relay identity:
{
"version": 1,
"principals": [
{"id": "review", "role": "viewer", "token_file": "viewer.token"},
{
"id": "ops-a",
"role": "operator",
"token_file": "operator.token",
"operator_name": "operator:studio/ops-a"
},
{
"id": "owner",
"role": "admin",
"token_file": "admin.token",
"operator_name": "operator:studio/owner"
}
]
}
Relative token paths resolve beside the policy. On POSIX, the policy and every token must be owner-readable regular files with no group/world permissions; symlinks are refused. Each UTF-8 bearer is 32–4096 bytes with no whitespace or control characters, optionally followed by one newline. The loader caps the policy at 64 principals and 256 KiB and rejects unknown fields/roles/versions, duplicate JSON keys, ids, tokens, or relay identities before binding HTTP.
chmod 600 ~/.config/synapse/dashboard-access.json ~/.config/synapse/*.token
synapse dashboard --operator \
--dashboard-access-file ~/.config/synapse/dashboard-access.json \
--cockpit-dist clients/cockpit/dist
An operator or admin entry requires --operator; a viewer-only policy does
not. --dashboard-access-file cannot be combined with --dashboard-token or
the compatibility --operator-name, because each write principal carries its
own attribution. The authenticated /dashboard-access.json response contains
only principal id, display role, current capability booleans, operator-arming
state, and the presentation trust boundary. It is no-store, varies on
Authorization, and contains no token, hash, path, or ACL rule.
Capabilities, not role names, decide which React controls exist. A viewer has
read only; operator and admin currently receive the same three shipped write
capabilities, with no invented admin verb. This document is a presentation
hint only: every POST rechecks the bearer and route server-side. Missing/bad
credentials receive 401, a known principal without the capability receives
403, and unarmed write routes remain 404; the hub then applies its own ACL
and durable audit as before. Without an access file, the existing one-token and
open-loopback behavior is unchanged.
With --feeds-db <hub.db> (--reliability-db is the same flag's original
name) the dashboard serves twelve feeds off the durable event store —
available when the hub is down, real sequences and timestamps, behind the
same dashboard bearer token as every other path:
/reliability.json— the same audit-signal report assynapse reliability(per-owner tallies and finding records anchored to event sequences, explicitly "audit signals, not scores"), which the cockpit's reliability panel consumes;/events.json?since=SEQ&limit=N— the raw event-log tail past a cursor, in the exact multihub snapshot shape (events,next_cursor, andlog_end_seq), so a polling client walks the log forward without loss or duplication;since=lateststarts at the log's end — the tail shortcut that spares a client the full history walk on a large log;/causality.json?seq=N|task=ID&direction=causes|effects— one causality query in the CLI's exact--jsonshape;task=IDresolves to the task's most recent recorded event, so a client can hop from a log row to its causal cone without knowing sequences (an unrecorded task is 404, not an invented anchor). Apresent: falseanswer carries anotenaming which absence it is: an event recorded but outside the coordination causal graph (chatter carries no causal edges), or no event at that sequence at all;/postmortem.json?task=ID— the same replayable task evidence assynapse postmortem, projected as JSON for cockpit links. The identifier is required and bounded before storage access; a task with no matching events returnspresent: falseand an empty timeline rather than invented history;-
/metrics.json— store-attested log metrics for the cockpit's metrics panel: total and per-kind event counts plus the same split over trailing hour/day windows, measured against the log's own final timestamp (never the wall clock) so the document is deterministic over a given log. Honest scope stated in the document itself: these are log metrics; the live process registry (connection gauges, handler timings) is the hub's own/metricsendpoint and is deliberately not duplicated here. -
/state-at.json?seq=N— coordination state (claims + board) reconstructed as of eventseqby bounded replay, in the live snapshot shape plusas_of_seqandlog_end_seq; deterministic (judged at the bounded event's own timestamp),seqclamped into range, presence/roster omitted (not journalled). /merkle-proof.json?seq=N— an RFC 6962 Merkle inclusion proof for eventseq, in the same shapesynapse debug merkleemits, so a cockpit row's verify button can check the row against the attested tree root; aseqthe committed log does not hold returns{"present": false}with a note, never a fabricated proof./health-anomalies.json— the honest hub-side alert surface: the orphaned, dangling, and stale coordination anomalies the causality graph makes visible, in the same shapesynapse causality --healthemits, with ananomaly_countfor a cockpit alerts badge. Fired alerts stay collector-side off/metrics; this is only what the durable log can prove./sessions.json— the opt-insession_metrictelemetry the fleet left in the log, in the same shapesynapse participant costsrenders: per-session token counts, cost, latency, and error/abstention rates, withtotalsaggregated across sessions. Every record carries theseqof the snapshot it was read from, so a cockpit joins a session's cost straight to its causal cone via/causality.json; each record's coordinationtask_id(from the note body) is the same join keysynapse participant costsreports. A log with no session notes reports emptysessionsand zeroedtotals, never a fabricated cost./waits.json— the pending coordination gates reconstructed from the plan: each non-terminal task blocked on a dependency that has not reached a terminal status, withwhois waiting (the task's suggested owner, or whoever declared it),on_whatdependency ids it is blocked on, andsincewhen it was declared, plus await_count. This is the "what is the fleet stuck behind" panel. Transient socket waiters (a client's-rxconnection) are not journalled and are omitted; this is only the coordination gates the durable plan can prove./operator-actions.json?since=SEQ&limit=N— the governed operator-action history reconstructed fromoperator_relayaudit events: direction, action, namespace, task, operator, origin/owner hubs, peer or local requester, status, reason, break-glass tag, detail, and realseq/tsjoin anchors. Ordinary releases without relay provenance are omitted./receipts.json?since=SEQ&limit=N— the universal receipt feed projected from receipt-bearing durable events: release/claim evidence, content-minimized claim and guard denials, delivery receipts, dead-letter escalations, identity-pin reclaim decisions, sandbox run attestations, approval/policy/verification notes, governed operator relays, cross-hub pointers, A2A validation notes, and postmortem notes in one shape (seq,ts,receipt_id,kind,subject,actor,status,summary,source_event_kind,payload). Every declared durableEventKindhas an explicit receipt/non-receipt disposition; ordinary state, chat, memory, and idempotency events remain intentionally omitted rather than being inferred as receipts.
Without the flag each endpoint answers 404 naming the remedy; an
unreadable store answers 503 rather than an empty document pretending the
log is clean. --federation-store <federation.json> adds
/federation.json — the imported peerings with provenance and the bundle
fingerprints operators compared in the exchange ceremony; namespace
outcomes are hub-runtime state no durable store carries, so that section
ships empty with the reason stated. --cockpit-dist <dir> serves a built
cockpit single-page app read-only under /cockpit/ (paths escaping the
directory or with unrecognised suffixes are refused).
Operator write-path (opt-in). --operator arms three write routes so the
cockpit can act on the fleet rather than only observe it:
POST /message{"to": "<name|group|all>", "text": "..."}— relay one chat message to the fleet.POST /task{"id": "...", "title": "...", "depends_on": ["..."]?}— declare a board task.POST /task/update{"id": "...", "status": "..."?, "note": "..."?}— change a task's status and/or append a progress note (at least one of the two).
The write-path is off by default — without the flag every route answers 404,
indistinguishable from an unknown path, and the dashboard stays a pure observer.
When armed, a write still requires a recognized dashboard bearer and exact
route capability, is rate-limited, and uses either the compatibility identity
operator:<name> (--operator-name) or its access-file principal identity,
never impersonating an agent. The relay reimplements neither authorisation nor
auditing: the hub applies its own ACL to the relayed frame and records it in the
durable log, so every operator action is authorised at the hub and shows up in
replay, /state-at, and the signal stream like any other frame. The response is
JSON — 200 when the hub delivered, dead-lettered, or applied it, 403 when the
ACL refuses it, 409 when the blackboard refuses the task on its own terms
(unknown id, empty title, dependency cycle, unknown status), and 503 when the
hub is unreachable.
Public status-page posture: the dashboard is read-only by default —
every endpoint answers GET only unless --operator is set (above), and
nothing on the read surface mutates the hub or the store — so a public
status page needs no special mode: expose
the dashboard deliberately with --allow-non-loopback, set
--dashboard-token (or accept the generated one), and point it at a
store copy with --feeds-db if you want it serving with the hub down.
What limits a public page is the data, not the verbs: the snapshot and
feeds carry agent names, task ids, and message subjects, so publish them
only where that operational metadata is fine to show. Every response also
carries browser-hardening headers (X-Content-Type-Options: nosniff,
Referrer-Policy: no-referrer, X-Frame-Options: DENY, and a same-origin
Content-Security-Policy), so the page cannot be framed and pulls no remote
resources.
Identities and groups¶
An identity is a name; when several agents share a project they use composite
names <project>/<agent>, e.g. quantum/claude-7f3a and quantum/codex-2b40.
A target is then a name, a comma list, a group glob (quantum/* for every
agent on the project, quantum/claude-* for one role), or all. List who is live:
synapse who # agents online, with -rx waiter sidecars counted apart
synapse who --project quantum # only quantum/... instances
synapse who --observed-peer east=ws://127.0.0.1:8877 # add advisory peer owners
synapse who --name quantum/codex-2b40 --me # this identity plus its -rx waiter status
syn who --me # same check using the resolved syn identity
syn reap # list this identity's shell-hook waiter pidfile
syn reap --pid 1234 # clean up only that verified identity waiter PID
syn reap --stale # reap all verified waiters whose owner shell is dead
syn reap --stale --dry-run # report the sweep verdicts without acting
syn locks # list this project's leases, scopes, ages, and release commands
syn ask <target> <message> # send, require an online recipient, and wait for replies
syn inbox # exact resolved identity, with its own cursor
syn inbox --project-wide # explicit project-wide feed and project cursor
syn inbox --name PROJ/name # one exact address, with its own cursor
syn inbox --as PROJ/coordinator # also drain a role name, under its own cursor (repeatable)
syn commit <paths> -m <message> # hold the project git lease and commit only those paths
synapse send --target quantum/* "rebasing main now" # the whole project team
Bare synapse who opens its short-lived query connection under the literal
default name USER; it does not infer a name from $USER, $SYN_PROJECT,
$SYN_IDENTITY, or the working directory. If another live socket or a machine-
identity pin already owns USER, choose a fresh, project-scoped query name with
synapse who --name <project>/<query-name>. The syn who ergonomic wrapper is
different: it resolves the project from an explicit --project, then
$SYN_PROJECT, then the repository/working-directory name; an agreeing
$SYN_PROJECT and $SYN_IDENTITY pair supplies the exact session identity. It
passes that result to synapse who --name ..., which avoids the shared USER
name. For --me, pass the identity whose presence and waiter you intend to
inspect; the command itself connects as <name>-who, so it does not create the
subject presence it reports.
synapse who --me queries as <name>-who, then reports <name> and
<name>-rx, so the check does not create the presence it describes. It keeps the
output honest: presence is not a wake loop, and a missing -rx waiter means
directed messages will not wake that terminal promptly.
synapse wait --directed-only suppresses routine broadcasts: it wakes on messages
that name you (or a group you are in), but still wakes on a priority broadcast
(synapse send --priority) and on any message from CEO — so an all that
genuinely matters reaches a quiet waiter promptly while peer chatter is left for the
next synapse relay/inbox read. Use --priority sparingly, for announcements that
must reach everyone immediately.
When several agents share a repo, serialise the operations that must not overlap — above all commits — by wrapping them in a lease. The hub grants one live lease per id, so the others wait their turn instead of clobbering each other:
synapse lock quantum:git -- git push # holds quantum:git while pushing
synapse lock quantum:git --wait-timeout 0 -- git push # fail fast if someone holds it
synapse lock quantum:git --release-timeout 10 -- git push # hold the exit up to 10s for the release confirmation (slow links)
A lock is a named mutex keyed by its id: quantum:git and physics:git are
independent, so one repo's push-lock never blocks another's. The lease is held only
for the wrapped command and dropped when it exits. A claim that no commit or merge
will auto-release — a git-claim --auto-release-on manual — is dropped by its owner
with synapse release <task> --name <owner>.
Add receipt fields when the release is also the closeout record. The hub echoes
the receipt on release_granted; if any evidence field is present, it records the
same receipt as an assessment progress note on the board. Use --receipt-json
when another tool should consume the hub-confirmed receipt:
synapse release BUILD --name api-dev \
--evidence "pytest tests/test_feature.py -q: passed" \
--evidence "mypy src/synapse_channel/feature.py: passed" \
--changed-file src/synapse_channel/feature.py \
--generated-artifact docs/_generated/capability_manifest.json \
--artifact coverage.xml \
--approval "reviewed-by=owner" \
--known-failure "none" \
--confidence medium \
--freshness-seconds 60 \
--receipt-json
The receipt records the releasing owner's submitted evidence; it does not certify
that the evidence is complete or sufficient. It also includes advisory
epistemic_status and epistemic_reasons fields derived from the submitted
evidence, known failures, and --freshness-seconds: fresh positive evidence is
unverified, positive evidence without freshness is needs_freshness, old
positive evidence is stale, declared known failures are degraded, and no
positive evidence is unsupported. The observed verify-release path may set
supported after it executes the declared checks and produces their evidence;
the label remains advisory and is not a signature-verification result.
Use synapse verify-release when the closeout record must include observed
command execution instead of hand-written evidence. The command runs each
declared --run argv, records exit codes plus stdout/stderr SHA-256 digests,
hashes named --artifact files, captures Git HEAD, tree, and changed files,
and writes receipt JSON consumable by synapse release --receipt:
synapse verify-release BUILD --name api-dev \
--run ".venv/bin/python -m pytest tests/test_feature.py -q" \
--run ".venv/bin/python -m mypy --strict src/synapse_channel/feature.py" \
--artifact coverage.xml \
--merkle-db ~/synapse/hub.db \
--output verified-release.json
synapse release BUILD --name api-dev \
--receipt verified-release.json \
--receipt-json
--merkle-db additionally commits the coordination log's RFC 6962 Merkle root
into the receipt, binding the release to the exact coordination history behind
it. Because the log is append-only, anyone can later re-verify the commitment —
synapse policy-check --merkle-db recomputes the committed log prefix and adds
a merkle_commitment decision that fails when that prefix was rewritten,
truncated, or renumbered since the receipt (and passes as the log grows):
synapse policy-check BUILD --policy ./policy.json \
--receipt-json verified-release.json \
--merkle-db ~/synapse/hub.db # re-verify the committed log prefix
The bare commitment lets a verifier holding the receipt and current log check
that committed prefix; it cannot prove who attested it, persist an independent
tree head, or detect a rollback when the retained receipt is absent.
--signing-key closes that gap: synapse merkle keygen creates the hub
deployment's Ed25519 receipt-signing keypair (private key 0600, public half in
a distributable PATH.pub), verify-release --signing-key signs the commitment
into verification.merkle_signature, and policy-check --trusted-signing-key
(repeatable, one .pub per trusted hub) adds a merkle_signature decision. A
verifier holding only the receipt and the .pub file — no access to the live
log — learns which hub attested that exact log state; a tampered root, an
untrusted or transplanted key, and a signature with no commitment to cover all
fail, and only a receipt with no signature at all reads not_applicable:
synapse merkle keygen ~/synapse/hub-receipt.key # once per hub deployment
synapse verify-release BUILD --name api-dev \
--run ".venv/bin/python -m pytest -q" \
--merkle-db ~/synapse/hub.db \
--signing-key ~/synapse/hub-receipt.key \
--output verified-release.json
synapse policy-check BUILD --policy ./policy.json \
--receipt-json verified-release.json \
--trusted-signing-key ~/synapse/hub-receipt.key.pub # provenance, offline
The generated receipt is still advisory coordination evidence. A supported
epistemic_status means verify-release executed the declared checks and they
did not report a failure; it does not mean Synapse independently established
correctness, reviewed the commands, or proved the artifacts sufficient. When
--trusted-signing-key is supplied,
policy-check also emits a closed evidence_verdict object. A passing historical
receipt is VALID_LEGACY, never AEF VALID; malformed, unknown-key, and invalid-
signature outcomes remain distinct. The verifier never derives this verdict
from caller-supplied epistemic_status.
synapse git-claim accepts the task id either positionally (synapse git-claim
TASK-1 --paths src) or as a named field (synapse git-claim --task-id TASK-1
--paths src) for generated argv. Use one form, not both. synapse git-release
is hook-invoked and does not take a task id; when a manual drop is needed, use
synapse release <task> --name <owner>.
synapse git-claim also accepts semantic selector flags for the same local
resolver exposed by tools/semantic_claims.py: --module, --symbol, --api,
--source, --test, --generated, and --migration. The command resolves the
selectors against the local git root, merges the derived source/test/generated
paths with any explicit --paths, and sends only canonical path scopes to the
hub. Symbol and API selectors use a synthetic descendant below their source;
other selectors and companion paths stay whole-file.
With the optional semantic extra, --diff-base main maps tracked working-tree
changes to the smallest named declaration in Python, JavaScript/JSX,
TypeScript/TSX, Rust, or Go. Add --diff-head HEAD for a committed comparison
and repeat --diff-path src/pkg to filter it. Every incomplete mapping widens to
the whole file. python tools/semantic_diff_claims.py --base main --claim-args
exposes the same diff-only planning surface. Add --semantic-evidence-json
semantic-evidence.json to either selector or diff claims to write receipt-ready
local evidence; no parser download or hub-side Git access occurs.
The local Git evidence boundary is non-interactive and bounded to ten seconds,
eight MiB of stdout, and 64 KiB of stderr per command. It disables pagers,
external diff, and textconv; sanitizes failure text; and terminates an isolated
child on timeout, overflow, or pipe-reader failure. Repository-configured
filesystem monitors are explicitly disabled. A known file with unavailable safe
patch/blob evidence widens to whole-file scope, while unavailable file
enumeration denies the operation.
Claim paths are coordination scopes, not filesystem reads. Normal relative paths
such as src/auth.py stay narrow. Absolute paths and any path containing ..
are treated as traversal-like declarations and widen to the whole worktree, so a
suspicious scope may block more work but cannot miss a conflict.
synapse git-claim-check --staged is the read-only commit gate. It ignores hook
filenames and parses git diff --cached --name-status -z --find-renames
--find-copies; copies and renames require claims for both names. An empty index
returns success before resolving identity or contacting a hub. Otherwise the
explicit --name, worktree-scoped synapse.identity, and an ambient
SYN_PROJECT plus SYN_IDENTITY pair must agree. Run
git-init --name <exact-owner> once in every linked worktree; it enables Git's
per-worktree config and writes that seat's identity, selected URI, and at most a
token-file path without leaving a shared identity fallback. It installs only the
post-commit/post-merge release hooks; those convenience scripts remain
repository-wide, so mixed-identity linked worktrees should use manual release. A
repository must wire the checker into its pre-commit framework to make it
blocking. See Git-native claims.
Use syn locks for the operator view before releasing or asking another owner to
release. It queries the live state snapshot as <identity>-locks, filters to the
resolved project by default, and prints the task id, holder, scope, age, remaining
lease time, checkpoint, git branch context, and the exact synapse release ...
command. syn locks --all removes the project filter; syn locks --owner <name>
shows one owner or project namespace; syn locks --json emits the same rows as
JSON.
Use syn commit <paths> -m <message> for the common commit workflow. It resolves
the current identity, acquires the <project>:git lease, runs git add -A --
only for the supplied paths, and runs git commit -m <message> -- for the same
paths. Unrelated staged or modified files stay outside that commit. The command
rejects empty, absolute, parent-traversal, and .git paths before contacting the
hub.
Getting woken on a message¶
A turn-based assistant cannot hold a socket between turns, so it learns of a
message only when it checks. synapse wait turns that into a push: it blocks on
the connection and exits the instant a message addressed to you arrives. Run it as
a background task — when it exits, the message has landed (and a harness that
re-invokes an agent on background completion wakes you). On wake, read the message,
act, and re-launch synapse wait. It costs nothing while it waits.
synapse wait --name api-dev-rx --for api-dev # blocks; prints + exits on a message for api-dev
synapse wait --for api-dev --timeout 60 # give up after 60s (exit 2) instead of waiting forever
synapse wait --for api-dev --capability-card card.json # re-register a dispatch card on every (re)connect
With --capability-card FILE the waiter re-registers a persistent dispatch
card for its --for identity on every connect — so automated dispatch can
discover the seat across reconnects and hub restarts. The file is JSON:
{"description": ..., "skills": [...], "task_classes": [...], "model": ...,
"dispatchable": true|false}. An unreadable or malformed card is reported and
the wait continues unregistered — a lost registration never silences the
waiter. The registration is reserved for project-scoped seats
(<project>/<seat>) and is sent unsigned from the waiter's -rx name for its
bare identity. While the wait blocks, the card is re-advertised every
--capability-refresh-seconds (default 21600 = 6h; 0 disables), so a
healthy long-lived waiter never lets its 24-hour registration lapse.
synapse arm states its binding out loud before it holds the socket: the first line
names exactly whose messages it wakes on (waiting for messages to <identity>), and
when the shell environment carries a different SYN_IDENTITY it says so on a second
line — an explicit --name/--for always wins, but that mismatch is the classic
sign of arming from a borrowed shell, so it surfaces immediately rather than after a
night of silently missed messages.
A waiter is deaf while it is disconnected — the gap between a dropped connection (or
a re-arm as a fresh process) and the next connect. A directed message that lands in
that gap is durable, but the waiter is not woken by it and it waits unread until an
unrelated wake happens to drain it. synapse arm --mailbox closes the gap: on each
connect the waiter asks the hub to replay the directed messages it missed and wakes
on them as it would on a live message. It resumes from a since_seq cursor kept per
identity under ~/synapse/mailbox-cursor/, so a re-arm picks up where it left off
rather than being replayed — and woken by — the whole retained backlog again. It is
off by default; a plain arm is unchanged, and against a hub older than wire version
2 the mailbox request is simply ignored.
synapse arm --name api-dev-rx --for api-dev --mailbox # also wake on messages missed while offline
The durable arm and a terminal pane bridge have separate receiver identities:
the mailbox arm uses api-dev-rx, while agent-tmux wait --identity api-dev
uses api-dev-pane-rx. Both subscribe to messages for api-dev, so durable
gap replay and live prompt injection coexist without one receiver superseding
the other. synapse doctor and ready-task dispatch recognise either sidecar;
the shared owner remains api-dev. A plain arm without --mailbox still yields
beside an active provider because it contributes no independent durability.
When the shell hook launches an interactive provider command, worker-session
automatically starts or attaches a persistent tmux session and keeps a directed
wake bridge alive. The user still types the provider command normally, for
example codex or claude; the provider process starts with SYN_PROJECT and
SYN_IDENTITY already set.
Use synapse agent-tmux as the manual diagnostic/admin surface for that tmux
wake path. It works for any terminal coding agent — Codex, Kimi K2, Claude Code —
selected with --agent-command. It starts or targets a named tmux session and
injects only a fixed instruction; the Synapse message body stays in the inbox and
the agent reads it itself. synapse codex-tmux is a Codex-defaulted alias kept
for backward compatibility (--codex-command instead of --agent-command).
Every start, status, and wake operation verifies the live tmux session's stable
SYN_PROJECT and SYN_IDENTITY environment. Reusing one session name for a
different project or identity fails closed; wake returns before send-keys, so
a Fleet receiver cannot inject into a Core-owned pane (or vice versa).
# Generic form — choose the agent with --agent-command (defaults to codex):
synapse agent-tmux start --identity api-dev/kimi --session api-dev-kimi --agent-command kimi --cwd "$PWD"
synapse agent-tmux wait --identity api-dev/kimi --session api-dev-kimi --agent-command kimi --cwd "$PWD"
synapse agent-tmux status --identity api-dev/kimi --session api-dev-kimi --agent-command kimi --cwd "$PWD"
# Codex alias (equivalent to --agent-command codex):
synapse codex-tmux wait --identity api-dev/codex-main --session api-dev-codex --cwd "$PWD"
A terminal agent does not wake its own idle pane on a Synapse message: its
synapse wait is a foreground tool call whose turn ends, so the message lands in
the inbox but the pane never re-engages. agent-tmux wait is the external bridge
that closes that gap — it blocks on synapse wait for the identity and, on each
directed message, delivers the fixed prompt only to a verified idle provider
composer.
Its connection name is <identity>-pane-rx, deliberately distinct from the
permanent mailbox arm's <identity>-rx; the split is receiver arbitration, not
a second agent identity.
Before emitting a key, the bridge captures a bounded pane tail and requires the
known idle-composer profile for Codex, Claude Code, Kimi, Grok, or Gemini, with
no modal, approval, or busy marker. Unknown or ambiguous panes receive no key:
the consumed wake is persisted in the owner-only registry and retried after the
bounded pane-probe interval, including after a bridge restart. For a safe pane,
the fixed prompt is delivered as one bracketed paste rather than shortcut-capable
individual characters. After --submit-delay, a second capture must still show
the exact prompt in the safe composer before Enter is sent. It retries a failed
synapse wait with backoff instead of exiting, giving up only after
--max-wait-failures consecutive failures (unbounded by default), so a hub
restart does not permanently stop the waker.
The default --pane-probe-interval 5 bounds stale pane-bridge presence: after
each quiet interval the wait socket closes, then the bridge rechecks the tmux
session, exact identity binding, and active agent pane before registering
again. If the pane disappeared or became inactive, the bridge exits without
injecting or starting an owner application; supervision may then report and
repair it through the ordinary service path.
When a broadcast (--target all, or a --priority/CEO message that reaches a
--directed-only waiter) wakes every terminal at the same instant, their agents
all re-invoke and call the model provider at once. That synchronised burst trips the
provider's request-rate limiter — not a synapse limit: Anthropic's API, for
instance, answers "Server is temporarily limiting requests" (a request-rate
throttle, distinct from your usage quota). synapse wait --wake-jitter <seconds>
(default 8) spreads the broadcast wakes over 0..jitter so each agent reacts
without the stampede; a
one-to-one directed message has no herd and still wakes immediately. Set 0 to
disable for a latency-critical single-waiter setup.
The shell hook records its background waiter in an identity-scoped pidfile under
$XDG_RUNTIME_DIR/synapse-shell (or /tmp/synapse-shell). Use syn reap to list
the pidfile for the resolved identity. If the pidfile points at a dead PID,
syn reap --pid <pid> removes only that pidfile; if the PID is live, it sends
SIGTERM only after the command line verifies as this exact synapse arm --name
<identity>-rx --for <project> waiter. It refuses unrelated PIDs instead of
searching or pattern-killing.
syn reap --stale sweeps every pidfile in the runtime directory and reaps the
verified waiters whose owner is demonstrably dead — the recorded --owner-pid,
or the terminal PID embedded in a …/terminal-<pid> identity. A waiter whose
owner is alive, or that names no checkable owner, is kept; a live process whose
command line is not this Synapse waiter is reported and never signalled. Add
--dry-run to see the verdicts without acting.
synapse wait --for api-dev # default: broadcast wakes jitter 0–8s
synapse wait --for api-dev --wake-jitter 0 # disable the jitter
The same herd from the sending side: to push a fleet-wide update, do not
--target all a fleet of waiters at once — send directed, spaced a few seconds
apart, so the wakes (and re-invocations) do not stampede the provider.
Messaging: broadcast, several, or one¶
Every message carries a target. The hub broadcasts each message to all
connected clients and records it in history and the relay log; the target
selects who it is for:
synapse send --target all "deploy is green" # everyone (the default)
synapse send --target SCPN-CONTROL "kernel built, run the control tests" # one agent
synapse send --target SCPN-CONTROL,REMANENTIA "you two: rebase on main" # several
synapse send --require-recipient --target SCPN-CONTROL "ping" # also print the positive receipt
If a one-shot send accidentally uses a waiter name such as api-dev-rx, the
command sends as api-dev instead. That keeps the persistent wake socket online
and avoids the hub's duplicate-name refusal for the short-lived sender.
Every directed synapse send asks the hub for a delivery receipt. The reply
includes delivered, message_target, message_id, consume-live recipients,
all socket-level matched_recipients, stale_recipients, a machine-readable
reason, the optional sender-supplied client_msg_id, and whether the hub
dead_lettered it. Programmatic clients that retry chat reuse a bounded printable
client_msg_id and receivers deduplicate on (sender, client_msg_id); the hub
remains intentionally at-least-once and gives each attempt a new msg_id / seq.
A socket match counts as live
only when the recipient reacted within the configured liveness window or has a
fresh -rx waiter; otherwise the CLI prints delivery failed: no live recipient
matched ... and exits 1, the same as an offline target. The message is still
journalled and best-effort routed to the stale socket, so a later mailbox replay
can settle its deferred receipt. On a journal-backed hub, receipt frames carry a
stable receipt_notification_id; an offline sender receives the pending outbox frame
after its next authenticated connection and deduplicates any at-least-once retry by
that id. Receipt frames are not replayed through the chat mailbox, and WebSocket
acceptance never proves that a model read or acted. The durable verdict remains
queryable with synapse event-query <db> "receipts <sender>".
--require-recipient additionally
prints a positive delivered to ... receipt and fails if an older hub returns no
receipt; without the flag, receiptless older hubs retain their historical success
result.
For selected sensitive bodies, synapse send --encrypt-key-file replaces the
plain payload with an AES-256-GCM envelope whose authenticated data binds the
visible sender, target, channel, task id, and recipient set. The hub routes the
ciphertext and metadata; it does not receive the plaintext:
synapse send --target SCPN-CONTROL \
--encrypt-key-file ./payload.key \
--encrypt-key-id project-main-v1 \
--encrypt-recipient SCPN-CONTROL \
"private handoff note"
synapse listen --name SCPN-CONTROL --for SCPN-CONTROL \
--decrypt-key-file ./payload.key
synapse channel key-check ./payload.key
The key file is a local 32-byte owner-only file. This first runtime tranche does not discover, rotate, revoke, or escrow keys.
Private channels scope delivery without encrypting payloads:
synapse channel create ops --name alice
synapse channel invite ops --invitee bob --name alice # owner-only; grants bob one join
synapse channel join ops --name bob # refused without an invite
synapse send --name alice --channel ops "operator note"
synapse channel history ops --name bob --limit 20
synapse relay ./feed.ndjson --channel ops
synapse relay ./feed.ndjson --public-only
synapse relay ./feed.ndjson --channel ops --channel-metadata
synapse event-query ./synapse.db "channel ops between seq 1 999999"
The hub retains a bounded live history per channel for current members and journals channel chat with a visible channel id. Relay filtering can select one private channel or the default public lane. Event-query channel results expose metadata and payload byte length, not private payload bodies.
For the common question workflow, use syn ask <target> <message>. It resolves
the same identity as syn say, dispatches to synapse send with
--wait-seconds 30 --require-recipient, and prints replies during that wait
window. Override the window with syn ask --wait 10 <target> <message>. Use
--no-require-recipient for broadcasts or to tolerate a receiptless legacy hub;
a directed negative receipt from a current hub still exits non-zero.
A reader sees only the messages addressed to it with --for, which also drops
presence noise and other agents' cross-talk — a per-agent inbox. Because the
relay log is durable, an agent that was offline still catches up on its next read:
synapse relay ./feed.ndjson --for SCPN-CONTROL --cursor ./control.cursor
synapse listen --name SCPN-CONTROL --for SCPN-CONTROL # live inbox
New relay rows use codec version 2: structured JSON payloads and auxiliary envelope fields such as task ids, lease epochs, paths, and Git context survive the file round trip under a compact extension mapping. The decoder also reads existing version-1 rows, which contain only the historical core string fields. Both formats retain timestamps to millisecond precision.
Hub options¶
synapse hub --port 8876
synapse hub --port 8876 --db ./synapse.db # crash-safe persistence
synapse hub --port 8876 --rate 5 --burst 20 # per-agent rate limiting
synapse hub --port 8876 --durable-ingress-events 100 --durable-ingress-bytes 1048576
# per-principal accepted chat events/serialized-frame bytes inside --durable-ingress-window
synapse hub --port 8876 --relay-log ./feed.ndjson # mirror the channel to a file
synapse hub --max-clients 32 --max-msg-kb 256 # cap connections and frame size
synapse hub --max-connections-per-host 4 # tighten the per-host socket cap (default 32; 0 disables)
synapse hub --allow-origin https://app.example # admit that browser Origin (repeatable; default: none)
synapse hub --advertised-host hub.example:8876 # trusted Host when bind is not loopback / behind a proxy
synapse hub --max-progress-per-task 500 # cap retained board progress per task id
synapse hub --max-findings-per-agent 200 # cap durable findings admitted per agent
synapse hub --shutdown-close-timeout 5 # bound active socket close handshakes
synapse hub --tls-certfile ./hub.crt --tls-keyfile ./hub.key # native wss://
synapse hub --host 0.0.0.0 --token-file ./tok # token from a file, not argv (ps-safe)
synapse hub --host 0.0.0.0 --insecure-off-loopback # bind off-loopback WITHOUT a token (refused otherwise)
synapse hub --expect-multi-seat # multi-seat intent for flood auto-enable (default off)
synapse hub --bridge-exposed # declare A2A/MCP bridge for flood auto-enable (default off)
Binding a non-loopback host without a token (and, with --metrics, a metrics
token) is refused by default — the hub will not start exposed by accident.
A token presented over plaintext ws:// off loopback is also refused: the
token and every coordination frame would be readable on the wire, so an
off-loopback bind requires native TLS (--tls-certfile/--tls-keyfile) or a
wss:// proxy. --insecure-off-loopback downgrades either refusal to a warning
for a trusted private network. Independently, an off-loopback bind with a
plaintext --db event store is refused — the durable coordination log would
sit unencrypted on the host's disk; encrypt it with --db-key-file (after
synapse encrypt-key migrate-sqlcipher) or pass --insecure-plaintext-at-rest to
accept the risk. Loopback binds and encrypted stores are unaffected.
--max-connections-per-host is a connection-count cap keyed by the
remote host (default 32; pass 0 to disable); it is separate from
--host-rate, which meters inbound frames from that host. The hub always
installs a handshake guard: browser Origin values are refused unless listed
via --allow-origin (concrete scheme://host[:port] only; never null or
wildcards), and every upgrade — browser or native — must present a Host
authority derived from the loopback bind and/or --advertised-host. Without
--advertised-host, a bind-all address (0.0.0.0 / ::) admits no Host and
fail-closes. Metrics/health keep their token gates when --metrics is on. Open and secured
hubs both reap sockets that never bind a name within --auth-timeout (default
10s; close code 4012). Without --secure, disabled flood limits (--rate /
--burst / --host-rate / --host-burst / --max-connections-per-host left
at 0) may still be filled automatically when the hub starts in an exposed
posture (off-loopback bind, connect token, multi-seat intent, or bridge
exposed). Use --expect-multi-seat (default off) when more than one seat is
expected without --team-secure / identity material, and --bridge-exposed
(default off) when an A2A or MCP bridge is knowingly reachable against this hub
— the hub does not detect those processes automatically. Details:
Auto flood-enable (REV-SEC-06). Native wss:// uses --tls-certfile plus --tls-keyfile; it protects
the transport but does not replace --token for off-loopback binds. Supply the
token with --token-file or the SYNAPSE_TOKEN
environment variable rather than --token, which is visible in ps. The hub
drains on SIGTERM/SIGINT, so a container stop shuts it down cleanly. synapse
health is a liveness probe — exit 0 when the hub answers, 1 otherwise —
wired as the Docker HEALTHCHECK. --shutdown-close-timeout bounds the
WebSocket close handshake during stop; accepted mutations are durable at append
time when --db is enabled, not deferred to process exit:
Takeover and identity-conflict paths are logged for auditability without message payloads: accepted takeovers, cooldown refusals, name conflicts, and name-switch denials include the sender name, remote host, and close reason.
synapse health # exit 0 if the local hub is reachable
synapse health --uri ws://host:8876
synapse health prints nothing by design — it reports only through its exit code,
so an empty response is a pass, not a failure. Read $? (or rely on the container
healthcheck) rather than the output. When you want a human-readable account of what
is or is not wired — identity, hub exposure, waiters — run synapse doctor instead;
health answers "is the hub up?", doctor answers "is my setup right?".
Selecting the hub¶
Every client command talks to ws://localhost:8876 by default. To point the
whole CLI at another hub — a remote coordinator, or a second local hub on another
port — set SYNAPSE_URI once instead of passing --uri to each command:
export SYNAPSE_URI=ws://coordinator.internal:8876
synapse who # queries the hub named by SYNAPSE_URI
synapse board # so does every other command
An explicit --uri on a single command overrides the environment for that call,
and unsetting SYNAPSE_URI (or leaving it blank) restores the loopback default.
The companion SYNAPSE_TOKEN supplies the shared secret for a secured hub the
same way.
Worker options¶
synapse worker --name FAST --provider ollama --model gemma3:4b
synapse worker --name OFFLINE --provider rule
synapse worker --name TIER --provider tiered --model small --heavy-model big
synapse worker --prefix remanentia/ --name FAST --provider rule
A tiered worker classifies each request and routes trivial requests to a cheap
rule path and hard requests to the heavy model.
--prefix is prepended to --name to form the identity the worker registers
under (here remanentia/FAST), so the same role can run under several projects on
one hub without a name clash. synapse team --prefix remanentia/ namespaces a
whole team the same way; address a namespaced worker by its full identity, for
example synapse send --target remanentia/FAST "status?".
Observing¶
synapse listen streams chat (and optional presence) until it stops. An intentional
stop — Ctrl+C, or a finite message cap in tests/tools — exits 0. If the hub
refuses the name (close 4009 name conflict, including after the welcome
handshake) or the socket dies for any other hub-initiated reason while listening,
the process exits nonzero and prints the close code and reason via the same
connect-failure wording as send. There is no silent dead listener: a lost
connection never leaves the process advertised as a healthy long-running listen
with exit 0.
synapse listen --name USER
synapse board
synapse manifest
synapse directory --task-class chat --json
synapse route-task TASK-1 --limit 3 --event-store ./synapse.db --json
synapse resource-bids TASK-1 --resource-kind gpu --json
synapse memory-recall ./synapse.db "transport handoff" --json
synapse a2a-card --endpoint-url https://agent.example.com/a2a/v1
synapse a2a-conformance --json
synapse a2a-serve --endpoint-url http://127.0.0.1:8877
synapse a2a-serve --endpoint-url http://127.0.0.1:8877 --bearer-auth --a2a-token-file ~/.config/synapse/a2a-token --state-file ./a2a-state.json
synapse a2a-serve --endpoint-url http://127.0.0.1:8877 --task-timeout 300 --subscribe-timeout 1
synapse relay ./feed.ndjson --cursor ./feed.cursor
synapse compact ./synapse.db --all --max-checkpoints-per-task 3 --archive-report ./compact-report.html
synapse compact ./synapse.db --floor-seq 142 --drop-corrupt --archive-report ./corrupt-recovery.html
synapse event-query ./synapse.db "task TASK-1 timeline"
synapse event-query ./synapse.db "conflicts at seq 120" --json
synapse event-query ./synapse.db "channel ops between seq 1 999999"
synapse event-query ./synapse.db "receipts ALICE" --json
synapse event-query ./synapse.db "universal-receipts all" --json
synapse event-query ./synapse.db 'timeline("TASK-1").'
synapse event-query ./synapse.db 'MATCH (task:TASK {id:"TASK-1"}) RETURN timeline'
synapse postmortem ./synapse.db TASK-1
synapse debug ./synapse.db --fork-at 142
synapse debug ./synapse.db --task TASK-1 --fork-at 142 --set status=blocked --json
synapse reproduce ./synapse.db TASK-1
synapse reproduce ./synapse.db TASK-1 --expect 9f2c… --json
synapse causality causes ./synapse.db 142
synapse causality effects ./synapse.db 118 --json
synapse causality counterfactual ./synapse.db 96
synapse causality contention ./synapse.db # who should yield on overlapping live claims
synapse causality causes ./hub.db peer:96 --peer peer=./peer-hub.db # federated, HUB:SEQ refs
synapse causality causes ./hub.db peer:96 --peer peer=./peer-hub.db --clock-skew peer=-6.4
synapse causality causes ./hub.db peer:96 --peer peer=./peer-hub.db --dot # Graphviz, cluster per hub
synapse causality otel ./synapse.db --out spans.json # OpenTelemetry projection
synapse causality otel ./synapse.db --endpoint http://127.0.0.1:4318/v1/traces # OTLP push [otel]
synapse causality otel ./synapse.db --out s.json --filter T1 --service-name hub-eu # named tasks only
synapse causality health ./synapse.db # orphaned/dangling/stale claims
synapse merkle root ./synapse.db
synapse merkle prove ./synapse.db 142 --json > proof.json
synapse merkle verify proof.json --expect 9f2c…
synapse merkle verify proof.json --json
synapse merkle keygen ~/synapse/hub-receipt.key # receipt-signing keypair (private + PATH.pub)
synapse reliability ./synapse.db
synapse accounting record --name alpha --task TASK-1 --model claude-opus-4-8 --input-tokens 1200 --output-tokens 300
synapse accounting report ./synapse.db --pricing pricing.json --budget budget.json
synapse approval request --name dev --subject TASK-1 --reason "needs human sign-off"
synapse approval decide --name ceo --subject TASK-1 --approve --reason "ship it"
synapse approval status ./synapse.db --pending
synapse ttl-advice ./synapse.db
synapse ingest ./synapse.db --cursor ./ingest.cursor # drain new events as JSON lines, resumable
synapse multihub observe --peer-db ./peer.db --json # fold a peer hub's log offline
synapse multihub follow --peer-uri ws://peer:8876 # pull a peer's board over a connection
synapse multihub follow --peer-uri wss://peer:8877 --pin sha256:HEX # pin a self-signed TLS peer
# Serve authenticated multi-hub frames and use pinned client-auth routes.
synapse hub --tls-certfile hub.pem --tls-keyfile hub.key \
--multihub-serving-policy serving.json \
--multihub-client-certfile client.pem --multihub-client-keyfile client.key \
--relay-peer syn-owner=wss://owner:8876 --relay-peer-pin syn-owner=sha256:HEX
# serving.json, its federation store, and client CA must be service-owned 0400/0600 files
synapse supervisor --idle-seconds 300 --history-multiplier 3
synapse capability-card keygen --key-id PROJECT:worker:v1 --private-out ./card.pem --agent PROJECT/worker --project PROJECT --trust ./card-trust.json
synapse capability-card sign ./card.json --key ./card.pem --key-id PROJECT:worker:v1 --sequence 1 --out ./signed-card.json
synapse capability-card verify ./signed-card.json --trust ./card-trust.json --json
synapse manifest prints live capability cards advertised by connected agents.
When a card carries capability contracts, the readable output includes the
contract count while JSON surfaces such as the MCP manifest resource, A2A Agent
Card metadata, and dashboard snapshot retain the full contracts entries with
task_class, input_schema, output_schema, preconditions, and
postconditions.
synapse capability-card keygen|sign|verify manages a separate Ed25519
advertisement-signing profile. keygen writes an owner-only private key and can
enrol the public key into an explicitly agent/project-scoped trust bundle; sign
emits strict canonical JSON without overwriting an output; verify performs a
one-shot check without consuming live replay history. A worker opts in with
--capability-card-key, --capability-card-key-id, and
--capability-card-project; a hub opts in with --capability-card-trust and
bounded clock/history controls. Results are advisory and visible on every card;
unsigned cards remain missing_signature, and live replay/downgrade history is
currently in memory.
synapse directory builds a read-only capability directory from the live
manifest plus resource offers from the state snapshot. It supports --agent,
--task-class, --skill, --resource-kind, and --json. The directory is
discovery metadata only: it helps route or review work, but it does not reserve
capacity, authorize execution, or certify trust.
synapse route-task TASK-1 fetches the board, manifest, and state snapshots and
returns advisory routing recommendations for that board task. The scorer is
local and deterministic: task-class matches rank first, skill matches and card
description overlap add explainable evidence, and cards with contracts carry a
small evidence bonus. --event-store ./synapse.db optionally adds observed
capability evidence from positive release-receipt assessment notes in the
durable log, preserving the source task id and event sequence for review.
--include-zero shows unmatched agents for diagnostics; no route recommendation
claims the task, changes suggested_owner, reserves capacity, grades an agent,
or certifies trust.
synapse resource-bids TASK-1 fetches the same board, manifest, and state
snapshots, then ranks live resource offers for the board task. The scorer is
local and deterministic: requested resource kind, capacity, provider task-class
matches, skill tags, provider description overlap, resource kind/name overlap,
and matching metadata all appear as reason codes in the output. --resource-kind
filters offers before scoring, --include-zero keeps diagnostic candidates, and
--json prints the stable report. A resource bid is a marketplace-style
directory hint only: it does not reserve capacity, authorize execution, mutate
the board, or certify provider trust.
synapse memory-recall DB QUERY reads the same local SQLite event store and
returns deterministic recall hits over durable memory records. The projection
uses findings, checkpoints, and handoffs, ignores recall-query telemetry, and
keeps each hit tied to its source sequence, timestamp, event kind, source field,
task id, actor, evidence reference, score, and matched tokens. --since-seq
limits the read to records above a cursor, --limit bounds output, and --json
prints the stable machine-readable report. This is local event-log projection:
it does not create external embeddings, contact a service, certify truth, or
mutate hub state.
synapse supervisor watches the shared board and re-offers stalled plan tasks.
The fixed --idle-seconds threshold remains the operator ceiling. By default the
supervisor can lower the effective threshold when completed tasks on the board
show enough faster progress cadence; --history-multiplier,
--min-history-samples, and --min-predictive-idle-seconds tune that heuristic,
and --no-predictive-stall disables it. This is local board evidence, not a
claim that a worker process has failed.
synapse compact is an offline maintenance command for the SQLite event store
created by synapse hub --db. It needs either --floor-seq <seq> (the lowest
sequence every read-side consumer has ingested) or --all (only when the whole
log is settled). --archive-report PATH writes an owner-only static HTML report
from the pre-compaction event snapshot, then records the actual checkpoint and
finding removal counts from the compaction run. A malformed stored row is read as
a safe corrupt_event marker containing its sequence, validation reasons, and a
SHA-256 payload digest — never the raw stored payload. A hub recovered around any
such row reports degraded health and refuses every mutation while keeping
read/query/heartbeat paths available for diagnosis.
Removing a quarantined row is always explicit: pass --drop-corrupt plus the
settled --floor-seq (or --all only when every consumer has settled the entire
log). --drop-corrupt requires --archive-report, ensuring the safe digest and
reasons survive deletion. The command writes a Planned compaction archive
before changing SQLite, then atomically replaces it with the completed result;
an archive-write failure leaves every row intact. Restart the hub after the
offline repair and confirm
journal_corrupt_rows is 0 before resuming writes. The report includes event-kind
counts, board tasks, release receipt notes, and a bounded coordination timeline;
--archive-report-limit N controls the row cap for bounded sections.
synapse event-query is a temporal event-log query command for the same SQLite
event store. It supports task <id> timeline, task <id> at seq <n>,
task <id> at time <seconds>, path <path> between <start> <end>,
channel <id> between seq|time <start> <end>, conflicts at seq|time <n>, and
receipts <agent|target|all> for the durable delivery-receipt ledger. Use
universal-receipts <selector|all> for the first-class receipt view across
claim/release evidence, delivery, sandbox-run, policy, approval, operator-relay,
cross-hub, A2A-validation, and postmortem receipt families.
Federation receipts also include durable multi-hub equivocation quarantines and
explicit recoveries. The quarantine record exposes peer id, sequence, both event
fingerprints, detection metadata, and status, but never the conflicting payload;
synapse event-query DB "universal-receipts PEER" --json is the forensic read.
Channel queries return metadata-only records so private-channel bodies are not
printed by this forensic path. Receipt queries return the requested, immediate,
deferred, and expired delivery-receipt audit events that involve the selected
participant. Universal receipt queries return the shared receipt shape with the
source event kind and real event-log sequence. It also accepts prototype aliases over the same
model: Datalog-like timeline("TASK")., state("TASK", seq, 120).,
touches("src/auth.py", 0, 9999999999)., channel("ops", seq, 1, 99).,
receipts("AGENT")., universal_receipts("all").,
conflicts(seq, 120)., plus
Cypher-like MATCH (task:TASK {id:"TASK"}) RETURN timeline and related
AT/BETWEEN forms. It is read-only forensic evidence: it reconstructs what
the event log said at a sequence or timestamp, but it does not contact the live
hub or certify that a merge is safe.
Queries read selectively rather than loading the whole store. Each query pushes
its sequence/time window and the event kinds it needs into SQLite, so a query
deserialises only candidate rows even as a dogfood event log grows large — the
result is identical to a full scan because the loaded window always contains
every event the query keeps. Memory is bounded by the query window, not the log
size: a point-in-time query (at seq|time) never loads events after its cutoff,
and a range query (between) loads only its window. --limit N additionally
caps the printed output to the most recent N records (and conflict pairs); it
bounds output, not the reconstruction, so task-state and conflict results stay
correct.
synapse postmortem ./synapse.db TASK-1 builds a replayable postmortem from the
same event store. The Markdown or --json report lists the task timeline,
observed owners, release events, assessment evidence, reconstructed path-overlap
conflicts, and candidate unanswered messages that mention the task id. Candidate
unanswered messages are an audit signal only: the log proves the directed chat
and the absence of a later matching chat reply, not intent or off-channel
communication.
synapse debug ./synapse.db --fork-at 142 forks a task's reconstructed state at
a sequence point. It folds the durable log back into the exact claim state the
task held as of that sequence — owner, status, declared paths, and the saved
resume checkpoint — then prints the resume manifest an agent would pick up if the
task were rewound there, alongside the events that really happened after the fork
point. The task is inferred from the snapshot at the sequence, or named with
--task; --set FIELD=VALUE overrides a resume field (owner, note,
status, data_ref, worktree, checkpoint) on the manifest only. It is a
read-only what-if: the hub runs no task, so nothing is executed or changed. Exit
status is 0 when the task held a live claim at the fork point, 1 when it did
not (released or never claimed — nothing to fork), and 2 on a bad argument or a
missing store.
synapse reproduce ./synapse.db TASK-1 fingerprints a task's authoritative
history. Because hub state is a pure fold of an append-only log, a task's claim
snapshots and releases must replay to the same state on every machine; this
command canonicalises that slice and hashes it into a stable SHA-256 digest, so
two operators — or two federated hubs — holding the same history derive the same
digest. --expect DIGEST gates on a known-good value (exit 1 on mismatch, like
a release-receipt check); a task with no authoritative events exits 2.
synapse causality causes ./synapse.db 142 traces coordination causality over
the event log. It folds the durable log into a directed acyclic graph of three
recorded relations — a task's own lifecycle (claim before update before release),
a declared depends_on satisfied by the dependency's completion, and a release
that let a later, path-overlapping claim proceed — then answers against an event
sequence: causes returns the events upstream of it, effects the events it
enabled downstream, and counterfactual the downstream events whose recorded
cause traces back through it and so would lose all support if it were removed.
This is coordination causality inferred from recorded scheduling semantics, not
statistical causal discovery and not program-trace causality; every edge is
backed by a concrete event, and the counterfactual is a structural what-if over
the inferred graph, not a claim that the work would never have happened another
way. Exit status is 0 when the sequence names a coordination event, 1 when it
does not, and 2 on an unknown direction or a missing store.
synapse causality contention ./synapse.db takes no sequence: it finds every
pair of overlapping live claims — different owners, same worktree, path scopes
that intersect (an empty scope means the whole tree) — and weighs each contender
by what its task gates downstream: the distinct tasks causally reachable from
any of its recorded events, plus its declared dependents (transitively) that
have not completed. The contender whose task gates less is advised to yield;
on an equal count the later claim yields, so first-come precedence breaks the
tie. The advice is advisory only — no claim is preempted, nothing contacts a
live hub — and each recommendation prints with both standings and its reason
(--json for the machine shape). The exit code doubles as a collision signal:
0 when no live claims overlap, 1 when at least one pair does, 2 on a
missing store.
A worked example. Alice claims src/parser.py for parser-rework, which three
declared tasks (grammar-tests, error-recovery, docs-refresh) depend on;
Bob later claims the whole src/ scope for lint-sweep, on which only
lint-config depends. The scopes intersect in the same worktree, so the pair
contends — and because Alice's task gates three downstream tasks against Bob's
one, the advice names Bob's claim as the lighter one:
$ synapse causality contention ./synapse.db
# Contention: 1 overlapping live claim pair(s)
## lint-sweep (bob) should yield to parser-rework (alice)
- reason: parser-rework blocks 3 downstream task(s) versus 1; the lighter claim yields
- keeps: parser-rework (alice, seq 7) blocks 3 downstream task(s): docs-refresh, error-recovery, grammar-tests
- yields: lint-sweep (bob, seq 8) blocks 1 downstream task(s): lint-config
- advisory only: no claim is preempted; coordinate the yield explicitly
$ echo $?
1
Had both tasks gated the same count, the later claim (Bob's, higher sequence) would still be the one advised to yield — first-come precedence breaks ties. Acting on the advice stays explicit: Bob releases his claim (or narrows its paths so the scopes no longer intersect) and re-claims once Alice releases.
With --peer HUB=PATH (repeatable) the sequence queries trace causality
across federated hubs: the named hubs' logs merge in the deterministic
multi-hub order (timestamp, then hub id, then sequence — the same order the
multi-hub read side folds), every event keeps its global identity hub:seq,
and the three recorded relations are derived over that merged order. An edge
whose endpoints two different hubs authored is tagged federation, with the
recorded relation it derives from kept as its basis — a dependency completed
on one hub and claimed on another renders as federation:dependency. Events
are addressed as HUB:SEQ; a plain SEQ means the primary DB's hub, whose id
defaults to the DB file name and can be set with --hub-id. Honest scope:
within one hub, precedence is the hub's own monotonic sequence —
authoritative; across hubs there is no shared sequence, so the merged order
falls back to event timestamps, and a federation edge is clock-ordered
evidence, only as good as the hubs' clock agreement. Like the multi-hub fold,
the query observes and grants nothing.
When a live probe has measured clock skew for a hub, annotate the offline
causality report with --clock-skew HUB=SECONDS. Values are local-minus-peer
seconds; positive means the local clock was ahead of that hub. The default
warning threshold is 5 seconds and can be changed with --skew-warn-seconds.
Warnings appear as a ## Clock skew warnings section in Markdown, a
clock_skew object in --json, and comments in --dot; they do not alter the
graph, they mark the timestamp evidence as outside the operator's bound.
A worked cross-hub example: eu-hub completes and releases
schema-migration; us-hub declares api-rollout depending on it and
claims it. The claim on us-hub traces its cause across the hub boundary to
the release on eu-hub:
$ synapse causality causes ./eu-hub.db us-hub:2 --peer us-hub=./us-hub.db
# Federated causality (causes): us-hub:2
- Hubs: eu-hub, us-hub
- Event: us-hub:2 kind=claim task=api-rollout owner=us/codex-2b40 status=claimed
- Direct causes: 2
- Transitive: 5
## Direct causes
- [federation:dependency] eu-hub:4 kind=release — task api-rollout depends on schema-migration
- [lifecycle] us-hub:1 kind=ledger_task — ledger_task → claim
## Transitive
- eu-hub:1 kind=ledger_task task=schema-migration
- eu-hub:2 kind=claim task=schema-migration owner=eu/claude-11ab status=claimed
- eu-hub:3 kind=task_update task=schema-migration owner=eu/claude-11ab status=done
- eu-hub:4 kind=release task=schema-migration
- us-hub:1 kind=ledger_task task=api-rollout
With --dot the same federated answer renders as a Graphviz digraph: every
hub becomes a cluster, so an edge inside a cluster is same-hub causality
and an edge crossing cluster boundaries is a federation edge — drawn in
colour and labelled with its basis. The rendered edges are the query's
induced subgraph (also carried in the JSON as edges), so the picture shows
the whole causal neighbourhood, not just the one-hop links; the queried node
is double-bordered and a counterfactual's unsupported descendants are
dashed. Replayed against the same two stores (dot -Tsvg renders it):
$ synapse causality causes ./eu-hub.db us-hub:2 --peer us-hub=./us-hub.db --dot
digraph federated_causality {
...
subgraph cluster_0 {
label="eu-hub";
"eu-hub:4" [label="eu-hub:4\nrelease schema-migration", shape=box];
...
}
subgraph cluster_1 {
label="us-hub";
"us-hub:2" [label="us-hub:2\nclaim api-rollout", shape=box, peripheries=2];
...
}
"eu-hub:3" -> "eu-hub:4" [label="lifecycle"];
"eu-hub:4" -> "us-hub:2" [label="federation:dependency", color=blue];
}
contention stays single-hub (--peer is refused there): yield advice
weighs one hub's live claims, and claims are never granted across hubs.
Exit codes are unchanged — 2 additionally covers a malformed --peer
spec, a duplicate hub id, or a reference naming an unmerged hub; --dot
without --peer is refused, and --dot excludes --json.
synapse causality otel ./synapse.db --out spans.json projects the whole
graph onto OpenTelemetry spans: one trace per task (root span covering the
task's recorded lifetime), one child span per coordination event, and — the
part that carries the causality — a span link on every event a recorded
dependency or contention edge enabled, pointing at the causing event's
span in the other task's trace. "This claim proceeded because that release
freed its paths" renders as a first-class link in any trace viewer. Ids are
deterministic SHA-256 derivations of the task id and event sequence, so
re-exporting the same log yields identical spans and cross-task links always
resolve. --out FILE writes the span records as JSON with no extra
dependency; --endpoint URL pushes real OTLP over HTTP to a collector's full
traces URL (typically http://host:4318/v1/traces) and needs the optional
extra: pip install 'synapse-channel[otel]'. Exactly one of the two is
required; a failed push exits 2 with the exporter's verdict rather than
pretending success, and taskless events are counted in the summary line, not
silently dropped. Replayed against a real hub log and a real OTLP collector
(otel/opentelemetry-collector, debug exporter):
$ synapse causality otel ./synapse.db --endpoint http://127.0.0.1:4318/v1/traces
exported 5 span(s) across 1 trace(s) to http://127.0.0.1:4318/v1/traces
The collector's log confirms the batch (service.name=synapse-channel,
spans: 5). Timestamps are the hub's own event timestamps; the projection is
read-only and, like every causality mode, contacts no live hub.
Three flags shape the projection. --service-name NAME overrides the
service.name resource on the exported spans (default synapse-channel), so
several hubs exporting into one observability tenant stay distinguishable.
--filter TASK_ID (repeatable) projects only the named tasks' traces and
refuses a task the log does not record; links pointing at excluded tasks are
kept — the deterministic ids resolve against any export that carried the other
task — and the exclusions are counted in the summary, never silently truncated.
An event recording the task lifecycle's failure terminal (failed) projects
span status ERROR — as does a task root whose final recorded status is the
failure terminal — so failed coordination is visible at a glance in a trace
viewer; everything else stays UNSET, because the log records progress, not
success verdicts. Replayed against a real 333-task hub log:
$ synapse causality otel ~/synapse/hub.db --out spans.json \
--service-name workstation-hub --filter OUTPUT-INTEGRITY-REAL-SURFACE --max-nodes 0
exported 3 span(s) across 1 trace(s) to spans.json, 332 task(s) filtered out
--watch turns the one-shot export into live coordination observability:
the store is reread and the spans re-exported every --interval seconds
(default 2.0) until --count ticks ran (0 = until Ctrl-C, which stops the
watch cleanly). The deterministic ids make each re-export idempotent on the
collector side — a span received twice is stored once — so a fixed cadence
against a growing log simply fills in the new events. A failing tick stops
the watch with exit 2, exactly as a single export fails visibly:
$ synapse causality otel ~/synapse/hub.db --out spans.json --watch --count 2 --interval 1 --max-nodes 0
exported 2582 span(s) across 338 trace(s) to spans.json
exported 2582 span(s) across 338 trace(s) to spans.json
synapse causality health ./synapse.db walks each task's recorded lifecycle
in the same causal graph and flags three shapes that usually mean coordination
went wrong: orphaned claims (a claim is its task's last recorded event —
claimed, then silence), dangling dependencies (a declared depends_on
whose task never completed, using exactly the completion predicate the
dependency-edge derivation uses, so the two never disagree), and stale
claims (claimed, never released, silent longer than --stale-after seconds,
default 3600). Ages are measured against the log's own final timestamp — never
the wall clock — so the assessment is deterministic and replayable. Exit 1
signals at least one anomaly, mirroring contention; every signal is an
operator hint derived from recorded events, not a verdict — an orphaned claim
may simply be an agent mid-work. Replayed against the real workstation hub log
(341 tasks), where it flagged 22 anomalies including a claim the session
itself was holding:
$ synapse causality health ~/synapse/hub.db --max-nodes 0
# Causal health: 22 anomalies across 341 task(s)
...
## Orphaned claims (claimed, then silence)
- seq=4807 task=causality-health owner=SYNAPSE-CHANNEL/claude-2759-work silent 238s
...
## Dangling dependencies (declared, never completed)
- seq=1532 task=director-ai-calib-gateb depends on remanentia-ms1-query-stream, which never completed
--since TS bounds the scan to events with ts >= TS — the focus control
for a large log, mirroring the trust graph's --since. Honest scope: a task
whose entire recorded lifecycle predates the window is not assessed, and a
window-straddling task is judged on the window's evidence only.
With --watch the assessment becomes a standing monitor: every
--interval seconds (default 2, --count bounds the ticks) the store is
reread and re-assessed, the first tick prints the full report as the
baseline, and every later tick prints only the transitions — + fact
for a new anomaly, - fact for a cleared one — so a steady fleet stays
quiet and the scrollback reads as a timeline of what went wrong and what
recovered. The facts carry each anomaly's identity (kind, anchoring
sequence, task, owner) and deliberately omit the ages, which grow every
tick; --json streams the full report as one NDJSON line per tick
instead. A failing tick stops the watch with exit 2, and a bounded watch
exits with the last tick's anomaly signal. Replayed against the live
workstation hub log: the baseline reported 31 anomalies across 405 tasks
and the following ticks stayed silent, exit 1 — the anomalies persist.
$ synapse causality health ~/synapse/hub.db --watch --count 2 --max-nodes 0
# Causal health: 31 anomalies across 405 task(s)
...
$ echo $?
1
synapse merkle root ./synapse.db commits the durable event log to a single
Merkle root: a 32-byte fingerprint of every event, so two operators — or two
federated hubs — holding the same log derive the same root and a mismatch proves
the logs differ (--expect ROOT gates on a trusted value, --through SEQ commits
only up to a sequence). synapse merkle prove ./synapse.db 142 emits an O(log
n) inclusion proof — the sibling hashes that reconstruct the root from one
event's leaf — and synapse merkle verify proof.json checks such a proof offline
with no event store, the light-client verification a follower runs against a
trusted root (--expect ROOT also pins the root). The tree follows RFC 6962
(Certificate Transparency): leaves and interior nodes carry distinct
domain-separation prefixes, so a leaf hash cannot be forged as an interior node.
The commitment proves what the log contains — integrity and inclusion — not the
semantic correctness of the coordination it records. root/prove exit 2 on a
missing store and prove exits 1 when no event has that sequence; verify
exits 0 valid, 1 on a bad proof or root mismatch, 2 on an unreadable file.
verify reports through its exit code and a stderr line by default; pass --json
to get a {"valid", "seq", "root"} verdict on stdout instead (with a reason when
invalid), matching the --json stdout payload that root and prove already
carry.
synapse merkle keygen PATH creates the hub deployment's receipt-signing
keypair: an Ed25519 private key at PATH (owner-only 0600, never overwritten)
and its distributable public half at PATH.pub — a small JSON document whose
key_id is a fingerprint derived from the key material. The private key signs
receipt commitments (synapse verify-release --signing-key); the .pub file is
what verifiers pass to synapse policy-check --trusted-signing-key, so a third
party can check which hub attested a receipt's log state without access to the
live log. keygen exits 2 when either file already exists.
synapse reliability ./synapse.db builds evidence-only reliability memory from
the same event store. It counts stale claims, declared failed-check evidence,
broken handoff candidates, and reconstructed conflict pairs per owner. The
output is audit signals, not scores: it does not rank agents, assign trust
grades, or prove intent.
synapse trust-graph ./synapse.db projects the same evidence as a queryable
graph: typed edges between agent and task nodes — positive_receipt (from
positive release receipts tied to a prior board task), stale_claim,
declared_failed_check, broken_handoff_candidate, and one agent-to-agent
conflict_pair edge per reconstructed conflict — each carrying the event-log
sequence, timestamp, and evidence fields that created it. Focus a review with
--agent NAME, --task ID, or --since TS (the decay window: evidence older
than the timestamp drops out of the view without being deleted from the log),
and choose the projection with --json or --dot (Graphviz; agents are
ellipses, tasks boxes, conflicts dashed undirected edges). Like reliability,
it is evidence with provenance, not scores. Against a store holding one
positive receipt, one degraded receipt, a stale claim, an overlapping claim
pair, and an expired handoff:
$ synapse trust-graph ./synapse.db
Trust graph: evidence with event-log provenance, not scores; authorship is as recorded in the local log and is not cryptographically verified here
generated_from_seq=7 as_of=100.000
nodes=8 edges=6
alpha -[positive_receipt seq=2]-> ROUTING: release receipt: evidence=pytest tests/test_routing.py -q
alpha -[declared_failed_check seq=3]-> ROUTING: release receipt: known_failures=mypy failed; epistemic_status=degraded
beta -[stale_claim seq=4]-> STALE: lease expired at 20.000 by as_of 100.000
alpha -[conflict_pair seq=6]-> beta: OVERLAP-A@alpha overlaps OVERLAP-B@beta
gamma -[broken_handoff_candidate seq=7]-> HANDOFF-BROKEN: handoff recipient had no later task update/checkpoint/release before lease expiry 30.000
gamma -[stale_claim seq=7]-> HANDOFF-BROKEN: lease expired at 30.000 by as_of 100.000
Piping --dot into Graphviz (synapse trust-graph ./synapse.db --dot | dot
-Tsvg -o trust.svg) draws the same evidence for a review meeting; every edge
label keeps the seq pointer back to the log.
synapse cross-repo ~/code widens coordination from one repository to the
whole checkout tree. It scans every immediate subdirectory of the root for
dependency manifests — pyproject.toml, Cargo.toml, package.json,
go.mod — and CODEOWNERS files, and composes them into a graph: a
dependency edge where one repository's manifest names a package another
scanned repository declares (external dependencies create no edge), and a
shared_owner edge where two repositories cite the same CODEOWNERS handle. A
manifest that exists but cannot be parsed is reported as a problem, never
silently skipped (TOML manifests need Python 3.11+ or the tomli package).
With --db the live claims of a hub event log join the graph — a claim's
worktree is its repository — and with --repo the report focuses on one
repository and the exit code becomes a coordination signal: 1 when a live
claim exists in a repository connected to the focus by a dependency edge,
answering "is anyone working right now in a repository mine depends on, or
one that depends on mine?" before a cross-cutting change starts. Against a
tree where consumer declares a dependency on the package provider
publishes, with live claims in both:
$ synapse cross-repo ./org --db ./synapse.db --repo consumer
Cross-repository dependency graph: declaration-level evidence, advisory only
root=./org repositories=3 edges=2 focus=consumer
consumer -[dependency]-> provider: consumer depends on provider-pkg (python) provided by provider
consumer -[shared_owner]-> provider: consumer and provider share owner(s) @org/platform
Live claims
provider [depends_on] PROV-1@agent-a seq=1 paths=src/thing.py
consumer [self] CONS-1@agent-b seq=2 paths=src/thing.py
The command exits 1 here because provider — a repository the focus
depends on — holds a live claim. --json emits the graph as data and
--dot a Graphviz digraph (the focus double-bordered, repositories holding
live claims labelled with their count, shared-owner edges dashed,
version-conflict edges red). Like every analysis surface it is
declaration-level, advisory evidence: it reads manifests and the log,
resolves nothing, and decides nothing.
The graph also flags declared version constraints that can never be
satisfied together. Every package two or more scanned repositories consume —
including external packages that create no dependency edge — is checked
pairwise, and a version_conflict edge appears when the constraints are
provably disjoint: no version lies in both declared ranges. When
consumer pins httpx>=0.27 while provider pins httpx<0.25:
$ synapse cross-repo ./org
Cross-repository dependency graph: declaration-level evidence, advisory only
root=org repositories=2 edges=3
consumer -[dependency]-> provider: consumer depends on provider-pkg (python) provided by provider
consumer -[shared_owner]-> provider: consumer and provider share owner(s) @org/platform
consumer -[version_conflict]-> provider: consumer pins httpx '>=0.27' but provider pins '<0.25' (python)
The comparison is deliberately conservative so a flagged conflict is always
defensible: it models PEP 440 specifier sets, Cargo requirements, and npm
semver ranges over plain numeric release versions, and anything outside
that bounded model — pre-release or epoch segments, unrecognised
operators — never claims a conflict. Direct URL references stay
uncompared with one honest exception: two references to the same base
URL pinned at two hex revisions of which neither prefixes the other are
provably two different commits and do conflict (identical revisions
overlap; a branch or tag revision is mutable and never supports a claim).
PEP 440 exclusions
(!=) are ignored, which can only suppress a claim, never invent one.
go.mod requirements are never compared: a Go requirement is a minimum
that minimal version selection reconciles, and a different major version is
a different module path. This stays declaration-level satisfiability — not
a resolver: no lockfiles, no transitive closure, no knowledge of which
versions are actually published.
--suggest-resolution turns each detected conflict into actionable advice:
for every provably conflicting package it intersects all consumers'
declared ranges (the same bounded interval model, so advice and detection
never disagree) and names which single repository's declaration is the
odd one out — the one whose removal leaves every other consumer a
common range, rendered so the operator sees what the rest already agree
on. When a concrete version inside that remainder is already named by one
of the remaining declarations (an inclusive ==, >=, or <= bound),
the advice says so — a pin lifted from a manifest some consumer already
wrote, never invented, because the scanner reads manifests, not package
indexes, and cannot know what an index publishes. When no single
declaration is the outlier the constraints split into
mutually disjoint camps, and the advice says so instead of guessing;
declarations outside the bounded model are listed as unassessed, never
silently skipped. Advisory text only — nothing rewrites a manifest.
Replayed against the real GOTM checkout tree, where it named both sides of
a cryptography standoff and honestly refused the nine-way
scpn-studio-platform split:
$ synapse cross-repo ~/code --suggest-resolution
...
### python cryptography
- DIRECTOR-AI declares '>=42,<49' (pyproject.toml)
- SCPN-PHASE-ORCHESTRATOR declares '>=49,<50' (pyproject.toml)
...
- ODD ONE OUT: DIRECTOR-AI ('>=42,<49') — the other declarations reconcile at >=49, <50; 49 would satisfy them all (a version SCPN-PHASE-ORCHESTRATOR already declares)
- ODD ONE OUT: SCPN-PHASE-ORCHESTRATOR ('>=49,<50') — the other declarations reconcile at >=42, <49; 42 would satisfy them all (a version DIRECTOR-AI already declares)
### python scpn-studio-platform
...
- no single declaration is the odd one out; the constraints split into mutually disjoint camps
Like status, the report can stand watch: --watch rescans the manifests
and rejoins the claims every --interval seconds (default 2, --count
bounds the refreshes, Ctrl-C stops cleanly). A TTY clears and redraws the
report in place; piped output appends each report behind a --- divider,
and --json --watch streams one compact document per refresh (NDJSON).
The exit code reports the last refresh's --repo signal. --dot does not
combine with --watch.
With --notify-cmd CMD the watch also tells someone: whenever the
coordination facts — live claims joined to the graph and provable version
conflicts — change between two consecutive refreshes, CMD runs with the
delta on stdin (+ fact appeared, - fact cleared) and the scanned root
in SYNAPSE_CROSS_REPO_ROOT. It fires on transitions only: never on the
first refresh (the baseline) and never on a steady state, so a quiet fleet
stays quiet. The command is shlex-split and run without a shell (wrap in
sh -c '…' for pipes); a failing or hanging sink is reported on stderr
and never stops the watch. The sink is deliberately generic — a desktop
notifier, synapse send back onto the bus, or anything else — so the
scanner stays decoupled from any live hub. Validated end-to-end: a claim
seeded between ticks delivered this to a real sink script:
+ claim repo-a LIVE-1@bob [self]
synapse benchmark measures the installed package on your machine and
prints a scorecard. The probes exercise real production surfaces — durable
event-store appends and a full journal replay against a temporary SQLite
file, the lite relay encoding over realistic envelopes, and who
request/response plus claim-to-grant round-trips over a real WebSocket
connection to an in-process hub on a loopback port. The
durable-claim-grant probe gives that hub a real temporary SQLite journal,
forces claim and release through synchronous=FULL, and reports p50/p95/max
event-loop delay beyond a concurrent 1 ms scheduler interval. The
state-deepcopy probe seeds a busy fleet and times the copy-on-write deep copy
of the authoritative SynapseState that each durable mutation takes, so its
p50/p95 quantify how the commit-before-publish copy scales with live-claim
count. Each probe
reports throughput and p50/p95 per-operation latency, and the scorecard carries the
context that makes a number honest: package version, interpreter, CPU model
and governor, and the load averages before and after the run, plus an
explicit isolation label — a run of this command is shared-workstation
evidence for functional comparison and regression hunting, not an
isolated-core production benchmark. --list names the probes, --probe
selects a subset (repeatable), --iterations overrides the defaults,
--json emits the scorecard as data, and --results FILE also writes it to
disk. The committed reference numbers in Benchmarks come
from the deeper repository harnesses; this command is the quick scorecard
for your installed version.
A saved scorecard becomes a regression gate with --compare BASELINE.json:
the run is measured as usual, then every directional metric shared with the
baseline — throughput (*_per_second, higher is better) and latency
percentiles (*_ms, lower is better) — is checked for drift beyond
--tolerance (default 25%, sized for shared-workstation noise), and any
regression exits 1. Ungated context metrics (byte ratios, rebuilt-claim
counts) never gate. A baseline recorded on a different CPU model is refused
outright — that comparison would measure hardware, not the package — while
softer drift (governor, interpreter, package version) is reported as loud
warnings. A real second run against a baseline saved minutes earlier on the
same loaded workstation:
Baseline comparison (tolerance ±25%)
encode-lite/messages_per_second: 213,906.67 -> 282,884.31 (+32.2%, higher-is-better) ok
event-store-append/events_per_second: 217.89 -> 190.07 (-12.8%, higher-is-better) ok
event-store-append/p50_ms: 3.50 -> 3.50 (+0.0%, lower-is-better) ok
event-store-append/p95_ms: 10.66 -> 13.81 (+29.5%, lower-is-better) REGRESSION
1 regression beyond ±25% tolerance
That tail-latency trip is the isolation label speaking: on a shared
workstation p95 jumps with scheduler load, which is exactly why the default
tolerance is generous and why a CI gate should compare like against like —
record the baseline on the machine (and load profile) the gate runs on.
Under --json the emitted document gains a comparison object beside the
scorecard; --compare composes with --results, so a passing run can
become the next baseline.
Where --compare gates one run against one baseline, --trend STORE.db
watches the long arc: the finished scorecard is appended to a local SQLite
history and every stored run renders as per-metric sparkline trend lines —
first and latest values, the observed range, and the series shape — so a
slow regression no single gate trips stays visible. A change of CPU model,
governor, or package version between consecutive runs is annotated as an
explicit context break rather than silently connected; unlike
--compare, a differing CPU model is annotated, not refused, because a
history legitimately spans upgrades. The store is a plain SQLite file the
operator owns. Two real runs on the same workstation:
$ synapse benchmark --probe encode-lite --trend bench-trend.db
...
Benchmark trend: 2 stored run(s)
encode-lite lite_to_raw_ratio: ▁█ 0.72 → 0.72 (min 0.72, max 0.72, 2 runs)
encode-lite messages_per_second: ▁█ 31,585.98 → 60,645.33 (min 31,585.98, max 60,645.33, 2 runs)
Under --json the document gains a trend object with the full stored
history and its context breaks; --trend composes with both --results
and --compare, and --export-csv FILE also writes the history as
long-format CSV (one row per stored metric value, the context columns on
every row) for spreadsheets and external monitors. For consoles and CI log viewers without UTF-8, --ascii
renders the same trend block in printable ASCII — the glyph ramp becomes
._-=+*#%@ and the arrow and dash punctuation degrade to -> and --
(it requires --trend; the stored history and the JSON document are
unchanged):
$ synapse benchmark --probe encode-lite --trend bench-trend.db --ascii
...
Benchmark trend: 2 stored run(s)
encode-lite lite_to_raw_ratio: .@ 0.72 -> 0.72 (min 0.72, max 0.72, 2 runs)
encode-lite messages_per_second: .@ 31,585.98 -> 60,645.33 (min 31,585.98, max 60,645.33, 2 runs)
--alert turns that history into a deterministic statistical gate: the
latest value of every probe metric is measured in sigma distances from the
sample mean of its predecessors, and a value further out than
--alert-sigma (default 3) is a drift finding that exits 1. Honesty
bounds the statistics the same way the rendering bounds the sparklines:
only the trailing same-context segment (same package version, CPU
model, and governor as the latest run — exactly the fields the context
breaks annotate) forms the population, a series with fewer than
--alert-min-samples same-context samples (default 5, floor 3) is
reported as insufficient and never silently gated, and a perfectly flat
baseline has no sigma, so any deviation from it is flagged as such.
Captured from a real run against a store seeded with a flat 100 msg/s
history:
$ synapse benchmark --probe encode-lite --trend bench-trend.db --alert
...
Drift alert: 1 finding(s) across 1 gated series (5 same-context run(s), sigma 3, floor 5)
DRIFT encode-lite messages_per_second: 997,319.24 is off a flat baseline (baseline mean 100.00, std 0.00, 5 samples)
insufficient samples for encode-lite lite_to_raw_ratio: 1 of 5 same-context run(s) — not gated
$ echo $?
1
Under --json the document gains a drift object with the findings and
the insufficient series; --alert composes with --compare (either gate
failing exits 1) and requires --trend.
synapse accounting records and reports opt-in model cost/token usage. Synapse
never calls a model provider and collects no telemetry, so token and cost figures
exist only when an agent or operator records them: synapse accounting record
posts one usage note (a usage-kind progress note carrying a canonical
key=value body) onto the shared ledger, and synapse accounting report
./synapse.db reads those notes back into per-agent and per-model totals. Pass
--pricing pricing.json (model → {input_per_1k, output_per_1k}) to estimate
cost from tokens, and --budget budget.json (agent → ceiling) for budget
evidence. Budgets are evidence, not an enforcement gate: the report states spend
against a ceiling, it does not block work. Non-Python clients can record usage by
posting the identical note body.
synapse fleet-scorecard ./synapse.db --out fleet-scorecard.json composes the
existing causality, accounting, contention, and reliability reports into one
portable schema. Add --trend bench-trend.db to include the complete benchmark
history and context breaks. The output is replaced atomically with owner-only
permissions because task identities and opt-in cost evidence can be sensitive.
No source report is weakened: accounting remains opt-in evidence, contention
remains advisory, reliability remains findings rather than scores, and benchmark
numbers remain host-dependent.
With the optional otel extra,
synapse fleet-scorecard ./synapse.db --endpoint http://127.0.0.1:4318 pushes
the existing deterministic causality spans to /v1/traces and the scorecard
gauges to /v1/metrics. --endpoint is a collector base URL; embedded
credentials, query strings, fragments, and pre-appended signal paths are
refused. The JSON bundle carries full benchmark history; the metric plane sends
only the latest value and a same-package, same-host relative change, never a
fabricated historical backfill. The command reads local stores only; the two
collector posts are its sole network actions and are not transactional. If one
signal export fails, the command exits 2 even if the collector already accepted
the other, so rerun it after repairing the endpoint.
synapse approval runs a human-in-the-loop approval gate over the same ledger.
synapse approval request --name <actor> --subject <id> posts an
approval-kind note that puts the subject in awaiting_approval; synapse
approval decide --subject <id> --approve|--reject [--reason ...] records the
decision; and synapse approval status ./synapse.db [--subject <id>] [--pending]
[--json] replays the notes into the current decision state per subject (the
latest event wins, so a fresh request after a decision re-opens the gate). It is
advisory evidence and an audit trail, not a hard runtime gate — nothing blocks a
hub mutation. An approved subject can be cited in a release receipt via synapse
release --approval "<id>: approved by <actor>".
synapse ttl-advice ./synapse.db builds read-only adaptive lease TTL advice from
the same event store. It derives completed-task duration samples, active
live-claim counts, and stale-claim counts, then prints an advisory default and
optional owner-specific rows when enough samples exist. The command does not
change the hub default, and explicit manual TTL values remain the control path.
synapse auto-action introspects the opt-in auto-action reactor — the layer that
turns the session advisor's per-round signals into automatic actions (compact a
filling context, write a log, hand a run over). The bare command previews the
static model: which advisory signal maps to which action, which signals
deliberately map to none (over-budget halts the loop; approaching-rate-limit is
steered by the router), and — given --arm compact,log or --all — how a
hypothetical armed set would read. It touches no files and fires nothing.
The reactor is armed in-process by an orchestration loop rather than by a
hub-side toggle, but which actions are armed is now a durable policy the loop can
read: synapse auto-action arm compact,log and disarm log add or remove actions,
clear disarms everything, and show renders the persisted posture. The policy is
a JSON file in the coordination home ($SYN_HOME or ~/synapse, overridable with
--store PATH); an orchestration harness loads the same file to build its dispatch,
so the terminal and the loop agree on one persisted armed set. Persisting a policy
still fires nothing — an armed action fires at runtime only when its signal is
raised and a handler was supplied. Add --json to show or the bare command for
the machine-readable form.
Observe or follow a peer hub's event log with synapse multihub observe or
synapse multihub follow; both read the peer log rather than the local one.
multihub observe --peer-db ./peer.db folds a peer's log file offline into its
board and claims; multihub follow --peer-uri ws://peer:8876 pulls the same
snapshot from a live peer over a connection (--pin sha256:<hex> accepts a
self-signed wss:// peer by certificate pin). Both are read-only observations
tagged with a peer id and neither mutates the local hub.
If a standing watcher durably quarantines an equivocating peer, stop the local hub, verify the replacement peer log or checkpoint out of band, and record the accepted generation before restarting:
synapse multihub recover \
--journal ./hub.db \
--peer-id west \
--operator incident-commander \
--reason "accepted replacement after checkpoint verification" \
--new-log-generation west-2026-07-29-g2
The command refuses a peer with no active durable quarantine. It commits the recovery at full durability, stores only a SHA-256 digest of the free-text reason, and leaves the peer's event log untouched. It must run while the hub is stopped because a live watcher also holds quarantine state in memory. The generation/checkpoint label records the operator's acceptance; the current wire does not independently attest it. On restart, the follower reloads the recovery, drops the prior peer view, and begins again at cursor zero. See Multi-hub sync for the federation and trust model.
The hub itself can run that follower standing, feeding partition detection:
synapse hub --port 8876 --hub-id syn-a \
--namespace-owner MY-PROJECT=syn-a \
--multihub-watch syn-b=ws://peer:8876 --multihub-watch-interval 30
--namespace-owner NS=HUB_ID (repeatable, requires --hub-id) declares the
single authoritative owner of each namespace, deny-by-default: an ungoverned
namespace grants nothing, a remote-owned claim is refused with the owner named.
Remote-owned claims with a configured owner route are forwarded to the owning hub;
the owner treats a retry of the same (task_id, claimant) as idempotent and
relays the existing lease without renewing or double-counting it. A forwarding
timeout is refused in place with forward_error=timeout, not hidden behind the
generic ownership refusal. /metrics exposes
synapse_forwarded_claims_total, synapse_forwarded_claims_granted_total,
synapse_forwarded_claims_denied_total, and
synapse_forwarded_claim_timeouts_total.
--multihub-watch PEER=URI (repeatable, requires --namespace-owner) polls
each named peer's event log on a bounded interval and folds the claims it
observes into the asserting-owners view (--multihub-watch-pin PEER=sha256:<hex>
pins a self-signed wss:// watch peer's certificate, mirroring
multihub follow --pin; the peer must also be named by --multihub-watch) — a namespace a watched peer is seen
contesting resolves as partitioned and refuses to grant until the contest
clears. Naming a peer is the operator confirmation for the always-on outbound
connection; a failed poll keeps the last successful observation, so a link
outage never lets a contested namespace silently resume granting. Validated
live: a claim held on the watched peer flips the namespace to repeated
partitioned refusals, and the peer's release clears it on the next poll.
--claim-peer HUB_ID=URI (repeatable, requires --namespace-owner) declares the
route this hub uses to forward a claim it does not own to the owning hub. The
key is the owning hub id from --namespace-owner; a remote-owned claim with a
matching route is relayed to the owner and its verdict returned, carrying the
owner's authentic lease, while a remote-owned claim whose owner has no route stays
refused with the owner named — the routing is opt-in and fail-closed, so adding a
route never opens a claim the hub could not already resolve. --claim-peer-token
TOKEN sends an authentication token on every forwarded claim to owners that gate
the first frame (mutual-TLS-only owners need none). An unreachable owner is refused
in place with forward_error=timeout, not hidden behind the generic ownership
refusal, so a forwarding outage never silently resumes local granting:
synapse hub --port 8876 --hub-id syn-a \
--namespace-owner OTHER-PROJECT=syn-b \
--claim-peer syn-b=ws://peer:8876
Agent2Agent bridge¶
synapse a2a-card projects the live SYNAPSE capability manifest into an A2A
Agent Card. synapse a2a-serve runs the local HTTP+JSON bridge and keeps A2A at
the edge of the system; the hub remains WebSocket-native.
The bridge is an interop surface for A2A-shaped clients, not a replacement for
orchestration frameworks, coding agents, or the native SYNAPSE hub protocol.
synapse a2a-conformance prints the current support matrix against the A2A
1.0.0 operation model, including rows that remain externally gated.
Supported local subset:
GET /.well-known/agent-card.jsonand/agent-card.jsonfor discovery.POST /message:sendto create a bridge task and forward text/data/file parts into SYNAPSE chat.POST /message:streamfor an immediate Server-Sent Events task snapshot.GET /tasks,GET /tasks/{id},POST /tasks/{id}:cancel, andPOST /tasks/{id}:subscribefor bridge-local task lifecycle operations.POST|GET|DELETE /tasks/{id}/pushNotificationConfigs[/config_id]for stored push-notification configuration.GET /tasks/{id}/pushNotificationDeliveriesfor the authenticated SYNAPSE extension that lists credential-free durable attempt evidence. JSON-RPC usestasks/pushNotificationDelivery/listfor the same task-scoped view.POST /rpcfor JSON-RPC 2.0 dispatch to the same operations.
Operational boundaries:
- Bearer auth is opt-in with
--bearer-auth --a2a-token-file /run/secrets/synapse-a2aand applies to protected bridge routes. The file must be owner-only and is read from the validated descriptor; errors never contain its value. The process-visible--a2a-token "$A2A_TOKEN"compatibility form wins when both are supplied. The public Agent Card remains public discovery. a2a-clientanda2a-interop-traceaccept the same--a2a-token-file/--a2a-tokenprecedence. With a resolved bearer, they refuse plaintext HTTP outside a literal IPv4/IPv6 loopback destination before network I/O.--a2a-allow-insecure-httpis the explicit cleartext-risk override; it does not disable certificate verification for HTTPS.- Both outbound clients cap each response at one MiB, apply the shared
64-level JSON nesting guard plus a 4,096-member cumulative shape ceiling,
and reduce malformed or wrong-shape peer bodies to fixed value-free failure
kinds. Their
--outputreceipts use owner-only sibling temporary files, fsync, and atomic replacement. Certificate pinning and outbound client-certificate identity are not provided by these controls. --grpc-port PORTenables a default-off, optional JSON-over-gRPCSendMessage/GetTasksubset on the same--host. The integrated CLI applies the selected A2A shared bearer and native TLS/mTLS files to both transports. It maps--max-concurrent-requeststomaximum_concurrent_rpcs, caps gRPC requests and responses at one MiB, uses the bounded JSON parser, returns stable value-free errors, and requires every gRPC call to carry a finite deadline no longer than--request-read-timeout; pending hub work is cancelled when the deadline expires. The shippedA2AGrpcClientsupplies that deadline and accepts the shared bearer and channel credentials explicitly. With native TLS the advertised interface usesgrpcs://; otherwise it usesgrpc://. HTTP Host/Origin checks are browser/HTTP controls and do not apply to gRPC. The shared bearer is not per-client identity or a method-level ACL, and the custom two-method binding remainspartial. See Current gRPC activation boundary.- Bind exposure (hub R4 parity for the HTTP edge): the bridge defaults to
loopback (
127.0.0.1). A non-loopback host is refused without bearer auth, and a non-loopback host with bearer auth is also refused while the bridge listens on plaintext HTTP so the bearer never rides the LAN in the clear by default. Prefer native HTTPS with--tls-certfileand--tls-keyfile, or a loopback bind behind a TLS-terminating reverse proxy (see the A2A deployment threat model).--insecure-off-loopbackdowngrades either refusal to a warning for a trusted private network; it is not a production deployment mode. The bind matrix:
| Host | Bearer | TLS / override | Result |
|---|---|---|---|
| loopback | no or yes | — | allow |
| non-loopback | no | no override | refuse |
| non-loopback | no | --insecure-off-loopback |
warn + allow |
| non-loopback | yes | plaintext HTTP, no override | refuse |
| non-loopback | yes | --tls-certfile + --tls-keyfile |
allow (native HTTPS) |
| non-loopback | yes | --insecure-off-loopback |
warn + allow |
Align --endpoint-url with the listen scheme: native TLS should advertise
https://…; an https:// card without native TLS assumes a reverse proxy.
synapse doctor --a2a-policy (plus --a2a-bind-host / --a2a-bearer-auth /
--a2a-tls / --a2a-endpoint-url) reports the same bind matrix without
starting the bridge.
- Every HTTP request must use an exact Host authority derived from
--endpoint-url, including requests withoutOrigin. Missing, malformed, ambiguous, or untrusted Host values are refused403on every route, the public agent card included, before authentication. Reverse proxies must preserve the advertised Host. --allow-origin ORIGINadditionally permits browser requests from one exact concrete web origin (scheme://host[:port]; repeat for several). Without it, every request carryingOriginis refused; opaquenullis never allow-listed. Origin-less clients remain compatible through the always-on Host boundary. The independent checks guard against DNS rebinding and drive-by browser requests.--state-filepersists bridge tasks, push configs, and bounded push-delivery attempt evidence. Corrupt state files fail fast; non-terminal persisted tasks recover as failed on restart; failed writes roll back the corresponding in-memory view.- Terminal task states are immutable: cancel and late SYNAPSE replies do not reopen or rewrite completed, failed, canceled, or rejected tasks.
--task-timeoutmarks open tasks failed when no correlated SYNAPSE reply arrives within the configured window.- A2A task correlation uses structured SYNAPSE chat metadata (
a2aTaskIdanda2aContextId). Inline marker-looking text in a payload is preserved as reply text and is never trusted to select a task. --subscribe-timeoutbounds one in-process subscription wait. Subscriptions emit bounded local replay frames for the current bridge process, then at most one queued update for that wait. Persisted task recovery restores task snapshots only; it does not rebuild durable subscription streams across restarts or bridge replicas.--max-concurrent-requests(default32) is the hard ceiling on concurrent in-flight A2A HTTP handlers. Extra accepted connections receive deterministic503 Service Unavailablewith reasonA2A_HTTP_CAPACITY_EXHAUSTEDand never enter the request handler. The admission slot is released on normal completion, parse error, read timeout, client disconnect, and handler exception.--request-read-timeout(default30seconds) is the wall-clock budget to finish reading one declared HTTP request body. Incomplete or stalled bodies receive deterministic408 Request Timeoutwith reasonA2A_HTTP_READ_TIMEOUTbefore JSON-RPC or message dispatch. The existing1 MiBbody cap still applies first.- Caller-supplied
taskIdandcontextIdvalues are restricted to bridge-safe characters. Duplicate caller task ids are rejected. - Webhook URLs must be HTTP(S), include a host, omit embedded credentials, and not
target localhost, loopback, private, or link-local IP literals. Redirects
retain the same DNS-pinning and address checks. Credential-bearing
301/302/303 are refused; 307/308 may preserve sensitive headers only across an
exact normalized same-origin redirect. HTTPS-to-HTTP is always refused and
redirect chains stop after five. Initial authenticated webhook URLs are not
yet forced to HTTPS, so configure them with HTTPS explicitly. Inspect the
fixed rule with
synapse doctor --a2a-policy. - Push delivery makes at most three attempts: immediately, after 0.25 seconds,
and after one additional second. Each expected network failure is reduced to
timeout,url-error, oros-error; exception text, URL, headers, payload, and authentication material never enter delivery evidence. Evidence records a per-update delivery id, task/config ids, attempt, failure class, retry delay/time and 1.25-second retry-delay window, then ends insucceededordead-lettered. The delivery id keeps concurrent or repeated task updates independently auditable. Each transport attempt retains its separate five-second socket timeout. A webhook failure is delivery truth only: the valid task transition is stored first and is never rewritten by retry exhaustion.
State-file durability matrix:
| Case | Behavior | Focused coverage |
|---|---|---|
| Clean restart | Tasks, push configs, and bounded credential-free delivery evidence reload from --state-file. |
test_task_store_persists_tasks_and_push_configs, test_a2a_task_store_persists_bounded_credential_free_delivery_evidence |
| Corrupt JSON | Startup fails fast with Invalid A2A state file. |
test_task_store_reports_corrupt_state_file |
| Atomic write | Writes go through an owner-only temp file, fsync the file, replace the state file, and best-effort fsync the parent directory. | test_a2a_task_store_fsyncs_state_file_and_parent_directory |
| Failed write | In-memory task/config changes roll back; the previous committed state file is left intact. | test_a2a_task_store_keeps_committed_state_file_when_temp_write_fails |
| Stale in-flight task | Persisted non-terminal tasks recover as failed on restart. | test_state_file_recovery_fails_stale_working_tasks |
| Push config recovery | Push configs persist, reload, list, get, delete, and roll back failed writes/deletes. | test_a2a_task_store_push_config_get_list_delete_paths |
| Push outcome recovery | Each attempt is atomically appended; failed evidence writes roll back, task expiry removes its evidence, and authenticated HTTP/JSON-RPC reads remain task-scoped. | test_a2a_task_store_rolls_back_delivery_evidence_when_save_fails, test_push_delivery_evidence_route_is_authenticated_and_task_scoped |
Bounded local soak coverage:
| Path | Local evidence | Limit |
|---|---|---|
| Network handler churn | Sixteen real localhost POST /message:send requests through one stdlib HTTP server persist and reload from a state file. |
This is not a latency or throughput benchmark. |
| Persistence churn | The same run exercises repeated fsynced state writes under a fixed task cap. | It does not simulate power loss or filesystem faults beyond focused write-failure tests. |
| Webhook failure pressure | Twelve task completions continue while all 36 bounded webhook attempts raise timeout errors and end in 12 durable dead letters. | It uses an injected failing deliverer with zero-delay test scheduling, not a remote receiver. |
| Subscriber fanout | Twelve concurrent subscribers receive the terminal update and the bridge clears subscriber queues. | It is bounded local thread pressure, not multi-process soak. |
Unsupported or externally gated:
- Independent evidence includes an official
a2a-sdk==1.1.0discovery/send/get/list/cancel lifecycle and an official A2A TCK HTTP+JSON MUST run (historically 55 passed, 5 structured-response failures, 175 skipped). Residual structured Message/Artifact scenarios are now answered by shipped handlers (TCK messageId prefixes /a2aScenario). This remains partial validation, not certification or full conformance. - An outbound external-server pass, remote public webhook and reverse-proxy production receipts, durable replay, and operator deployment sign-off remain open. Native HTTPS bind + dual interop-trace runs are covered in-repo.
synapse a2a-conformanceis the live local matrix for those supported, partial, unsupported, and external rows.- Exposed A2A bridge deployments should follow the A2A deployment threat model.
- The bridge does not make SYNAPSE itself an A2A-native hub; it is a separate edge process translating between A2A-shaped HTTP operations and SYNAPSE chat/tasks.
- Subscription replay is local process memory. It is not a durable event log shared across bridge restarts or multiple bridge replicas, and terminal recovered tasks reject subscription with a problem response.
Managing the task plan¶
synapse task lets a human drive the shared blackboard from the command line —
the persistent plan, not the live leases (claiming/holding a lease belongs to a
running agent, since a lease is released when its holder disconnects):
synapse task declare BUILD --title "Compile the package"
synapse task declare TEST --title "Run the suite" --depends-on BUILD
synapse board # BUILD ready, TEST blocked on it
synapse task update BUILD --status done # TEST now unblocks
synapse task progress TEST "started" --kind note
syn ack TEST --evidence "pytest tests/test_feature.py -q" --artifact coverage.xml
All three task writes accept --idem-key KEY. On a journal-backed hub, repeating
the same command with the same key replays its exact committed response without a
second board mutation, including after restart. Reusing the key for changed
request content fails with idempotency_conflict; the response does not expose
the key or either request payload. The key is scoped by authenticated sender and
message type, so declare, update, and progress keys do not collide. Omitting the
flag retains the documented at-least-once behavior.
Tasks carry an optional project scope and a monotonic version for compare-and-set updates:
synapse task declare FIX --title "Repair parser" --project SYNAPSE-CHANNEL
synapse task declare FIX --title "Repair parser" --expected-version 0 # create only if absent
synapse task update FIX --suggested-owner SYNAPSE-CHANNEL/kimi-3dcd --expected-version 2
A re-declaration with a conflicting non-empty --project is refused (a blank
value keeps the existing scope); --expected-version refuses the write unless
the board's current version matches, so two planners cannot silently
overwrite each other's assignment. A refusal (stale version, scope conflict)
prints the hub's reason and exits non-zero.
When a write follows a task event observed from another hub, bind that fact to the complete event rather than relying on wall-clock order:
synapse task update FIX --status done \
--causal-parent 'west:42:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef'
The reference is HUB_ID:SEQ:SHA256 (hub ids may themselves contain colons).
Use the event_fingerprint in multi-hub board provenance. The fold verifies the
parent identity, complete-event fingerprint, and task id before suppressing the
ancestor. A missing or invalid parent remains an unresolved head and is not
treated as proof that the updates were concurrent.
synapse dispatch¶
synapse dispatch --project P is the opt-in autonomous lane between the
board and the seats: it polls the board/state/manifest/who snapshots, picks
ready P-scoped tasks deterministically, pins suggested_owner through the
version CAS, and sends the seat a directed wake under a stable idempotency
key — exactly once, even across crashes (an append-only JSONL outbox in
~/.synapse/dispatch-outbox/ reconciles pending intents on restart: claimed
tasks are marked delivered, re-owned tasks conflicted, unanswered ones
retried with the same key up to --max-attempts).
synapse dispatch --project SYNAPSE-CHANNEL --dry-run # print the plan, mutate nothing
synapse dispatch --project SYNAPSE-CHANNEL --once # a single pass
synapse dispatch --project SYNAPSE-CHANNEL # daemon: poll every --interval seconds
Seats make themselves discoverable with synapse wait --capability-card.
A concurrent second dispatcher of the same project yields: the
dispatch:<project> singleton lease is exclusive. The dispatcher never
claims real tasks, approves, lands, or broadcasts — the woken agent claims
and updates the task itself.
The hub bounds the in-memory blackboard and memory-admission surfaces with
operator-set limits: --max-progress for the total retained progress notes,
--max-progress-per-author for one author, --max-progress-per-task for one
task id, and --max-findings-per-agent for durable findings admitted by one
agent. These limits apply on live writes and on --db replay; the append-only
event log still retains accepted events until synapse compact removes safe
history.
--board-task-cap N bounds the tasks served per board snapshot — on a
long-running fleet the full board eventually outgrows a websocket frame
(observed around a thousand tasks). Under a cap every live task is kept
ahead of any terminal one, the newest updated_at wins inside each class
when trimming, and the reply carries total_tasks and truncated so a
consumer sees the bound instead of mistaking the page for the whole plan;
the ready id list always stays complete, because ids are cheap and the
task bodies are what outgrow the frame. The default serves the full board
unchanged, and the ledger itself is never trimmed — the cap bounds one
reply, not the plan.
syn ack <task> is the ergonomic closeout path for a completed board task. It
requires at least one --evidence or --artifact value, writes those values as an
assessment progress note from the resolved syn identity, waits for the hub's
progress confirmation, marks the task done, and waits for the task-update
confirmation before printing success. Use repeated flags when a task has several
proof points:
syn ack TEST \
--evidence "pytest tests/test_feature.py -q" \
--evidence "mypy src/synapse_channel/feature.py" \
--artifact coverage.xml \
--note "ready for release"
Setup and terminal integration¶
These commands wire a machine and its terminals into the bus once, so agents
arm themselves and coding tools claim work without per-session setup. init
prints or installs the local user services; install-shell-hook adds an
auto-arming block to your shell rc (idempotent — re-running it replaces the
guarded block rather than appending); shell-hook prints that block for manual
sourcing; arm keeps a single waiter re-armed; and adapters wires detected
coding tools to the claim-aware hooks.
synapse init --project my-repo # print the hub/waiter/presence user units
synapse init --project my-repo --install-user-services # install them under systemd --user
synapse arm install --identity my-repo/agent --start # install/start only one permanent waiter (Linux)
synapse install-shell-hook # auto-arm Bash, Zsh, and Fish on new terminals
synapse install-shell-hook --shell fish # only the Fish integration
synapse shell-hook # print the block instead of installing it
Shell auto-identity: a default-shape `*/terminal-<pid>` identity is re-minted
when `/proc` is missing or the pid is outside this session's lineage (fail-closed
against DEL-INT-C env layering). Manual seats and `SYN_TMUX_PROVIDER=1` stay
unchanged. macOS and containers without `/proc` therefore get a fresh terminal
id each arm rather than keeping a foreign inherited auto name.
synapse completions bash > ~/.local/share/bash-completion/completions/synapse # tab completion
synapse completions fish > ~/.config/fish/completions/synapse.fish # (re-run after upgrades)
synapse arm --name my-repo --for "my-repo,my-repo/*" # keep a waiter armed, re-arming after each wake
synapse arm --name my-repo --owner-pid $$ # leash the waiter: disarm when this shell exits
synapse worker-session --identity my-repo -- codex # run a provider CLI with SYN_PROJECT/SYN_IDENTITY set
synapse adapters list # detect coding tools and report adapter status
synapse adapters install --project my-repo # write the claim-aware adapter into each tool
arm install requires an explicit identity and never silently uses ambient
SYN_IDENTITY for a background service. Add --uri URI for a remote hub and
--token-file PATH for a secured hub; raw --token and ambient
SYNAPSE_TOKEN are refused so a secret cannot be embedded in the unit. Without
--start, the command writes the template and prints exact systemctl follow-up
commands. It supports Linux systemd user services; native Windows installation
is not claimed, so use WSL with systemd enabled.
Generated shell wrappers, git hooks, and systemd units bind long-option values
with = so an identity such as --help remains data, not a new option. Copyable
operator commands additionally shell-quote terminal-safe values and place --
before untrusted positional arguments. shell-hook --provider accepts only a
bare command name; shell syntax, paths, and whitespace are refused before any
startup code is printed.
Governance and integrity¶
Governance commands are advisory by default — they report, they do not block —
so a rollout can observe before it enforces. identity audit inventories
declared identities for enforcement-rollout blockers; acl shadow evaluates
candidate accesses deny-by-default without denying anything live; policy-check
scores a release receipt against a policy (--enforce to gate); and federation
manages operator-confirmed peer-domain bundles for cross-hub trust.
The federation exchange pair moves bundle bytes over the wire while the trust
decision stays out-of-band, the SSH-known-hosts ceremony: the offering operator
authors their domain's bundle material, serves it with
synapse hub --federation-offer FILE, and reads the fingerprint block
(synapse federation offer FILE); the fetching operator pulls it
(synapse federation fetch), sees the identical block, compares the bundle
fingerprint over an independent channel, and only then imports explicitly. A
fetch never imports, and the whole-bundle fingerprint changes when any policy
content is altered in path — namespaces and scope grants as much as keys and
pins.
Trust material is a lifecycle, not a one-off ceremony: federation list
shows each peering's age since its confirmed import and renders a peering
whose bundle expiry has passed as [expired]; with --max-age DAYS an
active peering imported longer ago than the threshold is flagged
[stale: …] and the command exits 1, so a scheduled job can hold the
fleet to a re-ceremony cadence. The same policy applies at the cheapest
moment with federation import --max-age DAYS, which warns (the import
still succeeds — the operator is confirming it explicitly) when the
incoming bundle never expires or expires further out than the threshold.
federation rotate keeps a domain's own bundle fresh on the other side of
that lifecycle. It pushes the expiry to --lifetime-days out, unions any
--add-signing-key/--add-pin material with the existing sets — the grace
window: an old key stays valid until a later --retire-signing-key/--retire-pin
drops it, so a peer that has not re-fetched keeps verifying — and rewrites the
bundle in place, saving the prior one as <bundle>.prev (or --backup PATH).
Retiring material the bundle does not hold, or a non-positive lifetime, is refused
before anything is written. The rotation changes the fingerprint, so it is followed
by the same out-of-band ceremony before peers re-import. It mints no keys of its
own: the --add-* values are ids the domain generated and enrolled through its
existing signing-key and certificate tooling.
federation relay performs a governed operator action on a peer hub over the
same federation transport, rather than managing peerings. The first action is
release — an operator whose domain is granted the release verb in a namespace
the peer owns force-releases a stuck lease held on that peer, without a shell on
it. The peer authorises the relay deny-by-default (mutual TLS + the peering's
scope + it must own the namespace) and audits it with the verified peer, the
asserting operator, and the previous holder, so a lease revoked across hubs stays
attributable. --local-id must match a serving grant on the peer, --operator
records who asked (default: the OS user), and the exit code is 0 when the peer
applied the action, 1 when it refused it or there was nothing to release, 2
when the relay never reached the peer — the fail-closed case — and 3 when the
peer recorded the relay pending a second operator's approval (see two-person
approval below). Only registered actions relay: an unknown action is refused,
never smuggled through the wire.
For a retryable release, pass --idem-key KEY. The key is scoped by the verified
federation principal, resolved sender, and request type; an origin hub preserves it when
forwarding, and the owner binds it to canonical request content. The owner commits the
release, inbound relay provenance, exact verdict, and evidence intent in one transaction.
An identical retry re-runs the live certificate, scope, and namespace-ownership gates
before replaying that exact verdict; a different principal or changed content cannot reuse
the stored operation. This applies to a single-person release and to the second request
that completes a two-person quorum. The first/pending approval and its audit publish
together, but pending state is deliberately in-memory and restart-cleared, so a pending
verdict is not exact-replayed. Give each operator request its own retained key. The
forwarding hub's outbound audit is also outside the owner's local transaction.
--reason records why the action was relayed, in the audit on both hubs; --break-glass
tags it a distinct emergency override. A hub started with reason-required receipts refuses
a relay that carries no reason, so a team or production deployment can hold every governed
cross-hub action to an auditable why.
A hub can also require two-person approval: an authorised relay is not applied on its
own but recorded pending, and carried out only when a second, distinct verified federation
principal relays the same action (same namespace and task). Distinctness is bound to the
federation trust domain after live mutual-TLS authentication — the descriptive --operator label,
sender name, configured signing-key id, and certificate are not separate identities. Aliases and
key or certificate rotation within one domain cannot manufacture quorum. The first relay returns
exit 3; another label backed by the
same verified principal stays pending; a distinct principal completes the quorum and the peer
applies it. Both opaque principal fingerprints and human-readable labels are audited. The
policy is a hub setting (require_two_person_relay), off by default; break-glass does not bypass
it — an emergency still needs a second verified principal.
With --idem-key, the pending approval ledger and its audit event publish through the
same serialized candidate boundary, so an audit failure cannot leave an invisible live
approval and an in-flight keyed write cannot overwrite an unkeyed approval. The request
that completes quorum atomically removes the pending approval, releases the lease, commits
the final audit row and exact response, and publishes both live projections before
post-commit cancellation propagates. The earlier pending audit remains its own durable row.
The relay can also go through the operator's own hub instead of straight to the
peer: point --peer at your local hub, and if that hub is configured with a relay
route to the namespace's owner (a hub started with a relay-peer map, the operator-relay
counterpart of the claim-forwarding routes), it forwards the action to the owner on
your behalf and relays the verdict back — so the operator never needs the owning hub's
credentials, exactly as a claim routes through the claimant's hub. The originating hub
records an outbound audit event naming the requester and the destination owner, and
the owning hub records the inbound one when it applies the action, so a force-release
routed across hubs is attributable on both ends. A relay for a namespace the local
hub neither owns nor has a route to is refused fail-closed, never silently dropped.
synapse identity audit --identities ./identities.json # audit declared identities for blockers
synapse identity audit --identities ./identities.json --json
synapse acl shadow --policy ./acl.json --requests ./requests.json # non-blocking deny-by-default evaluation
synapse policy-check --policy ./policy.json --receipt-json ./receipt.json # advisory; --enforce to gate
synapse federation offer ./my-domain.json # validate own material; print fingerprints
synapse hub --port 8876 --federation-offer ./my-domain.json # serve it to peer operators (token-gated)
synapse federation rotate ./my-domain.json --lifetime-days 90 --add-signing-key ed25519:new # fresh expiry + a new key kept alongside the old for a grace window; backs up the prior bundle
synapse federation rotate ./my-domain.json --retire-signing-key ed25519:old # after the grace window, drop the superseded key
synapse federation fetch ws://peer-hub:8876 --out ./peer-domain.json # pull + fingerprints; NEVER imports
synapse federation fetch wss://peer-hub:8876 --pin sha256:<hex> --out ./peer-domain.json
synapse federation import ./peer-domain.json --confirmed-by ceo --source ws://peer-hub:8876 # after comparing
synapse federation list --store ./federation.json # imported peer domains, provenance, and age
synapse federation list --store ./federation.json --max-age 90 # flag active peerings imported >90 days ago; exit 1
synapse federation revoke example.org --store ./federation.json
synapse federation relay release --peer ws://peer-hub:8876 --namespace TEAM-X --task build-7 # force-release a stuck lease on a peer hub
synapse federation relay release --peer wss://peer-hub:8876 --pin sha256:HEX \
--client-certfile client.pem --client-keyfile client.key \
--namespace TEAM-X --task build-7
synapse federation relay release --peer ws://peer-hub:8876 --namespace TEAM-X --task build-7 --reason "wedged by a crashed agent" --break-glass # with an auditable reason, tagged break-glass
synapse federation relay release --peer ws://peer-hub:8876 --namespace TEAM-X --task build-7 --reason "wedged" --idem-key incident-2026-07-29-build-7 # safe single-person retry identity
synapse encrypt-key generate ./synapse.key # write a fresh owner-only 32-byte key file
synapse encrypt-key generate --from-passphrase ./synapse.key # derive the key from a prompted passphrase (scrypt) instead of random bytes
synapse encrypt-key generate --from-passphrase --scrypt-n 65536 ./synapse.key # tune the scrypt cost (n a power of two; also --scrypt-r/--scrypt-p)
synapse encrypt-key generate-wrapped ./synapse.wrapped.key # envelope-encrypted key whose passphrase can be rotated later
synapse encrypt-key rewrap ./synapse.wrapped.key # rotate that passphrase without re-encrypting any data
synapse encrypt-key generate-wrapped-pkcs11 --token-label synapse ./synapse.hsm.key # wrap the key on a PKCS#11 token (YubiKey/HSM)
synapse encrypt-key generate-wrapped-tpm2 ./synapse.tpm.key # wrap the key with a TPM 2.0 device (RSA-OAEP)
synapse encrypt-key check ./synapse.key # verify its ownership, mode, and length
--from-passphrase derives the 32-byte key from a passphrase (prompted twice)
with scrypt, whose cost is tunable via --scrypt-n (a power of two), --scrypt-r,
and --scrypt-p for a security/performance trade-off. A fresh random salt is used
per derivation and discarded — the written file is a normal key of record that
must be protected exactly like a random one, and the passphrase alone cannot
reconstruct it. Prefer the default random key unless a passphrase source is
specifically wanted.
generate-wrapped writes a different kind of key file — envelope encryption: a
random data key does the bulk AES-GCM, and a key-encryption key derived from the
prompted passphrase wraps it with RFC 3394 AES-KW. The salt is kept, so rewrap
can rotate the passphrase (unwrap with the old, wrap with the new) without
re-encrypting any data — the data key underneath is unchanged, so ciphertext
sealed before the rotation still decrypts. This is the model a hardware key store
(TPM, YubiKey, cloud HSM) plugs into: only the key-encryption key moves into
hardware, the wrapped-file format and the data key stay the same. Both commands
take the same --scrypt-* cost flags.
generate-wrapped-pkcs11 is that hardware step. It wraps the random data key with a
key-encryption key held on a PKCS#11 token — a YubiKey PIV, a cloud or network
HSM, or SoftHSM for testing — via RFC 3394 AES key wrap on the device, so the token
key never leaves the hardware. Point it at the token with --pkcs11-module (or the
PKCS11_MODULE environment variable) and --token-label; the key-encryption key is
generated on the token on first use (or --no-create-kek to require a pre-provisioned
one), and the PIN comes from PKCS11_PIN or an interactive prompt. Needs the optional
python-pkcs11 dependency (pip install synapse-channel[pkcs11]). The written file
records only the token and key labels — never the PIN or the module path — so loading
it at startup re-opens the token to unwrap the data key.
generate-wrapped-tpm2 is the same hardware step rooted in a TPM 2.0 device. A
decrypt-only RSA-2048 key-encryption key is derived from the TPM's storage seed and a
fixed template — the identical key every time, so nothing needs to be persisted as a
handle — and wraps the random data key with RSA-OAEP; the RSA private key is generated
inside the TPM and never leaves it. Point it at the device with --tcti (or the
TPM2_TCTI environment variable), defaulting to the in-kernel resource manager
device:/dev/tpmrm0; a software TPM such as swtpm (swtpm:host=127.0.0.1,port=2321)
serves for testing. Needs the optional tpm2-pytss dependency (pip install
synapse-channel[tpm2]). The written file records only the template version — no device
path — so loading it re-derives the same key inside the TPM to unwrap the data key.
Clearing the TPM hierarchy destroys the key-encryption key and, with it, access to the
data key, which is the intended way to make the store unrecoverable.
Experimental surfaces¶
Newer, advisory surfaces whose shape may still change before 1.0. sandbox
validates a capability manifest and pre-flights or runs a .wasm tool against
it (running needs the wasm extra); workflow validates a declarative workflow
and compiles it into the blackboard tasks the board would execute —
step-level requires predicates (receipt, tests, policy, approval,
sandbox_run, mailbox, dead_letters, or claim) hold a task until a
--evidence snapshot proves the required values, so a workflow can route on
attested evidence instead of status alone;
workflow contention FILE DB additionally joins the compiled task ids to the
durable log, running the same offline yield-advice analysis as
synapse causality contention but keeping only the overlapping live-claim
pairs a workflow task is party to (whether it keeps or yields); pairs outside
the workflow are counted in a trailing note instead of shown, the exit code
signals scoped collisions only (0 none, 1 at least one, 2 on an invalid
workflow, missing store, or the --max-nodes ceiling); participant
is the operator surface over the Participant Fabric — list probes each
registered provider driver (claude, codex, gemini, grok, kimi, ollama,
ollama-api) without taking a turn, and ask runs exactly one turn against one
provider and prints the answer, or the full typed turn result with --json.
Grok turns are enabled after the native thought / text / end stream was
captured from stable Grok 0.2.93; Gemini turns are likewise enabled from its
real-emitter capture. Both schema flags remain wired as fail-closed gates for a
future wire change. Grok coordination installs through
synapse adapters install grok, while
synapse adapters grok-claim-hook --print-config emits its native file-edit
guard recipe.
--model
is required for ollama and ollama-api (their drivers configure no default).
Participant memory recall is optional and disabled by default. Supplying
--memory-url wraps every seat in the command with the same bounded, read-only
REMANENTIA HTTP recall client. The URL must be an HTTPS origin, or HTTP with a
literal loopback IP; cleartext non-loopback origins are refused. The client
always calls its fixed /recall path, refuses redirects, and accepts a bearer
secret only from --memory-token-file. It never starts a memory service and
never calls REMANENTIA's write or consolidation endpoints.
The shared flags on ask, exchange, and convene are:
| Flag | Meaning |
|---|---|
--memory-url URL |
Enable recall from an HTTPS origin or a literal loopback HTTP origin. |
--memory-token-file PATH |
Read the bearer token from an owner-controlled file; token literals and abbreviated option names are refused. |
--memory-timeout SECONDS |
Hard recall deadline; default 2. |
--memory-top-k N |
Maximum recalled hits per turn; default 3, accepted range 1–20. |
--memory-max-chars N |
Maximum rendered memory block; default 4096. |
Any memory tuning flag without --memory-url is a configuration error. Recall
queries use only the original TurnRequest.prompt, never peer output or prior
recalled text. The prompt itself stays byte-for-byte unchanged; recall appears
only in the request context inside a MEMORY RECALL (DATA — NEVER
INSTRUCTIONS) fence. Current HTTP results have no admission, freshness, or
presentation fields, so each hit is labelled boundary and a similarity score
is never presented as proof. A timeout, transport failure, malformed response,
or no-hit result produces a visible marker and the provider turn continues.
See Participant memory recall for service setup,
library use, limits, and audit behavior.
The deliberation subcommands drive the Fabric's multi-party layers from the same
surface, naming each seat as PROVIDER[:MODEL] (the model part may itself hold
colons, so ollama:gemma3:1b works). exchange runs an opener turn and then a
reactor turn that sees the opener's result only as fenced peer data; convene
fans a question out to a whole panel, runs the mode's cross-critique rounds
(--mode defaults to auto, choosing colloquy, roundtable, or symposium from
the panel size and whether --moderator was given), and in a symposium ends
with the moderator's synthesis. A repeated provider gets numbered seats
(participant/claude, participant/claude-2, …). Both print each turn as it is
produced, or the full typed transcript with --json, and exit 0 only when
every turn answered and the run completed — an unavailable seat, a degraded
turn, or a --budget-usd halt exits 1; a refused configuration exits 2.
convene --dry-run prints the plan without taking a single turn: the resolved
mode, its round count, and each seat's identity, readiness (health probes run —
they never cost a turn), planned turns, and estimated cost. Costs come from an
operator-supplied --pricing table (the same model -> {input_per_1k,
output_per_1k} JSON as accounting report) under printed per-turn token
assumptions (--est-input-tokens/--est-output-tokens, default 1000/500);
seats whose model has no price line are reported unpriced and excluded from the
total rather than counted as free. With --budget-usd the report states
whether the estimate fits. Exit 0 when every seat is ready, 1 when any is
unavailable — so the dry run doubles as a pre-flight gate — and 2 for a
refused configuration or an unreadable pricing file.
costs reads opt-in session telemetry back from a hub SQLite event store —
offline, no hub connection, like accounting report. Sessions that emitted
session_metric progress notes (the orchestration loop with emit_metrics, or
any caller of the emit helper) appear as their latest cumulative snapshot per
(agent, session): turns, errors, abstentions, token pressure, metered spend,
mean latency, and the highest rate-limit utilisation seen, plus fleet totals.
When the emitting session names the coordination task it was advancing (a claim
or board task_id), that id rides each snapshot too, so a row can be read
against the work it was doing, not only its session.
Where accounting report answers what models cost, participant costs answers
how participant sessions are going and what they spent; both are descriptive
evidence, never an enforcement gate. Exits 0 for any produced report (even an
empty one) and 2 when the store is missing.
synapse sandbox validate ./manifest.json # validate a capability manifest
synapse sandbox test ./tool.wasm --manifest ./manifest.json # pre-flight without running
synapse sandbox run ./tool.wasm --manifest ./manifest.json --input ./in.json
synapse sandbox run ./tool.wasm --manifest ./manifest.json --approve --attest ~/synapse/audit.db # run + record an audit attestation
synapse workflow validate ./workflow.json # parse and validate
synapse workflow compile ./workflow.json --json # compile into blackboard tasks
synapse workflow plan ./workflow.json --evidence ./evidence.json # hold proof-carrying steps
synapse workflow contention ./workflow.json ~/synapse/hub.db # yield advice scoped to this workflow
synapse participant list # readiness of every provider driver
synapse participant ask ollama "summarise this diff" --model llama3 # one turn, print the answer
synapse participant ask claude "review src/foo.py" --context "be terse" --json
synapse participant ask claude "review src/foo.py" \
--memory-url http://127.0.0.1:8001 \
--memory-token-file /run/secrets/remanentia
synapse participant exchange "is this design sound?" claude codex # opener + reviewing reactor
synapse participant exchange "is this design sound?" claude codex \
--memory-url http://127.0.0.1:8001 \
--memory-token-file /run/secrets/remanentia
synapse participant convene "how should we cache this?" claude codex ollama:gemma3:1b \
--mode symposium --moderator claude --budget-usd 2.50 \
--memory-url http://127.0.0.1:8001 \
--memory-token-file /run/secrets/remanentia # panel + moderated synthesis
synapse participant convene "…" claude codex --dry-run --pricing ./prices.json \
--budget-usd 2.50 # plan + cost estimate, no turns taken
synapse participant costs ~/synapse/hub.db # per-session spend and telemetry + totals
synapse participant costs ~/synapse/hub.db --json # the same report, machine-readable
Headless provider prompts are an execution trust boundary, not shell text. SYNAPSE passes them as one argv value without a shell. Claude additionally runs with built-in tools disabled, an empty strict MCP configuration, safe mode, disabled skills/browser integration, and plan permissions; Codex uses its read-only sandbox; Gemini, Grok, and Kimi use their native non-mutating planning or approval modes. An older CLI that cannot enforce the pinned flags fails the turn instead of silently weakening it. Model output and peer contributions remain untrusted data and are fenced before another participant receives them. The guarded human-readable surfaces include Participant Fabric answers and health details, hub and peer snapshots, mailbox identities, routing and resource fields, capability-directory and outbound-MCP results, workflow-contention, event-query, memory-recall, and accounting reports, relay verdicts, acknowledgement errors, chat, and channel text; each renders terminal control characters as visible escape notation. Machine-readable JSON modes keep their stable JSON contract. Operator hints for mailbox replay, dead-letter draining, lease release, federation exchange, SQLCipher restart, redeploy, scaffolding, and claim commands apply the same shell and option boundaries before they are shown for copying.
For a secured hub, pass --token SECRET to worker, send, listen, board,
manifest, release, a2a-card, a2a-serve, and task.
Run any command with --help for its full set of options.