Skip to content

Python API reference

Everything public lives on the top-level synapse_channel package surface (70 exported names). This page starts with the handful you actually call, then the full generated reference follows. For the compatibility promise on every symbol here, see API and wire stability.

Two entry points

Almost every integration uses one of two classes:

  • SynapseAgent — the client. Connect an agent to a running hub, then issue coordination verbs (claim, release, task updates, messaging, checkpoints). This is what most callers reach for.
  • SynapseHub — the hub itself. Run the authoritative coordination process, usually from the synapse hub CLI, but embeddable in-process for tests or bundled deployments. Configure it with HubConfig.
from synapse_channel import SynapseAgent, SynapseHub, HubConfig

The client in one flow

A minimal SynapseAgent session — connect, wait for the hub's welcome, claim a file scope, update the task, and release — is the whole daily loop in code:

import asyncio

from synapse_channel import SynapseAgent


async def main() -> None:
    agent = SynapseAgent("ALPHA", uri="ws://localhost:8876")
    session = asyncio.create_task(agent.connect())  # one long-lived session

    # Wait for the hub's welcome before issuing verbs; fail loudly if it is down.
    if not await agent.wait_until_ready():
        raise RuntimeError("could not reach the hub — is `synapse hub` running?")

    # The hub refuses a request that overlaps another live file-scope claim.
    await agent.claim("refactor-parser", note="splitting the tokenizer", paths=["src/parser"])
    await agent.save_checkpoint("refactor-parser", "step=2")
    await agent.update_task("refactor-parser", status="working")
    await agent.release("refactor-parser")

    agent.running = False
    session.cancel()


asyncio.run(main())

Pass on_message_callback= to SynapseAgent(...) to react to inbound frames (chat, task events, release grants). The full worked example — with an event callback that waits on checkpoint and release confirmations — is in the quick start.

The verbs you will use most

Grouped by what they coordinate (all are async methods on SynapseAgent):

  • Work claimsclaim(task_id, paths=..., note=...) and release(task_id): file-scope mutual exclusion, the one thing that gates a mutation.
  • Task lifecycleupdate_task(task_id, status=...) drives the typed task state on the shared blackboard.
  • Checkpointssave_checkpoint(task_id, data) records resumable progress that survives a restart.
  • Messaging — send to everyone, a named group (A,B), or one agent; an idle agent catches up from its durable inbox on reconnect.

For the exact signatures of every method, read the generated reference below.

Embedding a hub

To run the hub in-process (tests, a bundled tool), construct SynapseHub from a HubConfig. HubConfig().to_kwargs() maps one-to-one onto the SynapseHub constructor — a contract the test suite enforces — so config built one way is always accepted by the hub.

from synapse_channel import SynapseHub, HubConfig

hub = SynapseHub(**HubConfig().to_kwargs())

Supporting surfaces

The remaining exports fall into a few families you reach for as needed:

  • Model workersSynapseLLMWorker, OpenAIChatClient, TieredChatClient, and the offline RuleBasedClient let agents reply on-channel through any OpenAI-compatible endpoint with a deterministic fallback.
  • Team helpersplan_team(...) / run_team(...) script a small fleet.
  • Coordination primitivesBlackboard, EventStore, TaskClaim, TaskStatus, MessageType, and the *Config types.
  • Pure predicatespaths_overlap, scopes_conflict, would_create_cycle, is_directed, is_recipient, and friends: no I/O, safe to call anywhere.

Everything is re-exported from the package root, so from synapse_channel import X works for any name below.

Full generated reference

The reference below is generated from the source docstrings for every public symbol.

synapse_channel

SYNAPSE CHANNEL — local-first multi-agent coordination bus.

A small WebSocket fabric that lets several agents share presence, claim and release units of work, chat, and advertise resources through one authoritative hub. The pieces compose: :class:~synapse_channel.core.hub.SynapseHub routes, :class:~synapse_channel.client.agent.SynapseAgent connects, and :class:~synapse_channel.client.llm_worker.SynapseLLMWorker answers on-channel through a pluggable :mod:~synapse_channel.client.chat_backends backend. The synapse console command (see :mod:synapse_channel.cli) drives all of it.

The public names below resolve lazily (:pep:562): the submodule behind a name is imported on first attribute access, so import synapse_channel stays cheap for consumers that touch only a slice of the surface — the CLI in particular no longer pays for the whole facade at start-up.

DEFAULT_HUB_URI = 'ws://localhost:8876' module-attribute

Default hub URI; matches the hub's default bind port.

HUB_URI_ENV_VAR = 'SYNAPSE_URI' module-attribute

Environment variable that overrides the default hub URI for the CLI.

MEMORY_KINDS = frozenset({EventKind.RECALL, EventKind.FINDING, EventKind.CHECKPOINT, EventKind.HANDOFF}) module-attribute

The durable event kinds the persistent-memory read-side ingests.

The query-stream (recall), the authored atoms (finding), and the highest-signal episodic state (checkpoint/handoff) — the subset of the log a downstream memory adapter reads through the seq-cursored ingest seam. The pure coordination kinds (claim/release/task_update/resource/the ledger kinds) are excluded; chat is filtered read-side, not here.

PRIORITY_SENDERS = frozenset({'CEO'}) module-attribute

Senders whose message wakes a directed-only waiter even on a broadcast.

The CEO command session directs the fleet; a broadcast from it is never merely routine peer chatter, so it must reach a quiet waiter promptly.

SynapseAgent

Bases: AgentLifecycleMixin, AgentDispatchMixin, AgentOutboundMixin, AgentQueryMixin

An async client that maintains one connection to the Synapse hub.

Parameters:

Name Type Description Default
name str

Unique agent name presented to the hub.

required
on_message_callback MessageCallback or None

Preferred: an async def called with every decoded inbound message. A synchronous callable that returns None is accepted for compatibility (only awaitables are awaited). Self-originated chat echoes are filtered out before the callback runs.

None
uri str

Hub WebSocket URI. Defaults to :data:DEFAULT_HUB_URI.

DEFAULT_HUB_URI
heartbeat_interval float

Seconds between keepalive heartbeats, clamped up to :data:MINIMUM_HEARTBEAT_INTERVAL. Defaults to 20.0.

20.0
verbose bool

When True, connection lifecycle notes are printed. Defaults to True.

True
token str or None

Shared-secret token presented on the registration message when the hub requires authentication. None sends no token (the default for an open, loopback hub).

None
takeover bool

When True, the registration asks the hub to evict a stale holder of name instead of failing with a name conflict. Defaults to False.

False
roles tuple of str

Full <project>/<role> names this identity also answers to, declared on the registration heartbeat so the hub binds them — a directed message to a role then reaches this agent and the role shows in /who. Empty by default.

()
mailbox bool

When True, the registration heartbeat declares mailbox: true and the agent's since_seq cursor, so a mailbox-capable hub replays the directed messages missed while offline; the agent then advances its cursor on every chat frame admitted by its acceptance gate and acknowledges live and replayed frames. The hub uses that receiver watermark for pending counts and can also confirm a deferred delivery receipt to the original sender. This does not acknowledge model processing. Defaults to False — an ordinary agent neither asks for a replay nor acks.

False
mailbox_since_seq int

The durable journal seq the agent has already processed, used to seed the cursor so a caller that persists it across reconnects resumes from where it left off rather than replaying the whole retained window. Floored at 0 (the whole window). Defaults to 0.

0
mailbox_for str

The identity whose backlog to replay, when it differs from name. A wake-listener connects under a receive-only name (an -rx suffix) but waits on its bare identity, so it sets this to that identity and the hub filters the replay by it rather than by the connection name. Empty (the default) leaves the hub replaying the backlog for name itself — correct for an agent that connects under its own identity.

''
mailbox_advance Callable or None

Gate consulted before the mailbox cursor advances past a chat frame (and before a live or replayed frame is acknowledged). A waiter that surfaces only a FILTERED subset of frames passes its wake filter here, so a frame it will never show cannot be silently consumed: an unadvanced cursor leaves the frame pending and a later (or correctly bound) waiter still receives it on replay. None (the default) advances on every chat frame — correct for an ordinary client whose callback processes everything it receives.

None
wake_capability str

Receiver capability declared on the registration heartbeat. Ordinary agents default to direct; passive wait sockets and pane bridges override it.

WAKE_DIRECT
request_lease bool

When True, the registration heartbeat declares lease: true, asking the hub for an ownership lease on the bound name: the hub then admits a later claim on that name only when it presents the granted token, so a reconnect re-takes its own name and a stranger cannot squat it in the gap. Off by default — a client that does not opt in keeps classic first-come name semantics, and a pre-lease hub ignores the field entirely.

False
owner_lease str

The lease token to present for the bound name, persisted from an earlier grant (see :mod:synapse_channel.owner_lease). Empty (the default) presents nothing, which is correct for a first claim. Updated in place when the hub grants a fresh lease.

''
on_lease_granted Callable[[str], None] or None

Called with the token the moment the hub grants a lease, so the caller can persist it before the process exits. None (the default) only records the token on :attr:owner_lease.

None
machine_identity bool

Present the zero-config trust-on-first-use machine key when no explicit identity_key_path is given (the default). Resolution is best-effort: a core-only install or an unreadable key degrades to an unsigned connection with the module's stated one-time warning. Pass False for a deliberately unsigned agent — a hub enforcing an identity pin for the name will then refuse the connection, by design. An explicit identity_key_path always wins over the machine default.

True
per_message_auth_key_id str or None

Key id used to sign mutating frames with per-message authentication. None leaves frame signing off.

None
per_message_auth_secret str or bytes or None

HMAC secret paired with per_message_auth_key_id. Both fields must be set to sign frames.

None
capability_card_key_path str or None

Owner-only Ed25519 PEM used only to sign capability advertisements.

None
capability_card_key_id str

Public id of capability_card_key_path in the hub's separate card trust bundle. A path and id must be supplied together.

''
capability_card_project str

Optional assertion of the namespace prefix in name. Signed live cards require a namespaced agent, and this value must match that hub-resolved prefix.

''
capability_card_lifetime_seconds float

Lifetime recorded in each signed advertisement.

DEFAULT_CAPABILITY_CARD_LIFETIME_SECONDS
ping_interval float

Seconds between client keepalive pings, so a half-open connection — a hub that was killed, an ungraceful restart, or an eviction whose close frame never arrived — is detected and :meth:connect returns instead of blocking forever. Without this a waiter can linger for days holding a dead socket. Defaults to 20.0.

20.0
ping_timeout float

Seconds to wait for a ping reply before dropping the connection. Defaults to 20.0.

20.0

mailbox_cursor property

Return the highest durable journal seq this agent has processed.

Starts at the seeded mailbox_since_seq and advances as the agent sees chat frames, so a caller that persists it across reconnects — a waiter re-armed as a fresh process — can seed the next agent's mailbox_since_seq and resume the backlog from where this one stopped rather than from zero.

ChatBackend

Bases: Protocol

Structural type for anything that generates a reply from two prompts.

generate(*, system_prompt, user_prompt)

Return a reply for the given system and user prompts.

OpenAIChatClient

Backend for any OpenAI-compatible /v1/chat/completions endpoint.

Parameters:

Name Type Description Default
api_key str

Bearer token. Local servers (e.g. Ollama) accept any non-empty value.

required
model str

Model identifier passed in the request body.

required
base_url str

Base URL of the OpenAI-compatible API; a trailing slash is stripped. Only http/https schemes are accepted — a file:// or custom scheme smuggled in through configuration is refused at construction rather than silently opened.

required
timeout_seconds float

Per-request timeout, clamped up to 3.0 seconds.

required

Raises:

Type Description
ValueError

If base_url carries a scheme other than http or https.

generate(*, system_prompt, user_prompt)

Request a completion and return the sanitised reply text.

Parameters:

Name Type Description Default
system_prompt str

System role content steering the model.

required
user_prompt str

User role content the model responds to.

required

Returns:

Type Description
str

The assistant message content, whitespace-collapsed and truncated.

Raises:

Type Description
RuntimeError

On an HTTP error status, a connection failure, or a response whose shape does not contain the expected completion content.

RuleBasedClient

Deterministic offline backend that acknowledges receipt.

The reply carries no sender prefix: the wire envelope already records the author, so the hub and every reader render the name once.

generate(*, system_prompt, user_prompt)

Return a fixed acknowledgement, ignoring both prompts.

Parameters:

Name Type Description Default
system_prompt str

Unused; present to satisfy :class:ChatBackend.

required
user_prompt str

Unused; present to satisfy :class:ChatBackend.

required

Returns:

Type Description
str

A constant on-channel acknowledgement.

SynapseLLMWorker

A hub agent that answers addressed messages via a chat backend.

Parameters:

Name Type Description Default
name str

Agent name presented on the channel.

required
uri str

Hub URI. Defaults to :data:~synapse_channel.client.agent.DEFAULT_HUB_URI.

DEFAULT_HUB_URI
provider str

Backend provider: ollama (default), openai, or rule.

'ollama'
model str

Model identifier for HTTP providers. Defaults to "llama3".

'llama3'
base_url str

OpenAI-compatible base URL. Defaults to the local Ollama endpoint.

DEFAULT_OLLAMA_BASE_URL
api_key_env str

Environment variable holding the API key. Defaults to "OPENAI_API_KEY".

'OPENAI_API_KEY'
max_context int

Number of recent messages retained for prompt context (floored at 2).

8
reply_target_mode str

"all" to answer the room or "sender" to answer privately.

'all'
min_reply_interval float

Minimum seconds between replies (floored at 0). Defaults to 0.7.

0.7
ready_timeout float

Seconds to wait for the hub handshake in :meth:run. Defaults to 5.0.

5.0
token str or None

Shared-secret token presented to a hub that requires authentication; None for an open hub.

None
task_classes tuple[str, ...] or list[str]

Routing classes this worker advertises on its capability card; defaults to ("chat",).

('chat',)
heavy_model str

Model used for the heavy tier when provider="tiered"; defaults to model when empty.

''
capability_card_key_path str

Separate Ed25519 card-signing credential and project binding. All are opt-in; unsigned advisory discovery remains the default.

None
capability_card_key_id str

Separate Ed25519 card-signing credential and project binding. All are opt-in; unsigned advisory discovery remains the default.

None
capability_card_project str

Separate Ed25519 card-signing credential and project binding. All are opt-in; unsigned advisory discovery remains the default.

None
capability_card_lifetime_seconds float

Lifetime recorded in each signed advertisement.

DEFAULT_CAPABILITY_CARD_LIFETIME_SECONDS

on_message(data) async

Filter an inbound message and queue it when a reply is warranted.

Parameters:

Name Type Description Default
data dict[str, Any]

A decoded inbound message envelope.

required

run() async

Connect, wait for the handshake, and run the worker loop.

The connection and worker tasks run concurrently; when either finishes the other is cancelled and any terminal error is reported.

TaskClass

The coarse routing classes a request can fall into.

TieredChatClient

A chat backend that routes :meth:generate to a per-class backend.

Parameters:

Name Type Description Default
backends Mapping[str, ChatBackend]

One backend per task class. The default_class must be present.

required
default_class str

Class used when the classifier picks one with no registered backend. Defaults to :attr:TaskClass.SLM.

SLM
classifier Callable[[str], str]

The prompt classifier; defaults to :func:classify. Injectable for tests.

classify

Raises:

Type Description
ValueError

If default_class has no backend in backends.

route(prompt)

Return the task class :func:classify assigns to prompt.

generate(*, system_prompt, user_prompt)

Classify user_prompt and delegate to the matching backend.

Parameters:

Name Type Description Default
system_prompt str

System prompt forwarded to the chosen backend.

required
user_prompt str

User prompt, both classified and forwarded.

required

Returns:

Type Description
str

The chosen backend's reply.

Intervention dataclass

A single action the supervisor decided to take on a task.

Attributes:

Name Type Description
task_id str

The task the intervention concerns.

action str

What to do; currently always "reoffer".

reason str

Human-readable explanation, recorded with the re-offer.

StallPolicy dataclass

Operator-tunable policy for stall detection.

Parameters:

Name Type Description Default
idle_seconds float

Fixed no-activity ceiling for in-progress tasks. Values below one second are clamped to one second.

DEFAULT_IDLE_SECONDS
predictive bool

Whether completed-task history may lower the effective in-progress threshold. Defaults to True.

True
history_multiplier float

Multiplier applied to the median historical activity gap. Values below one are clamped to one.

DEFAULT_HISTORY_MULTIPLIER
min_history_samples int

Minimum number of historical gaps required before prediction is used. Values below one are clamped to one.

DEFAULT_MIN_HISTORY_SAMPLES
min_predictive_idle_seconds float

Floor for the predictive threshold. Values below one second are clamped to one second.

DEFAULT_MIN_PREDICTIVE_IDLE_SECONDS
history_task_limit int

Number of recent terminal tasks to inspect for cadence. Values below one are clamped to one.

DEFAULT_HISTORY_TASK_LIMIT

__post_init__()

Clamp policy values so the detector has no invalid runtime states.

SupervisorWorker

An on-channel agent that polls the board and re-offers stalled tasks.

Parameters:

Name Type Description Default
name str

Agent name presented on the channel. Defaults to "SUPERVISOR".

'SUPERVISOR'
uri str

Hub URI. Defaults to :data:~synapse_channel.client.agent.DEFAULT_HUB_URI.

DEFAULT_HUB_URI
idle_seconds float

Fixed no-activity ceiling passed to :func:detect_stalls.

DEFAULT_IDLE_SECONDS
predictive_stall bool

Whether completed-task history may lower the effective no-activity threshold.

True
history_multiplier float

Multiplier applied to the historical median activity gap.

DEFAULT_HISTORY_MULTIPLIER
min_history_samples int

Minimum historical gaps required before prediction is used.

DEFAULT_MIN_HISTORY_SAMPLES
min_predictive_idle_seconds float

Floor for the predictive threshold.

DEFAULT_MIN_PREDICTIVE_IDLE_SECONDS
interval float

Seconds between passes (floored at 1).

DEFAULT_INTERVAL_SECONDS
settle_seconds float

Pause after requesting the board to let the snapshot arrive.

DEFAULT_SETTLE_SECONDS
ready_timeout float

Seconds to wait for the hub handshake in :meth:run. Defaults to 5.0.

5.0
token str or None

Shared-secret token for a secured hub.

None
clock Callable[[], float]

Wall-clock source; injectable for deterministic tests.

time

on_message(data) async

Capture the latest board snapshot the hub sends back.

evaluate_and_apply() async

Run the stall policy on the latest board and apply each re-offer.

Returns:

Type Description
list[Intervention]

The interventions applied (empty when no board has arrived yet or nothing is stalled).

run() async

Connect, wait for the handshake, and supervise until the link ends.

TokenAuthenticator

Validates a shared-secret token, optionally bound to agent names.

Parameters:

Name Type Description Default
tokens Mapping[str, Iterable[str]] or Iterable[str]

Either a mapping of token -> permitted agent names (an empty name set permits any agent), or a plain iterable of tokens that each permit any agent. Empty-string tokens are dropped.

required
Notes

An authenticator constructed with no usable tokens denies every connection; pass None to :class:~synapse_channel.core.hub.SynapseHub to leave the hub open instead.

is_empty property

Whether no usable token is configured (so every connection is denied).

authenticate(token, agent)

Check a presented token for a connecting agent.

Parameters:

Name Type Description Default
token str

The secret the agent presented; an empty value is always refused.

required
agent str

The agent name the connection claims, checked against any name binding on the matched token.

required

Returns:

Type Description
tuple[bool, str]

(True, message) when the token is valid and permits the agent, otherwise (False, reason).

authenticate_with_principal(token, agent)

Authenticate and return the stable quota bucket for the credential.

The principal is a domain-separated SHA-256 fingerprint of the matched connect token. It is deliberately not the asserted agent name: every name admitted by one credential shares one claim budget, so reconnecting under aliases cannot multiply the quota. The raw token is never returned or logged. Callers must keep the fingerprint internal; it is persisted only in private claim snapshots so a restart preserves the same budget.

Returns:

Type Description
tuple[bool, str, str or None]

Authentication verdict, human-readable reason, and the stable quota principal on success. Refusals always carry None.

CapabilityCard dataclass

A small, A2A-shaped description an agent advertises about itself.

Attributes:

Name Type Description
agent str

Name of the advertising agent.

description str

Free-form summary of what the agent does.

skills tuple[str, ...]

Capability tags the agent claims (free-form).

task_classes tuple[str, ...]

Routing classes the agent can take (e.g. chat, rule, reason), used to pick a worker for a task.

model str

Optional model identifier backing the agent.

project str

Hub-resolved project namespace bound into a signed card.

manifest_digest str

Optional digest of a package/tool manifest this advertisement describes.

contracts tuple[CapabilityContract, ...]

Declarative input/output contracts keyed by task class.

meta dict[str, Any]

Arbitrary descriptive metadata.

signature dict[str, Any]

Optional Ed25519 card-signature envelope.

verification CapabilityCardVerification

Explicit advisory verification result; never an execution grant.

advertised_at float

Wall-clock time, in seconds, when the card was last refreshed.

as_dict()

Return a JSON-serialisable snapshot of this card.

CapabilityRegistry

One capability card per agent, exposed as a queryable manifest.

The registry is single-threaded and synchronous; the hub owns one instance. Live cards are kept fresh by re-advertising and are dropped on disconnect or when they pass the soft TTL; persistent dispatch registrations (:meth:advertise_persistent) survive disconnects and expire only when not refreshed within persistent_ttl_seconds.

Parameters:

Name Type Description Default
ttl_seconds float

Liveness window after which an un-refreshed card is expired. Defaults to :data:DEFAULT_CARD_TTL_SECONDS.

DEFAULT_CARD_TTL_SECONDS
persistent_ttl_seconds float

Refresh window for persistent dispatch registrations. Defaults to :data:PERSISTENT_CARD_TTL_SECONDS.

PERSISTENT_CARD_TTL_SECONDS

advertise(agent, *, description='', skills=(), task_classes=(), model='', project='', manifest_digest='', contracts=(), meta=None, signature=None, now=None)

Store or refresh an agent's capability card.

Parameters:

Name Type Description Default
agent str

Name of the advertising agent.

required
description str

Free-form summary.

''
skills Iterable[str]

Capability tags; stripped, de-duplicated, blanks dropped.

()
task_classes Iterable[str]

Routing classes; stripped, de-duplicated, blanks dropped.

()
model str

Backing model identifier.

''
project str

Hub-resolved project namespace. Signed cards require a non-empty value.

''
manifest_digest str

Digest of the tool/package manifest this card describes.

''
contracts object

Contract mappings or :class:CapabilityContract objects. Malformed entries are ignored and valid entries are normalised.

()
meta dict[str, Any] or None

Descriptive metadata; None becomes an empty mapping.

None
signature object

Candidate signed-card envelope. Malformed values remain visible through the verification result but are not echoed as a trusted mapping.

None
now float or None

Override for the current wall-clock time, in seconds.

None

Returns:

Type Description
CapabilityCard

The stored card.

advertise_persistent(agent, *, dispatchable=True, description='', skills=(), task_classes=(), model='', project='', manifest_digest='', contracts=(), meta=None, signature=None, now=None)

Register or refresh a persistent dispatch card for agent.

The registration survives disconnects and expires only when it is not refreshed within persistent_ttl_seconds. Fields and verification are identical to :meth:advertise.

Parameters:

Name Type Description Default
agent str

Name of the registering agent (a project-scoped seat identity).

required
dispatchable bool

Whether automated dispatchers may consider this seat. Defaults to True.

True
now float or None

Override for the current wall-clock time, in seconds.

None

Returns:

Type Description
CapabilityCard

The stored card payload.

forget(agent)

Drop an agent's live card, e.g. when it disconnects.

A persistent registration is deliberately kept: it means the seat may be woken for work even while no interactive session is live.

forget_persistent(agent)

Drop an agent's persistent dispatch registration (opt-out).

get(agent)

Return an agent's card, or None when it has none.

get_persistent(agent)

Return an agent's persistent registration, or None.

expire(now=None)

Drop every card not refreshed within the TTL of now.

manifest(now=None)

Return all live cards as dicts, sorted by agent name.

One entry is emitted per agent. A persistent registration adds the additive persistent/dispatchable keys to that agent's entry — merged onto the fresher live card when both exist — so consumers see a single, unambiguous card per agent and legacy consumers simply ignore the new keys.

Parameters:

Name Type Description Default
now float or None

Override for the current wall-clock time used to expire stale cards.

None

Returns:

Type Description
list[dict[str, Any]]

One card mapping per live agent.

for_task_class(task_class, now=None)

Return the agents that advertise a given task class, sorted by name.

Parameters:

Name Type Description Default
task_class str

The routing class to match against each card's task_classes.

required
now float or None

Override for the current wall-clock time used to expire stale cards.

None

Returns:

Type Description
list[str]

Names of live agents that can take the task class.

CapabilityContract dataclass

Declarative contract for one advertised task class.

Parameters:

Name Type Description Default
task_class str

Routing class the contract describes, for example chat or rule.

required
input_schema dict[str, Any]

JSON Schema style mapping describing accepted input.

dict()
output_schema dict[str, Any]

JSON Schema style mapping describing produced output.

dict()
preconditions tuple[str, ...]

Declarative checks that should hold before the task is invoked.

()
postconditions tuple[str, ...]

Declarative checks that should hold after the task completes.

()

as_dict()

Return the canonical manifest representation of the contract.

CompactionResult dataclass

What one :func:compact sweep removed.

Attributes:

Name Type Description
checkpoints_removed int

Number of superseded checkpoint events deleted.

findings_removed int

Number of expired finding events deleted.

floor_seq int

The floor the sweep honoured; no event above it was considered.

corrupt_rows_removed int

Number of explicitly quarantined rows deleted.

total_removed property

Return the total number of events the sweep deleted.

RetentionPolicy dataclass

How much of the durable memory log to keep.

The three knobs are independent and additive — a sweep applies whichever are set. A policy with neither set is a no-op (:attr:is_noop).

Attributes:

Name Type Description
max_checkpoints_per_task int or None

Keep only the latest this-many resume checkpoints per task; older checkpoints below the floor are removed. None keeps every checkpoint. Must be at least 1 when set — dropping the newest checkpoint could lose the only surviving snapshot of a claim and so corrupt replay.

finding_grace_seconds float or None

Remove a finding whose validity window closed (validity.valid_to set) more than this many seconds ago. None keeps every finding; a finding with an open window (valid_to unset) is never aged out, whatever the grace.

drop_corrupt_rows bool

Explicitly delete quarantined rows at or below the settled floor. The CLI requires an archive report for this destructive recovery operation.

is_noop property

Return True when neither retention knob is set, so a sweep does nothing.

__post_init__()

Reject a policy that would corrupt replay or invert time.

Decision dataclass

The outcome of running a finding through the emit gate.

Attributes:

Name Type Description
verdict str

One of :data:ACCEPT, :data:FLOOR, or :data:REJECT.

finding Finding or None

The admitted record — the original on accept, the lowered record on floor, None on reject.

reasons tuple[str, ...]

Why the record was floored or rejected; empty on a clean accept.

ClaimStatus

The epistemic standing of the claim.

EvidenceKind

What kind of evidence backs the assertion.

Finding dataclass

One emit-time memory atom: an assertion placed on the three honesty axes.

Construct from the wire with :meth:from_dict (forward-tolerant) and stamp the hub-attested fields with :meth:attested before journalling. The emit gate decides whether the record is admitted, floored, or rejected.

Attributes:

Name Type Description
statement str

The assertion being remembered.

subkind str

The episodic category; an unknown value is carried opaque.

evidence_kind str or None

What backs the claim; None when no basis is named.

claim_status str or None

The epistemic standing; None when unstated.

freshness str

How recently the reference was re-checked at source.

evidence_ref str or None

A reference to the evidence (file:line, commit, command output).

provenance Provenance or None

Origin coordinates; None only on a malformed/absent record (rejected).

validity Validity or None

Bi-temporal window; None only on a malformed/absent record (rejected).

lifecycle str

Whether the atom is current, superseded, or retracted.

supersedes str or None

Identifier of the atom this one replaces, if any.

verified_at_source SourceCheck

The producer-asserted re-check with hub-attested origin.

producer_confidence float or None

Advisory producer confidence; never gates recall.

execution_substrate str or None

Where the result was produced, when relevant.

entities tuple[str, ...]

Named entities the finding concerns (read-side routing hooks).

tags tuple[str, ...]

Free-form tags (read-side hierarchy hooks).

from_dict(raw) classmethod

Parse a finding from a wire mapping without raising.

Missing optionals become None, malformed fields become their empty form, and an unknown enum member is carried as-is. The freshness axis is derived from the re-check recency and reference signals only when the producer leaves it unset.

Parameters:

Name Type Description Default
raw dict[str, Any]

The decoded finding message body.

required

Returns:

Type Description
Finding

The parsed record, ready for the emit gate.

attested(*, by, at, project_fallback='')

Return a copy with the hub-attested identity and time stamped in.

The producing identity (by) and receive-time (at) come from the connection, not the producer, so they cannot be forged. provenance.actor is overwritten with by; provenance.ts and validity.valid_from are anchored to at when the producer left them unset; provenance.project falls back to project_fallback when blank; and verified_at_source carries the attested by/at.

Parameters:

Name Type Description Default
by str

The hub-attested producing identity.

required
at float

The hub-attested receive-time, in seconds.

required
project_fallback str

Project to record when the producer named none.

''

Returns:

Type Description
Finding

A new record with the attested fields stamped in.

as_dict()

Return a JSON-serialisable snapshot of the whole record.

Freshness

How recently the supporting reference was re-checked at source.

The recency-of-re-check axis, orthogonal to how the claim is known (:class:EvidenceKind): a measured fact can be re-checked this session or left unverified for months. It gates validation — only a source-verified claim renders validated — so a reference that exists but was never re-checked cannot pass for one that was.

Lifecycle

Whether the atom is current, replaced, or withdrawn — orthogonal to status.

Subkind

What a finding is about — its episodic category.

SynapseHub

Routing core that maintains presence, history, and coordination state.

Parameters:

Name Type Description Default
default_ttl_seconds float

Lease TTL passed to the underlying :class:SynapseState. Defaults to 3600.0.

3600.0
hub_id str or None

Stable hub identifier stamped on outgoing system messages. When None a random "syn-XXXXXXXX" id is generated.

None
journal EventStore or None

When given, authoritative mutations are appended to this durable log and the hub's state is rebuilt from it on construction, so a restart resumes live leases and history instead of an empty registry. When None the hub is purely in-memory.

None
rate_limiter RateLimiter or None

When given, non-heartbeat messages from an agent over its limit are refused, so one runaway agent cannot swamp the single hub. None disables rate limiting.

None
host_rate_limiter RateLimiter or None

When given, every inbound frame — heartbeats included — is charged to a bucket keyed by the connection's remote host, so a single host cannot flood the hub by cycling agent names or with bare heartbeats. Independent of and additional to rate_limiter; None disables the per-host ceiling.

None
durable_ingress_quota DurableIngressQuota or None

When given, each accepted chat is charged to the connection's server-derived quota principal (events and serialized chat-frame bytes in a sliding window). Over-quota chats are refused before history or journal growth so one principal cannot fill the durable log; None disables the bound.

None
max_history int

Maximum chat messages retained in memory; the oldest are dropped beyond this bound so history cannot grow without limit. The durable log (when a journal is attached) still records every message. Defaults to :data:DEFAULT_MAX_HISTORY.

DEFAULT_MAX_HISTORY
relay_log str or Path or None

When given, every broadcast message is also mirrored to this newline- delimited log in the compact lite format (see :func:~synapse_channel.core.relay.encode_lite), so a token-budgeted agent can observe the channel by tailing a file instead of holding a socket. None disables the mirror.

None
relay_max_lines int

Upper bound on the relay log: it is trimmed back to its last this-many lines once it grows that far past the bound, so the mirror cannot grow without limit. Defaults to :data:DEFAULT_RELAY_MAX_LINES.

DEFAULT_RELAY_MAX_LINES
max_progress int

Maximum progress notes retained on the shared blackboard; the oldest are dropped beyond this bound. The durable log (when attached) still records every note. Defaults to :data:~synapse_channel.core.ledger.DEFAULT_MAX_PROGRESS.

DEFAULT_MAX_PROGRESS
max_progress_per_author int

Maximum progress notes retained for one author on the shared blackboard. Defaults to :data:~synapse_channel.core.ledger.DEFAULT_MAX_PROGRESS_PER_AUTHOR.

DEFAULT_MAX_PROGRESS_PER_AUTHOR
max_progress_per_task int

Maximum progress notes retained for one task id on the shared blackboard. Defaults to :data:~synapse_channel.core.ledger.DEFAULT_MAX_PROGRESS_PER_TASK.

DEFAULT_MAX_PROGRESS_PER_TASK
board_task_cap int or None

Bound on the tasks served per board snapshot (floored at 1): live tasks are kept ahead of terminal ones, the newest updated_at wins inside each class when trimming, and a capped reply carries total_tasks and truncated so a consumer sees the bound instead of mistaking the page for the whole plan. None (the default) serves the full board unchanged; the cap exists because a long-running fleet's full board eventually outgrows a websocket frame.

None
max_findings_per_agent int

Maximum durable findings one agent may admit before new findings are privately rejected. Defaults to :data:DEFAULT_MAX_FINDINGS_PER_AGENT.

DEFAULT_MAX_FINDINGS_PER_AGENT
compact_hint_threshold int

Record count past which a hub started on a durable log emits a one-off startup hint to run synapse compact (the log is never auto-compacted — pruning is safe only below a consumed read-side cursor). Clamped up to 1; set it very high to silence the hint. Defaults to :data:DEFAULT_COMPACT_HINT_THRESHOLD.

DEFAULT_COMPACT_HINT_THRESHOLD
dead_letter_escalation_threshold int

Escalate a dead-letter blackhole every this-many undelivered directed messages to one target — the hub broadcasts a one-line notice and journals an audit event when the count reaches the threshold and each further multiple, so a growing blackhole becomes an active signal rather than a passive snapshot entry. It never re-delivers a message (the ledger holds no bodies). 0 (the default) disables escalation, leaving the ledger's visibility unchanged, and is the default (DEFAULT_DEAD_LETTER_ESCALATION_THRESHOLD).

DEFAULT_DEAD_LETTER_ESCALATION_THRESHOLD
dead_letter_forwarder DeadLetterForwarder or None

The seam that hands a dead-letter blackhole signal to the peer hub whose domain owns the target, when an escalation fires for a target this hub's namespace-ownership and relay routes resolve to a peer. The origin always journals an audit-only forwarding event (counts and names, never a message body) and transmits the pointer to the owning hub best-effort. Defaults to :func:~synapse_channel.core.dead_letter_forwarding_transport.forward_dead_letter, the websocket transport, so forwarding is wired end-to-end wherever the relay routes it reuses are configured; pass None to record the forwarding intent without transmitting.

forward_dead_letter
authenticator TokenAuthenticator or None

When given, a connecting agent must present a valid shared-secret token on its first message or the hub refuses and closes the socket. None leaves the hub open, which is the right default for a loopback bind.

None
enable_metrics bool

When True the server also answers HTTP GET /metrics (Prometheus text exposition) and GET /health (a JSON liveness document) on the same port as the WebSocket endpoint, for scraping and container probes. Off by default — a plain WebSocket hub serves no HTTP.

False
auth_timeout float

Seconds to wait for a name-binding first frame before closing the socket (code 4012). On a secured hub the first frame must also authenticate and the roster is withheld until then; on an open hub the welcome is still sent on connect, but an idle socket that never registers is reaped so it cannot hold a connection or per-host slot. Defaults to :data:DEFAULT_AUTH_TIMEOUT.

DEFAULT_AUTH_TIMEOUT
max_unauth_clients int or None

On a secured hub, the most sockets allowed in their pre-auth window at once; a further connect is closed with code 4014 so an authentication-stall burst cannot fill the connection table for the whole auth_timeout. None (the default) tracks max_clients, i.e. no extra restriction until an operator sets a tighter value. Ignored on an open hub.

None
max_connections_per_host int or None

Maximum simultaneous sockets admitted from one remote host. This is distinct from the total max_clients ceiling and the frame-rate host_rate_limiter; it counts open sockets, including sockets still in their first-frame window. Defaults to :data:DEFAULT_MAX_CONNECTIONS_PER_HOST. None disables the per-host connection cap.

DEFAULT_MAX_CONNECTIONS_PER_HOST
shutdown_close_timeout float

Seconds allowed for active WebSocket close handshakes after SIGTERM or SIGINT asks the hub to stop. The timeout is passed to the WebSocket server so shutdown stops accepting new sockets and bounds how long active close handshakes may delay process exit. Defaults to :data:DEFAULT_SHUTDOWN_CLOSE_TIMEOUT.

DEFAULT_SHUTDOWN_CLOSE_TIMEOUT
metrics_token str or None

When set (and enable_metrics is on), GET /metrics and GET /health require this token — presented as Authorization: Bearer <token> — and answer 401 without it, so an exposed metrics endpoint does not leak operational metadata. None leaves the endpoint open, which is the right default for a loopback bind.

None
metrics_query_token_ok bool

Also accept the token as a ?token=<token> query parameter. Off by default because a query token can leak into access logs, shell history, and proxy records; the Authorization header is the recommended path.

False
insecure_off_loopback bool

Bind a non-loopback host even when it would be reachable unauthenticated. Off by default the hub refuses such a bind — raising :class:InsecureBindError rather than only warning — so a bus is never accidentally exposed to the network without a token (and, with metrics on, a metrics token); set this to downgrade the refusal to a warning.

False
insecure_plaintext_at_rest bool

Bind a non-loopback host with a plaintext --db event store. Off by default the hub refuses such a bind — raising :class:AtRestBindError — so the durable coordination log never sits unencrypted on an exposed host's disk; encrypt the store (--db-key-file) or set this to downgrade the refusal to a warning. Loopback binds and encrypted stores are unaffected.

False
per_message_auth_keys Mapping[str, MessageAuthKey] or list[MessageAuthKey] or None

HMAC keys accepted for opt-in per-message authentication. None leaves the verifier with no configured keys.

None
require_per_message_auth bool

When True, selected mutating frames must carry valid per-message authentication before they can mutate hub state. Defaults to False.

False
per_message_auth_window_seconds float

Timestamp window used for signed-frame freshness and replay-cache eviction. Defaults to :data:~synapse_channel.core.message_auth.DEFAULT_MESSAGE_AUTH_WINDOW_SECONDS.

DEFAULT_MESSAGE_AUTH_WINDOW_SECONDS
per_message_auth_replay_capacity int

Maximum in-memory nonce entries retained for replay detection. Defaults to 4096.

4096
signed_event_trust_bundle EventSignatureTrustBundle or None

Ed25519 trust bundle accepted as an alternative signed-event verification path when require_per_message_auth is enabled. None leaves HMAC frame authentication as the only enforcing path.

None
capability_card_trust_bundle CapabilityCardTrustBundle or None

Separate Ed25519 trust and bounded lifecycle state used only to label capability-card advertisements. Verification stays advisory and default-off.

None
multihub_serving_policy MultiHubServingPolicy or None

Deny-by-default gate for serving the event log to peer hubs over a multi-hub pull. None (the default) refuses every peer. An explicit policy serves only a peer whose sender grant and live certificate it trusts, mirroring the following side's fail-closed pull gate.

None
namespace_ownership NamespaceOwnership or None

Single-authoritative-hub map that routes claims by namespace ownership. None (the default) lets the hub grant claims in every namespace, preserving single-hub behaviour; a map refuses a claim whose namespace this hub does not own, fail-closed.

None
claim_peers Mapping[str, ClaimForwardPeer] or None

How to reach each owning hub to forward a claim it owns, keyed by owning hub id. None (the default) forwards nothing: a claim this hub does not own is refused with the owner named, as before. With an entry for the resolved owner, a remote-owned claim is forwarded to that hub and its verdict relayed to the claimant; an unreachable owner falls back to the same refusal, fail-closed.

None
claim_forwarder ClaimForwarder

The seam that forwards a claim to an owning hub; defaults to the network :func:~synapse_channel.core.multihub_claim_transport.forward_claim. Injected in tests.

forward_claim
relay_peers Mapping[str, OperatorRelayPeer] or None

How to reach each owning hub to relay a governed operator action into a namespace it owns, keyed by owning hub id — separate from claim_peers because relaying a force-release is more privileged than forwarding a claim. None (the default) forwards no relay: an operator-relay frame for a namespace this hub does not own is refused fail-closed. With an entry for the resolved owner, the relay is forwarded to that hub and its verdict relayed to the requester, and the origin hub records an outbound audit event so the relay is attributable on both hubs.

None
relay_forwarder RelayForwarder

The seam that relays an operator action to an owning hub; defaults to the network :func:~synapse_channel.core.operator_relay_transport.relay_operator_action. Injected in tests.

relay_operator_action
require_relay_reason bool

Whether this hub refuses an operator relay that carries no reason. False (the default) records a reason when one is given but does not demand it; a team or production hub sets it so every governed cross-hub action leaves an auditable why (reason-required receipts).

False
require_two_person_relay bool

Whether an authorised operator relay needs a second, different operator before it applies. False (the default) applies an authorised relay immediately; a team or production hub sets it so a governed cross-hub force-release is recorded pending and carried out only when a second operator submits the same action, leaving a two-operator audit trail.

False
observed_asserting_hubs Callable[[str], Iterable[str]] or None

A runtime feed of the hub ids observed asserting authority over a namespace, consulted when resolving ownership so a partition — a peer seen owning a namespace this hub also believes it owns — refuses every grant until it is re-established. None (the default) supplies no assertions, so ownership resolves from the static map alone. Build it from a follower's observed claims with :func:~synapse_channel.core.multihub_fold.asserting_owners.

None
federation_bundle FederationBundle or None

Deny-by-default policy composing a peered remote domain's coordination frames into the live authorisation path. None (the default) leaves the frame path byte-for-byte unchanged — every frame is local. With a bundle, a frame whose verified signing key and live certificate pin resolve to a peered domain is authorised against that peering's bounded scope (composed with mutual TLS, the event signature, and the mapped scope, deny-closed) instead of the local ACL; a frame resolving to no peer stays local.

None
federation_cert_source PeerCertificateSource

Reads the peer's live certificate for the federation gate; defaults to :func:~synapse_channel.core.multihub_serving.live_peer_certificate_der. Injected in tests to exercise the decision without a mutual-TLS handshake.

live_peer_certificate_der
federation_offer_path str or Path or None

Path to this domain's own federation-bundle material, answered to a peer operator's synapse federation fetch. None (the default) offers nothing — the request is answered with an error frame. The file is re-read per request, so the offered material rotates without a restart; a fetched offer stays untrusted until the fetching operator compares fingerprints out-of-band and imports it explicitly.

None
anti_rollback_checkpoint bool

When True (the default) and a journal is attached, the hub verifies the durable log against its persisted Merkle checkpoint BEFORE serving — a truncated tail or a rewritten prefix raises :class:~synapse_channel.core.merkle_checkpoint.AntiRollbackError at startup instead of restarting silently — then anchors the current state as the newest hash-chained checkpoint link.

True
checkpoint_store_path str or Path or None

Override for the checkpoint database location; defaults to <journal path>.checkpoint.db beside the event store. The checkpoint store must live outside the log it attests.

None

finding_quota property

Return the copyable finding quota used by transactional memory writes.

from_config(config=None) classmethod

Construct a hub from a grouped :class:HubConfig record.

Parameters:

Name Type Description Default
config HubConfig or None

The grouped configuration; None builds the same hub as a bare SynapseHub(). The record flattens to exactly this class's keyword parameters (pinned by contract tests), so the two construction paths cannot diverge.

None

reserve_finding_slot(agent)

Reserve one durable-finding quota slot for agent (handler surface).

online_agents()

Return the sorted names of currently registered agents.

set_agent_roles(name, roles)

Bind the roles an agent answers to, as declared on its registration heartbeat.

permitted_role_claims(name, roles)

Return the subset of declared roles name is permitted to bind.

With role-claim enforcement off — the default open/loopback posture — every declared role is permitted, so a single-user dev hub binds roles exactly as before. With --require-role-claim on, a role is kept when either:

  • the role-grant store (synapse role / --role-grants) authorises name for it, or
  • the loaded ACL policy grants role-claim on target kind role for that role value (namespace-scoped like every other ACL rule).

An unauthorised role is dropped and logged as a squatting attempt rather than dropping the socket. Enforcement with no store and no matching ACL rule denies the claim (fail closed). The gate keys off the self-reported name, so pair it with a connect token and identity binding to be a real boundary.

roles_of(name)

Return the roles name currently answers to (empty tuple if none).

set_wake_capability(name, capability)

Bind the receiver wake capability declared on an identity's registration.

wake_capability_of(name)

Return the declared receiver wake capability for name.

observing_identities(target)

Return connected identities the ACL policy grants observe on target.

Under directed-message routing an observer (a live monitor or auditor) still receives a directed message it is not a party to only when it holds an observe grant. With no ACL policy configured there are no observers, so directed routing narrows to the recipients alone; the grant is scoped to the observer's own namespace, so an operator designates observers without opening the traffic to everyone.

recipients_without_live_waiter(recipients)

Present recipients with no proof of liveness — the ones to warn about.

Thin wrapper over :meth:~synapse_channel.core.hub_liveness.HubLivenessView.recipients_without_live_waiter, kept because the chat handler and tests call hub.recipients_without_live_waiter.

roster_liveness()

Per-agent liveness annotation for the /who roster (handler surface).

Thin wrapper over :meth:~synapse_channel.core.hub_liveness.HubLivenessView.roster_liveness, kept because the who-snapshot handler and tests call hub.roster_liveness.

uptime_seconds()

Return seconds elapsed since the hub was constructed.

handle_message(raw_message, websocket) async

Parse and route one inbound frame.

Parameters:

Name Type Description Default
raw_message str or bytes

The raw frame received from a client socket.

required
websocket Any

The socket the frame arrived on.

required

handler(websocket) async

Serve one client connection from registration to disconnect.

Thin wrapper over :meth:~synapse_channel.core.hub_connection.HubConnection.handler, kept as the entry point :meth:serve hands to the modern asyncio server API. websockets 13.0 invokes this callback after process_request has already returned a non-upgrade HTTP response; later releases skip it. Ignore that closed probe connection instead of registering it as an agent.

serve(host=DEFAULT_HOST, port=DEFAULT_PORT, *, ssl_context=None) async

Run the hub's WebSocket server until cancelled.

Always installs :meth:_process_request so Origin/Host handshake policy applies to every upgrade. With :attr:enable_metrics set, the same port also answers HTTP GET /metrics and GET /health.

Parameters:

Name Type Description Default
host str

Bind address. Defaults to :data:DEFAULT_HOST.

DEFAULT_HOST
port int

Bind port. Defaults to :data:DEFAULT_PORT.

DEFAULT_PORT
ssl_context SSLContext or None

Server-side TLS context. When supplied, the hub serves native wss:// instead of plain ws://.

None

FederationConfig dataclass

Cross-domain federation: the peering bundle, certificate reader, and served offer.

HubAuthConfig dataclass

Connection authentication, per-message authentication, and ACL enforcement.

Everything here defaults to the open loopback posture: no token, no signed frames, no ACL. Each mechanism is opt-in and composes with the others exactly as the flat keyword surface documents.

HubConfig dataclass

Complete, grouped construction record for one :class:SynapseHub.

The direct fields cover the hub's identity and collaborators; the nested family records cover the opt-in surfaces. HubConfig() reproduces a bare SynapseHub() exactly.

to_kwargs()

Flatten the record into the keyword arguments SynapseHub accepts.

Returns:

Type Description
dict[str, Any]

One entry per SynapseHub.__init__ keyword parameter: the nested family fields spread under their own names, then the direct fields. Contract tests pin the key set and the defaults against the live signature.

from_kwargs(kwargs) classmethod

Re-group flat SynapseHub keyword arguments into a record.

The inverse of :meth:to_kwargs: each family field regroups under its family, every other key is a direct field, and an omitted key takes its default — so a caller can hand over the partial keyword set it actually assembled (for example the CLI's subset of the ~40 parameters) and still get a complete record to fingerprint or reconstruct from. On the full key set it round-trips with :meth:to_kwargs.

Raises:

Type Description
TypeError

If kwargs carries a key that is neither a family field nor a direct SynapseHub parameter.

HubLimits dataclass

Every ceiling the hub enforces: retention, quotas, and transport bounds.

Field names and defaults match the SynapseHub.__init__ keyword parameters of the same names; see that signature's documentation for the meaning and failure mode of each bound.

HubMetricsConfig dataclass

The optional HTTP /metrics and /health endpoint and its token.

MultiHubConfig dataclass

Multi-hub routing: serving policy, namespace ownership, claim and relay forwarding.

TakeoverDamping dataclass

Damping and ownership rules applied when one agent name is contested.

Cooldown blunts a single eviction storm; the oscillation window and threshold detect two waiters at war over one name; quarantine pins a thrashing name to its current owner. The lease offline TTL bounds how long a name's ownership lease outlives its holder's disconnect, so a re-arming owner re-takes its name and a stranger cannot squat it in the gap.

Blackboard

The team's shared plan: a task ledger plus an append-only progress stream.

The board is single-threaded and synchronous; the hub owns one instance and mutates it from its event loop. Posting a task is an upsert — the same id re-declares the task and replaces its planning fields — so a planner can refine the plan idempotently.

Parameters:

Name Type Description Default
max_progress int

Maximum progress notes retained; the oldest are dropped beyond this bound so the stream cannot grow without limit. Clamped up to 1. Defaults to :data:DEFAULT_MAX_PROGRESS.

DEFAULT_MAX_PROGRESS
max_progress_per_author int

Maximum progress notes retained for one author. Clamped up to 1. Defaults to :data:DEFAULT_MAX_PROGRESS_PER_AUTHOR.

DEFAULT_MAX_PROGRESS_PER_AUTHOR
max_progress_per_task int

Maximum progress notes retained for one task id. Clamped up to 1. Defaults to :data:DEFAULT_MAX_PROGRESS_PER_TASK.

DEFAULT_MAX_PROGRESS_PER_TASK

publish_from(candidate)

Publish one fully prepared board candidate without changing this identity.

post_task(*, task_id, title, author, description='', depends_on=(), suggested_owner='', project='', expected_version=None, now=None)

Declare or re-declare a task on the plan (an upsert).

Parameters:

Name Type Description Default
task_id str

Identifier and short name; both are required (whitespace-stripped).

required
title str

Identifier and short name; both are required (whitespace-stripped).

required
author str

Agent declaring the task; recorded as created_by on first post.

required
description str

Longer description.

''
depends_on tuple[str, ...] or list[str]

Prerequisite task ids; self-references and duplicates are dropped.

()
suggested_owner str

Advisory proposed owner.

''
project str

Project namespace. On a first declaration "" leaves the task unscoped; on a re-declaration "" keeps the existing scope, and a non-empty value conflicting with an already-scoped task is refused (fail-closed against accidental re-scoping).

''
expected_version int or None

Compare-and-set guard: when given, the mutation is refused unless the task's current version equals it (a missing task counts as version 0).

None
now float or None

Override for the current wall-clock time, in seconds.

None

Returns:

Type Description
tuple[bool, str]

(True, message) on success, (False, reason) when the id or title is missing, the dependencies would form a cycle, the project scope conflicts, or the version check fails.

update_task(task_id, *, status=None, suggested_owner=None, project=None, expected_version=None, now=None)

Change a declared task's planning status, owner, or project scope.

Parameters:

Name Type Description Default
task_id str

Identifier of the task to update.

required
status str or None

New planning status; must be in :data:LEDGER_TASK_STATUSES.

None
suggested_owner str or None

Replacement advisory owner ("" clears it).

None
project str or None

Replacement project scope ("" clears it).

None
expected_version int or None

Compare-and-set guard: when given, the update is refused unless the task's current version equals it.

None
now float or None

Override for the current wall-clock time, in seconds.

None

Returns:

Type Description
tuple[bool, str]

(True, message) on success, (False, reason) when the task is unknown, the status is not a recognised planning status, or the version check fails.

post_progress(*, task_id, author, text, kind='note', now=None)

Append a structured progress note, dropping the oldest past the bound.

Parameters:

Name Type Description Default
task_id str

Task the note concerns; "" for a board-wide note.

required
author str

Agent posting the note.

required
text str

Body of the note.

required
kind str

One of :data:PROGRESS_KINDS. Defaults to "note".

'note'
now float or None

Override for the current wall-clock time, in seconds.

None

Returns:

Type Description
tuple[bool, ProgressNote or str]

(True, note) on success, (False, reason) for an unknown kind.

note(*, task_id, author, text, now=None)

Append a plain note-kind progress entry, returning it directly.

A convenience over :meth:post_progress for callers that always use the note kind (so the kind cannot be rejected) and want the appended :class:ProgressNote without unpacking a result tuple.

Parameters:

Name Type Description Default
task_id str

Task the note concerns; "" for a board-wide note.

required
author str

Agent posting the note.

required
text str

Body of the note.

required
now float or None

Override for the current wall-clock time, in seconds.

None

Returns:

Type Description
ProgressNote

The appended note.

restore_progress(note)

Restore one persisted progress note while applying retention bounds.

Parameters:

Name Type Description Default
note ProgressNote

Persisted note to insert into the retained progress stream.

required

Returns:

Type Description
ProgressNote

The restored note.

blocking_dependencies(task_id)

Return the unmet dependencies of a task, in declaration order.

A dependency is unmet when the prerequisite is absent from the board or has not reached a terminal status. Returns an empty list for an unknown task.

Parameters:

Name Type Description Default
task_id str

Identifier of the task to inspect.

required

Returns:

Type Description
list[str]

Task ids that still block this task.

ready_tasks()

Return open tasks whose every dependency has reached a terminal status.

Returns:

Type Description
list[LedgerTask]

Tasks with planning status open and no blocking dependency, sorted by task_id.

snapshot(*, task_cap=None)

Return a consistent view of the plan and the recent progress stream.

Parameters:

Name Type Description Default
task_cap int or None

When set, bound the served tasks list (floored at 1): 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. ready always lists every ready id — ids are cheap, the task bodies are what outgrow a frame. None serves the full board unchanged.

None

Returns:

Type Description
dict[str, Any]

Mapping with tasks (sorted by id), ready (ready task ids), and progress (the retained notes in order); under a cap also total_tasks, truncated, and task_cap (the applied bound, so a consumer can render a "kept / cap" gauge).

LedgerTask dataclass

A declared unit of work on the shared plan.

Attributes:

Name Type Description
task_id str

Stable identifier, shared with any claim taken on the task.

title str

Short human-readable name of the work.

description str

Optional longer description or acceptance notes.

depends_on tuple[str, ...]

Task ids that must reach a terminal status before this task is ready.

status str

Coarse planning status from :data:LEDGER_TASK_STATUSES.

suggested_owner str

Optional agent name proposed to take the task; advisory only.

project str

Optional project namespace the task belongs to. "" means unscoped (legacy); an unscoped task is never eligible for automated dispatch.

version int

Monotonic mutation counter: 1 at declaration, +1 on every accepted re-declare or update. Enables compare-and-set via expected_version.

created_by str

Agent that first declared the task.

created_at float

Wall-clock seconds when the task was first declared.

updated_at float

Wall-clock seconds when the task was last changed.

as_dict()

Return a JSON-serialisable snapshot of this task.

ProgressNote dataclass

One structured entry in the append-only progress ledger.

Attributes:

Name Type Description
task_id str

Task the note concerns; "" for a board-wide note.

author str

Agent that posted the note.

kind str

One of :data:PROGRESS_KINDS.

text str

Free-form body of the note.

posted_at float

Wall-clock seconds when the note was posted.

as_dict()

Return a JSON-serialisable snapshot of this note.

TaskStatus

The legal status values for a claimed task.

CLAIMED is the entry state stamped when a lease is granted; DONE and FAILED are terminal. Values are the literal strings carried on the wire.

Metric dataclass

One Prometheus sample: a named gauge or counter with help text.

Attributes:

Name Type Description
name str

Metric name, e.g. synapse_active_claims.

documentation str

Human-readable HELP text.

metric_type str

gauge for a value that goes up and down, counter for a monotonic total.

value float

The current sample value.

EventStore

Append-only SQLite event log in WAL mode.

Parameters:

Name Type Description Default
path str or Path

Database file path. ":memory:" is accepted for ephemeral use, but only a file path survives a restart.

required
key_file str or Path or None

Owner-only 32-byte key file. When set, the store opens through SQLCipher (pip install synapse-channel[sqlcipher]) so every page is encrypted at rest. Omit for the default plaintext :mod:sqlite3 path.

None
key bytes or None

Raw 32-byte key material (tests and programmatic callers). When set, takes precedence over key_file.

None

encrypted property

Return whether this store opened through SQLCipher page encryption.

append(kind, payload, *, ts=None, durable=False)

Append one event to the log and return its assigned sequence number.

Parameters:

Name Type Description Default
kind str

Event kind tag.

required
payload dict[str, Any]

JSON-serialisable event body.

required
ts float or None

Event timestamp, in seconds; the system clock is used when None.

None
durable bool

When True the commit is synced at synchronous=FULL so it survives an OS crash; when False it commits at NORMAL (durable only against an application crash). Defaults to False.

False

Returns:

Type Description
int

The monotonic seq the row was assigned (the autoincrement primary key). It is durable and never reused across restarts — unlike the in-memory per-hub msg_id — so it is the stable cursor a reconnecting client resumes a directed-message backlog from.

Notes

A failed database write is rolled back. Durable attempts restore the connection to synchronous=NORMAL. If that cleanup alone fails after COMMIT, the append remains successful and the cleanup failure is logged; reporting a write failure at that point would contradict durable truth.

append_batch(events, *, ts=None, durable=False)

Append several events in one SQLite transaction.

The whole batch commits or rolls back as one unit. All rows share one timestamp so adjacent state and provenance events describe the same authoritative transition.

Parameters:

Name Type Description Default
events iterable[tuple[str, Mapping[str, Any]]]

Ordered (kind, payload) pairs.

required
ts float or None

Shared timestamp; the system clock is used when omitted.

None
durable bool

Use synchronous=FULL for the batch commit.

False

Returns:

Type Description
tuple[int, ...]

Assigned sequence numbers in input order. An empty input returns an empty tuple without opening a transaction.

get_operation(operation_key)

Return one completed operation without exposing the key in errors.

read_operations()

Return durable operation records in commit order for cache seeding.

pending_operation_outbox_count()

Return the number of committed operation intents awaiting projection.

pending_operation_intents(*, limit=100)

Return committed evidence intents awaiting idempotent local projection.

mark_operation_intent_delivered(operation_key, receipt_id)

Bind one operation intent to a value-free local projection receipt.

commit_delivery_receipt_request(*, chat, requested, stage_hook=None)

Atomically commit a directed chat and its requested-receipt aggregate.

The chat sequence is assigned inside the transaction and injected into the requested event and aggregate. A crash therefore leaves either both facts or neither; it can never retain a directed body without the fact that its sender requested a receipt.

commit_delivery_receipt_transition(*, kind, payload, notification, state, companion=None, stage_hook=None)

Commit one receipt transition and its stable notification outbox row.

The outbox identity is deterministic for (message_seq, state). A process crash after WebSocket acceptance but before acknowledgement may therefore replay the same frame, and the sender can deduplicate it without the hub pretending to know whether a model consumed it.

delivery_receipt_aggregate(message_seq)

Return the current durable receipt aggregate for one chat sequence.

pending_delivery_notifications(*, sender=None, limit=100)

Return undelivered receipt frames in stable insertion order.

mark_delivery_notification_attempt(notification_id, *, delivered)

Record one sender-delivery attempt and settle it only on transport success.

commit_operation(*, operation_key, request_digest, response, events, intent, response_event_seq_field=None, stage_hook=None)

Atomically commit a keyed mutation, exact response, and evidence intent.

pending_aef_events(*, limit=100)

Return queued legacy rows awaiting native AEF reconciliation.

mark_aef_delivered(legacy_seq, receipt_id)

Durably bind one queued legacy row to its emitted AEF receipt.

aef_delivery(legacy_seq)

Return the delivered receipt id, or None for pending/absent rows.

read_all()

Return every event in insertion order.

Returns:

Type Description
list[StoredEvent]

All persisted events, ordered by ascending sequence number.

iter_events(*, through_seq=None, kinds=None)

Yield events in ascending sequence order without materialising the log.

This is the bounded-memory read seam for whole-log folds (the Merkle commitment, causality reconstruction): rows stream off the SQLite cursor one at a time, so the peak footprint is one event, not the log. A kind filter is applied inside SQLite, so uninterested kinds (bulk chat on a long-lived hub) never cross into Python at all.

Parameters:

Name Type Description Default
through_seq int or None

Inclusive sequence ceiling; events after it are not yielded. None streams the whole log.

None
kinds Iterable[str] or None

When given, restrict the stream to these event kinds; an empty iterable yields nothing. None streams every kind.

None

Yields:

Type Description
StoredEvent

Each matching event at or below the ceiling, by ascending sequence.

read_since(after_seq, *, kinds=None, limit=None)

Return events whose sequence is greater than a cursor, in order.

This is the durable, presence-free ingest seam a downstream persistent-memory adapter polls: it tracks the last sequence it consumed, calls :meth:read_since with it, processes the batch, and advances — resuming with no loss or duplication across hub restarts, because the sequence is a monotonic primary key.

Parameters:

Name Type Description Default
after_seq int

Exclusive lower bound; only events with seq > after_seq are returned. Pass 0 for the whole log.

required
kinds Iterable[str] or None

When given, restrict the result to these event kinds (e.g. :data:~synapse_channel.core.journal.MEMORY_KINDS); an empty iterable returns nothing. None returns every kind.

None
limit int or None

Cap the batch size (floored at 0); None returns all matching events. The cap applies after ordering, so repeated calls walk the log forward in fixed-size batches.

None

Returns:

Type Description
list[StoredEvent]

Matching events ordered by ascending sequence number.

read_window(*, min_seq=None, max_seq=None, since_ts=None, until_ts=None, kinds=None, limit=None)

Return events inside an inclusive sequence/time window, in order.

This is the selective-read seam the event-query layer uses to avoid loading an unbounded event store for every point-in-time or windowed query: the bounds are pushed into SQLite so only candidate rows are deserialised. Every bound is optional and inclusive; omitting all of them is equivalent to :meth:read_all.

Parameters:

Name Type Description Default
min_seq int or None

Inclusive lower and upper sequence bounds (seq >= min_seq / seq <= max_seq).

None
max_seq int or None

Inclusive lower and upper sequence bounds (seq >= min_seq / seq <= max_seq).

None
since_ts float or None

Inclusive lower and upper timestamp bounds (ts >= since_ts / ts <= until_ts).

None
until_ts float or None

Inclusive lower and upper timestamp bounds (ts >= since_ts / ts <= until_ts).

None
kinds Iterable[str] or None

Restrict to these event kinds; an empty iterable returns nothing.

None
limit int or None

Cap the number of rows returned after ordering (floored at 0).

None

Returns:

Type Description
list[StoredEvent]

Matching events ordered by ascending sequence number.

corrupt_rows(*, through_seq=None)

Return safe forensic markers for every malformed row in sequence order.

Parameters:

Name Type Description Default
through_seq int or None

Inclusive sequence ceiling. None scans the complete event log.

None

Returns:

Type Description
tuple[CorruptEventRow, ...]

Markers contain reasons and a raw-payload digest, never raw payload bytes. This scan is the operator seam used by explicit compaction.

count()

Return the number of events currently stored.

max_seq()

Return the highest sequence number stored, or 0 when the log is empty.

Useful as a fully-settled compaction floor: with no read-side consumer lagging behind, the whole log up to the latest sequence may be compacted (see :mod:synapse_channel.core.compaction).

latest_at_or_before(through_seq)

Return the newest retained event at or below through_seq.

A direct descending primary-key lookup keeps state-at projections from decoding the whole journal merely to obtain their deterministic clock. Sequence gaps left by retention are handled by selecting the preceding retained event; an empty prefix returns None.

delete(seqs)

Delete the events with these sequence numbers; return how many were removed.

A maintenance primitive for retention/compaction (:mod:synapse_channel.core.compaction). A deleted sequence is never reused — the AUTOINCREMENT primary key only ever increases — so a downstream :meth:read_since cursor stays correct across a compaction: a removed sequence simply becomes a gap the cursor walks past. The delete commits at NORMAL durability; a delete lost to an OS crash is harmless because re-running compaction removes the same rows again.

Parameters:

Name Type Description Default
seqs Iterable[int]

Sequence numbers to remove; an empty iterable is a no-op.

required

Returns:

Type Description
int

The number of rows actually deleted.

vacuum()

Reclaim free pages left by deletes, shrinking the database file on disk.

A DELETE marks pages free for reuse but does not return them to the filesystem, so a large retention sweep leaves the file the same size until VACUUM rewrites the database to release the free pages. It rewrites the whole database, so call it from a maintenance path, not the hot loop.

close()

Close the underlying database connection.

__enter__()

Enter a context manager that closes the store on exit.

__exit__(exc_type, exc, tb)

Close the store when leaving the context.

MessageType

String constants for every Synapse message type.

The upper group is sent by agents to the hub; the lower group is emitted by the hub back to agents. Values are the literal strings that travel on the wire — never rename a value without migrating every peer.

ResourceOffer dataclass

A capability that an agent advertises to the rest of the team.

Attributes:

Name Type Description
agent str

Name of the offering agent.

kind str

Category of the resource, e.g. llm, compute, fs, or memory.

name str

Concrete resource identifier, e.g. a model name or device handle.

capacity int

How many concurrent consumers the offer can serve (minimum 1).

meta dict[str, Any]

Arbitrary descriptive metadata about the offer.

offered_at float

Wall-clock time, in seconds, when the offer was last refreshed.

SynapseState

Authoritative registry of presence, claims, tasks, and resources.

The registry is single-threaded and synchronous; the hub owns one instance and mutates it from its event loop. Every mutating call refreshes the caller's heartbeat and lazily expires stale leases and offers, so liveness is maintained without a background timer.

Parameters:

Name Type Description Default
default_ttl_seconds float

Lease duration applied to a claim when the caller does not request an explicit TTL. Clamped into [MINIMUM_TTL_SECONDS, MAXIMUM_TTL_SECONDS]. Defaults to 3600.0.

3600.0
max_claims_per_agent int

Most live claims one agent may hold. Clamped up to 1. Defaults to :data:MAX_CLAIMS_PER_AGENT.

MAX_CLAIMS_PER_AGENT
max_offers_per_agent int

Most live resource offers one agent may register. Clamped up to 1. Defaults to :data:MAX_OFFERS_PER_AGENT.

MAX_OFFERS_PER_AGENT
max_paths_per_claim int

Most distinct paths a single claim may declare before its scope is widened to the whole worktree. Clamped up to 1. Defaults to :data:~synapse_channel.core.scoping.MAX_DECLARED_PATHS.

MAX_DECLARED_PATHS

publish_from(candidate)

Atomically publish a privately mutated candidate state.

Durable hub mutations are prepared on a deep copy while the current state remains visible to readers. After the matching journal append commits, the event-loop mutation actor calls this synchronous method; because it contains no await, readers observe either the complete old state or the complete committed state, never a provisional mutation.

last_seen is updated in place because the hub liveness view retains that mapping by reference. The other registries are reached through hub.state on every read and can therefore be replaced wholesale. Configuration is immutable for a running hub and must agree between the authoritative state and its clone.

reindex_leases()

Rebuild the lease-expiry heap from the live claims.

Discards every superseded heap entry in one pass. Used after a bulk load that assigns claims directly — a journal replay — and as the churn-bound rebuild in :meth:_track_lease.

heartbeat(agent, now=None)

Record that agent is alive and expire anything now stale.

Parameters:

Name Type Description Default
agent str

Name of the agent reporting liveness.

required
now float or None

Override for the current wall-clock time, in seconds. When None the system clock is used. Primarily a testing seam.

None

claim(agent, task_id, note='', ttl_seconds=None, now=None, *, quota_principal=None, worktree=DEFAULT_WORKTREE, paths=(), path_identity=None, git=None)

Acquire or renew a scoped lease on a task.

An owner may freely renew its own live claim, and any agent may take over a task whose lease has expired. A live claim held by another agent blocks the request. Beyond the task id, a claim may declare a file scope (worktree + paths); the request is also refused when that scope contends with another agent's live claim, which is how the bus refuses overlapping scoped authority. Every successful claim or renewal is stamped with a fresh, strictly-increasing :attr:TaskClaim.epoch.

Parameters:

Name Type Description Default
agent str

Name of the agent attempting the claim.

required
task_id str

Identifier of the task; surrounding whitespace is stripped.

required
note str

Human-readable context stored with the claim.

''
ttl_seconds float or None

Requested lease duration, clamped into [MINIMUM_TTL_SECONDS, MAXIMUM_TTL_SECONDS]. A non-finite request falls back to default_ttl_seconds. None uses default_ttl_seconds.

None
now float or None

Override for the current wall-clock time, in seconds.

None
worktree str

Worktree label; different worktree identities do not contend.

DEFAULT_WORKTREE
paths tuple[str, ...] or list[str]

Declared file/directory paths; empty claims the whole worktree.

()
path_identity ClaimScopeIdentity or None

Client-derived filesystem-canonical identity aligned one-to-one with paths. Malformed alignment is refused rather than ignored.

None
git GitContext or None

Branch context to attach to the claim; None leaves it unset. A renewal replaces it with the supplied value, so a git-aware client keeps the branch current by passing it on every claim.

None
quota_principal str or None

Stable server-derived identity bucket charged for the claim. None preserves the direct-call compatibility behaviour by using agent.

None

Returns:

Type Description
tuple[bool, str]

(True, message) on success, (False, reason) when the task is missing an id, held by another agent, or its file scope overlaps another agent's live claim.

update_task(agent, task_id, *, status=None, note=None, data_ref=None, epoch=None, expected_version=None, now=None)

Update the status, note, or artefact reference of an owned task.

Only the claim owner may mutate it. Fields left as None are untouched; a non-empty status must be a legal lifecycle transition (see :func:synapse_channel.core.lifecycle.can_transition). When epoch is supplied it must match the claim's current epoch (lease guard), and when expected_version is supplied it must match the claim's current version (optimistic-concurrency guard against lost updates). A successful update bumps the version.

Parameters:

Name Type Description Default
agent str

Name of the agent issuing the update; must own the claim.

required
task_id str

Identifier of the task to update.

required
status str or None

New lifecycle status, applied only when truthy and the transition is legal.

None
note str or None

Replacement note; stripped before storage.

None
data_ref str or None

Replacement artefact reference; stripped before storage.

None
epoch int or None

Expected lease generation; when given and stale, the update is refused.

None
expected_version int or None

Expected field version; when given and mismatched, the update is refused so a stale writer cannot clobber a newer value.

None
now float or None

Override for the current wall-clock time, in seconds.

None

Returns:

Type Description
tuple[bool, str]

(True, message) on success, (False, reason) when the task is unknown, owned by a different agent, carries a stale epoch or version, or requests an illegal status transition.

save_checkpoint(agent, task_id, checkpoint, *, epoch=None, now=None)

Save a resume token on an owned task so it can continue after expiry.

Only the owner may save, and the checkpoint persists with the claim: if the lease later expires, a new claimant of the same task inherits it.

Parameters:

Name Type Description Default
agent str

The owner saving the checkpoint.

required
task_id str

Identifier of the owned task; whitespace is stripped.

required
checkpoint str

Opaque resume token to store.

required
epoch int or None

Expected lease generation; a stale epoch is refused.

None
now float or None

Override for the current wall-clock time, in seconds.

None

Returns:

Type Description
tuple[bool, str]

(True, message) on success, (False, reason) when the task is unknown, owned by another agent, or carries a stale epoch.

release(agent, task_id, now=None, *, epoch=None)

Release a task held by agent.

Parameters:

Name Type Description Default
agent str

Name of the agent releasing the claim; must be the owner.

required
task_id str

Identifier of the task; surrounding whitespace is stripped.

required
now float or None

Override for the current wall-clock time, in seconds.

None
epoch int or None

Expected lease generation; when given and stale, the release is refused so an agent cannot drop a lease that has since been superseded.

None

Returns:

Type Description
tuple[bool, str]

(True, message) on success, (False, reason) when the task id is empty, unclaimed, owned by another agent, or carries a stale epoch.

force_release(task_id, *, by)

Release a task regardless of who holds it, on externally verified authority.

Unlike :meth:release, this does not require the caller to be the lease owner: the authority to revoke another agent's claim is established before this is called — a governed cross-hub operator relay whose peer, scope, and namespace ownership have all been verified deny-closed. This method only executes the revocation the caller is already entitled to make, so it never checks ownership itself; it must not be reachable except behind that authorisation.

The revocation is otherwise identical to a self-release: the lease is dropped and any retained checkpoint discarded, so a later unrelated claim of the same id does not resurrect stale resume state. It does not touch the operator's heartbeat — the operator is remote and holds no presence on this hub.

Parameters:

Name Type Description Default
task_id str

Identifier of the task to revoke; surrounding whitespace is stripped.

required
by str

The operator identity the revocation is attributed to, named in the message for the audit trail.

required

Returns:

Type Description
tuple[bool, str]

(True, message) naming the operator and the previous holder on success; (False, reason) when the task id is empty or the task is not claimed.

handoff(agent, task_id, to_agent, *, note=None, epoch=None, now=None)

Transfer an owned task to another agent in one atomic step.

Ownership moves directly from the holder to to_agent with no release/re-claim window in which a third agent could grab the task. The task keeps its file scope, status, and artefact reference (its working context) and is stamped with a fresh epoch and a full lease, so the previous owner's epoch becomes stale and cannot act on the moved task. The version counter resets for the new owner.

Parameters:

Name Type Description Default
agent str

The current owner requesting the handoff.

required
task_id str

Identifier of the task to hand off; whitespace is stripped.

required
to_agent str

The agent to receive the task; whitespace is stripped.

required
note str or None

Replacement note for the moved claim; the existing note is kept when None.

None
epoch int or None

Expected lease generation; a stale epoch is refused.

None
now float or None

Override for the current wall-clock time, in seconds.

None

Returns:

Type Description
tuple[bool, str]

(True, message) on success, (False, reason) when the task is missing an id, unclaimed, owned by another agent, handed to its own owner, given no target, carries a stale epoch, the recipient already holds the live-claim cap, or the moved file scope conflicts with another agent's live claim (the same two invariants as direct :meth:claim).

offer_resource(agent, *, kind, name, capacity=1, meta=None, now=None)

Advertise a resource the agent can provide, keyed by agent/kind/name.

Re-offering the same triple refreshes the offer's liveness timestamp. A new offer is refused once the agent already holds :data:MAX_OFFERS_PER_AGENT live offers, so a runaway agent cannot bloat the registry; refreshing an existing offer is always allowed.

Parameters:

Name Type Description Default
agent str

Name of the offering agent.

required
kind str

Resource category, e.g. llm or compute.

required
name str

Concrete resource identifier.

required
capacity int

Concurrent-consumer capacity, clamped up to 1.

1
meta dict[str, Any] or None

Descriptive metadata; None becomes an empty mapping.

None
now float or None

Override for the current wall-clock time, in seconds.

None

Returns:

Type Description
str or None

The registry key "{agent}:{kind}:{name}" of the stored offer, or None when the agent is at its offer quota and this is a new offer.

query_resources(kind=None)

List currently offered resources, optionally filtered by kind.

Parameters:

Name Type Description Default
kind str or None

When given, only offers of this category are returned.

None

Returns:

Type Description
list[dict[str, Any]]

Offer mappings sorted by (agent, kind, name).

snapshot(now=None)

Return a consistent view of claims, agents, and resources.

Stale claims and offers are expired before the view is built, so the snapshot never reports leases that have already lapsed.

Parameters:

Name Type Description Default
now float or None

Override for the current wall-clock time, in seconds.

None

Returns:

Type Description
dict[str, Any]

Mapping with active_claims, agents, resources, and the generated_at timestamp.

TaskClaim dataclass

A lease held by one agent over a named unit of work.

The owner keeps the claim until it is explicitly released or its lease expires. While the lease is live, other agents are refused the same task.

Attributes:

Name Type Description
task_id str

Stable identifier of the claimed task.

owner str

Name of the agent currently holding the claim.

note str

Free-form human-readable context for the claim.

claimed_at float

Wall-clock time, in seconds, when the claim was last (re)acquired.

lease_expires_at float

Wall-clock time, in seconds, after which the claim auto-expires.

quota_principal str

Internal stable bucket charged for this claim. It is intentionally absent from public snapshots and wire grants; durable journal writers use :meth:as_persisted_dict so restarts cannot reset a principal's budget.

status str

Exact claim lifecycle marker: claimed, working, input_required, done, or failed.

data_ref str

Optional pointer to produced artefacts (e.g. a memory key or file path).

worktree str

Worktree label the work happens in; claims in different worktrees never contend for files.

paths tuple[str, ...]

Declared file/directory paths the claim intends to touch; empty means the whole worktree.

path_identity ClaimScopeIdentity or None

Optional client-derived canonical identity aligned with paths. The hub compares it but never resolves paths or reads the filesystem.

epoch int

Strictly-increasing lease generation. A mutation carrying a stale epoch is rejected, so a paused/expired agent cannot act on a superseded claim.

version int

Optimistic-concurrency counter bumped on every field update, used for compare-and-swap so a stale update is rejected. Reset on (re)claim.

checkpoint str

Opaque resume token the owner saves so the work can continue from where it stopped. It survives lease expiry: a later claimant of the same task inherits the last checkpoint instead of restarting.

git GitContext or None

The branch context the claim is scoped to, set by a git-aware client; None for a plain claim. Opaque to the hub: stored and displayed but never acted on because the hub runs no git.

as_dict()

Return a JSON-serialisable snapshot of this claim.

Returns:

Type Description
dict[str, Any]

A mapping with the claim's public fields, safe to embed in a wire message or state snapshot.

as_persisted_dict()

Return a durable snapshot including private quota accounting.

Public state and protocol views use :meth:as_dict, which omits the credential-derived principal. The append-only journal needs the bucket so replay preserves quota enforcement across a hub restart.

default_hub_uri()

Return the hub URI a command should use when --uri is not given.

Reads the SYNAPSE_URI environment variable so an operator can point the whole CLI at a non-default hub — a remote coordinator, a second local hub on another port — without repeating --uri on every command. A blank or unset variable falls back to :data:DEFAULT_HUB_URI, the loopback hub. Resolved each time a parser is built, so every fresh CLI process reads the current environment; an explicit --uri on the command line still wins.

sanitize_text(text, max_len=400)

Collapse runs of whitespace and truncate to max_len characters.

Parameters:

Name Type Description Default
text str

Raw text to clean. Non-string input is coerced via str.

required
max_len int

Maximum length of the returned string. Defaults to 400.

400

Returns:

Type Description
str

Single-spaced, length-bounded text.

plan_team(port, *, no_workers=False, fast_model=None, reason_model=None, prefix='', detect=detect_model)

Plan the child processes for a team without spawning anything.

Parameters:

Name Type Description Default
port int

Hub port; the worker URI is derived from it.

required
no_workers bool

When True only the hub is planned. Defaults to False.

False
fast_model str or None

Explicit model overrides; when None they are auto-detected.

None
reason_model str or None

Explicit model overrides; when None they are auto-detected.

None
prefix str

Namespace prepended to every worker name, so a team can run per project without clashing with another project's roster. Defaults to "".

''
detect ModelDetector

Model-detection callable, injectable for testing.

detect_model

Returns:

Type Description
list[ProcessSpec]

The hub spec followed by zero, one, or two worker specs. A second worker is added only when the reasoning model differs from the fast one.

run_team(port=8876, *, no_workers=False, fast_model=None, reason_model=None, prefix='', popen=subprocess.Popen, sleep=time.sleep, detect=detect_model, is_hub_ready=_hub_is_listening, shutdown_timeout_seconds=SHUTDOWN_TIMEOUT_SECONDS)

Spawn a hub and workers, then monitor them until one exits.

Parameters:

Name Type Description Default
port int

Hub port. Defaults to 8876.

8876
no_workers bool

When True only the hub is started. Defaults to False.

False
fast_model str or None

Explicit model overrides; auto-detected when None.

None
reason_model str or None

Explicit model overrides; auto-detected when None.

None
prefix str

Namespace prepended to every worker name. Defaults to "".

''
popen Callable

subprocess.Popen-compatible spawner, injectable for testing.

Popen
sleep Callable

time.sleep-compatible delay, injectable for testing.

sleep
detect ModelDetector

Model-detection callable, injectable for testing.

detect_model
is_hub_ready Callable

Predicate that reports whether the hub is accepting connections on the port, injectable for testing. Defaults to a real TCP probe.

_hub_is_listening
shutdown_timeout_seconds float

Seconds to wait during shutdown before killing a child process.

SHUTDOWN_TIMEOUT_SECONDS

Returns:

Type Description
int

0 on a clean Ctrl+C shutdown, 1 if the hub never started listening, otherwise the exit code of the first child that terminated (or 1 when it exited without a code).

is_service_message(sender, payload, msg_type='chat')

Return whether a message is system/sidecar noise the worker should skip.

Parameters:

Name Type Description Default
sender str

Name of the message sender.

required
payload str

Message text.

required
msg_type str

Message type. Defaults to "chat".

'chat'

Returns:

Type Description
bool

True for hub messages, *_LITE/*_CORE sidecars, system snapshot/notification types, and [ACK]/[ROUTE]/[MAIN] relay markers; False otherwise.

classify(prompt, *, rule_max_chars=DEFAULT_RULE_MAX_CHARS, heavy_min_chars=DEFAULT_HEAVY_MIN_CHARS)

Classify a prompt into a routing task class.

The policy is deterministic: a short prompt is rule; a long prompt or one containing a heavy keyword is heavy; everything else is slm.

Parameters:

Name Type Description Default
prompt str

The user prompt to classify; leading/trailing whitespace is ignored.

required
rule_max_chars int

Length at or below which a prompt is rule.

DEFAULT_RULE_MAX_CHARS
heavy_min_chars int

Length at or above which a prompt is heavy.

DEFAULT_HEAVY_MIN_CHARS

Returns:

Type Description
str

One of :attr:TaskClass.RULE, :attr:TaskClass.SLM, or :attr:TaskClass.HEAVY.

detect_stalls(board, *, now, idle_seconds=DEFAULT_IDLE_SECONDS, policy=None)

Decide which tasks on a board snapshot should be re-offered.

Parameters:

Name Type Description Default
board dict[str, Any]

A blackboard snapshot as returned by :meth:~synapse_channel.core.ledger.Blackboard.snapshot.

required
now float

Current wall-clock time, in seconds, used to age in-progress tasks.

required
idle_seconds float

Backwards-compatible fixed no-activity window. Ignored when policy is supplied.

DEFAULT_IDLE_SECONDS
policy StallPolicy or None

Full operator policy. None preserves the historical fixed-threshold call shape.

None

Returns:

Type Description
list[Intervention]

One re-offer per stalled task, sorted by task_id.

compact(store, policy, *, floor_seq, now=None, dry_run=False)

Apply a retention policy to the durable log, deleting only below the floor.

Parameters:

Name Type Description Default
store EventStore

The event log to compact in place.

required
policy RetentionPolicy

Which retention knobs to apply; a no-op policy deletes nothing.

required
floor_seq int

Compaction only considers events with seq <= floor_seq; nothing above it is touched, so a downstream ingest cursor at or below the floor never loses an unconsumed event. Pass the lowest sequence every memory consumer has ingested (e.g. via :meth:~synapse_channel.core.persistence.EventStore.max_seq for a fully settled log).

required
now float or None

Wall-clock time used to age out findings; the system clock is used when None.

None
dry_run bool

Calculate and return the exact deletion counts without changing the store. Used to persist a recovery archive before destructive removal.

False

Returns:

Type Description
CompactionResult

Counts of what was removed and the floor that was honoured.

would_create_cycle(waits, claims, waiter, holder)

Return whether waiter waiting for holder would close a cycle.

Parameters:

Name Type Description Default
waits Mapping[str, set[str]]

The current wait-for graph mapping each waiting agent to the tasks it waits for.

required
claims Mapping[str, Any]

The live claims registry (task id → claim object or mapping with an owner), used to resolve each waited task's current holder.

required
waiter str

The agent that wants to start waiting.

required
holder str

The agent currently holding what waiter wants.

required

Returns:

Type Description
bool

True if adding the edge waiter -> holder would create a cycle (including the degenerate self-wait waiter == holder); False when the wait is safe to register.

admit(finding)

Decide whether a finding is admitted, floored, or rejected.

Rejections are checked first — structural omissions and the one hard contradiction, falsified evidence claiming reference-validated (LOCK-4). A record that fails any is refused outright. Otherwise the claim status and freshness are lowered where the evidence cannot support them, in an order that preserves honesty: falsified evidence renders the claim refuted (INV-2), then producer-asserted testimony is stripped of any source-verified freshness and capped at the boundary (INV-6), then a reference-validated claim that lacks a reference or a source-verified freshness is floored (INV-1) — so a contradiction is resolved to its most honest standing exactly once.

Parameters:

Name Type Description Default
finding Finding

The parsed record to admit.

required

Returns:

Type Description
Decision

The verdict, the admitted record (or None on reject), and the reasons.

can_transition(current, target)

Return whether a task may move from current to target.

A move to an unknown status is never allowed. Re-affirming the same status is always allowed (it lets an owner update other fields without changing state). Otherwise the move must appear in the transition table.

Parameters:

Name Type Description Default
current str

The task's present status.

required
target str

The requested next status.

required

Returns:

Type Description
bool

True if the transition is legal.

collect_hub_metrics(hub)

Read the hub's live counters into a list of metrics.

Only the hub's in-memory state is inspected — no I/O — so this is safe to call from the event loop on every scrape.

Parameters:

Name Type Description Default
hub SynapseHub

The hub to read.

required

Returns:

Type Description
list[Metric]

A constant synapse_up liveness gauge, gauges for the live presence/claims/resources/history/board counts (waiter sidecars and dead-letter targets included), the monotonic message counter, and the hub's decision counters — claims granted/denied, releases, directed and broadcast chat, auth failures, rate-limit rejections, federation denials, forwarded-claim outcomes, takeovers and their quarantines — everything a Grafana panel or an alert rule needs to see the hub deciding, not just existing.

health_snapshot(hub)

Return a small JSON-serialisable health document for the hub.

Parameters:

Name Type Description Default
hub SynapseHub

The hub to summarise.

required

Returns:

Type Description
dict[str, Any]

status (ok or degraded when replay skipped corrupt rows), the journal_corrupt_rows count, the package version, the protocol_version (the wire-protocol version, decoupled from the package version), the hub_id, the config_epoch (a fingerprint of the configuration posture the hub was built from — empty for an ad-hoc construction), the uptime_seconds since start, and the current online-agent and active-claim counts. version and config_epoch together are the hub's pinning indicator: a change in either is a deploy or a config drift; protocol_version changes only on a wire-incompatible one.

render_prometheus(metrics)

Render metrics in the Prometheus text exposition format.

Each metric emits a # HELP line, a # TYPE line, and one sample line.

Parameters:

Name Type Description Default
metrics Iterable[Metric]

The samples to render, in order.

required

Returns:

Type Description
str

The exposition text, terminated by a trailing newline.

addresses_project(target, project)

Return whether a message to target reaches any agent in project.

Matches a broadcast, the project name itself, and any <project>/... identity or group glob — so a returning terminal catches up everything for its repo regardless of which instance id it now runs as.

Parameters:

Name Type Description Default
target str

The recipient field of a message.

required
project str

The project (repo) name, e.g. "quantum".

required

Returns:

Type Description
bool

True for a broadcast, target == project, or any project/... part.

build_envelope(sender, msg_type, *, target='all', payload='', now=None, **extra)

Build the agent-side message envelope sent to the hub.

Parameters:

Name Type Description Default
sender str

Name of the sending agent.

required
msg_type str

One of the :class:MessageType constants.

required
target str

Recipient agent name, or "all" for a broadcast. Defaults to "all".

'all'
payload str

Free-form text body of the message.

''
now float or None

Override timestamp, in seconds. None uses the system clock.

None
**extra Any

Additional protocol fields (e.g. task_id, limit) merged into the envelope after the base fields.

{}

Returns:

Type Description
dict[str, Any]

A JSON-serialisable envelope ready to hand to json.dumps.

is_directed(target, name, roles=())

Return whether target names name specifically rather than broadcasting.

Stricter than :func:is_recipient in two ways: "all" (and an empty target) is not a match, and a target part must match the full name (or one of its roles) rather than its bare project. So a bare <project> directs the waiter armed as that project, but is a routine broadcast — not a wake — for a <project>/<seat> sub-seat. A reader uses this to wake only on messages addressed to it, a role it holds, or a group glob it is in, and to treat both all broadcasts and project-level traffic as read-when-convenient.

This is the WAKE question, deliberately distinct from the INBOX question (:func:is_recipient): a sub-seat still receives a bare-project message, it just is not woken by one (which is why a multi-seat project's convene traffic no longer wakes every seat). A sole agent that wants project-addressed messages to wake it arms --for <project> (the bare project), not a <project>/<id> sub-identity. A message to a role the waiter holds is a directed wake, so addressing a role reaches its current holder promptly.

Parameters:

Name Type Description Default
target str

The recipient field.

required
name str

The reader's own agent name.

required
roles Iterable[str]

Additional full <project>/<role> names this identity answers to. A directed target that matches one of them wakes the holder. Empty by default.

()

Returns:

Type Description
bool

True only when a non-broadcast target part matches name or one of its roles directly (an exact name/role or a glob covering it); a bare project matches a waiter named exactly that project, not its sub-seats.

is_recipient(target, name, roles=())

Return whether name is an addressee of a message sent to target.

The hub broadcasts every chat to every connected client and carries the intended recipient in target; a reader uses this predicate to keep only the messages meant for it.

Beyond its own name, an identity may also answer to one or more roles — full <project>/<role> names it has bound (e.g. "quantum/coordinator") — so a message addressed to a role reaches whichever instance currently holds it. A role is matched exactly like a name (a case-sensitive glob), so a role target is exact and a group glob still covers it.

Parameters:

Name Type Description Default
target str

The recipient field: the broadcast keyword "all" (or empty), a single name, a comma-separated list, or a glob such as "quantum/*" (every agent in the quantum project) or "quantum/claude-*".

required
name str

The reader's own agent name, e.g. "quantum/claude-7f3a".

required
roles Iterable[str]

Additional full <project>/<role> names this identity answers to. Empty by default, which preserves plain name/project matching.

()

Returns:

Type Description
bool

True for a broadcast or when name, its bare project, or one of its roles matches one of the target parts (each part is matched as a case-sensitive glob, so a plain name is exact). A bare project target also reaches that project's <project>/... agents, so a message to "quantum" addresses "quantum/claude-7f3a" — keeping this consistent with :func:addresses_project, so a sole agent armed under a <project>/<id> identity still receives project-addressed messages.

system_message(payload, *, hub_id, msg_type=MessageType.SYSTEM, target='all', now=None, **extra)

Build a hub-originated system message.

Parameters:

Name Type Description Default
payload str

Human-readable body of the system message.

required
hub_id str

Identifier of the emitting hub, stamped into the envelope.

required
msg_type str

One of the hub-side :class:MessageType constants. Defaults to :attr:MessageType.SYSTEM.

SYSTEM
target str

Recipient agent name, or "all" for a broadcast. Defaults to "all".

'all'
now float or None

Override timestamp, in seconds. None uses the system clock.

None
**extra Any

Additional fields (e.g. task_id, online_agents, snapshot) merged into the envelope after the base fields.

{}

Returns:

Type Description
dict[str, Any]

A JSON-serialisable envelope with sender set to :data:SENDER_HUB.

wakes(target, name, *, directed_only, sender='', priority=False, roles=())

Return whether a chat to target should wake a waiter listening for name.

In the default mode any recipient match wakes (:func:is_recipient). In directed-only mode only a directed match wakes (:func:is_directed) — except that a priority-flagged message, or one from a :data:PRIORITY_SENDERS sender, also wakes when it still reaches this waiter (a broadcast, or a message addressed to it). So an "all" broadcast that genuinely matters (a CEO directive, a flagged announcement) reaches a quiet waiter promptly, while routine peer broadcasts stay suppressed — and a priority or CEO message directed to a different agent does not wake one it was never addressed to. Directed-only means "no routine broadcast wakes me", not "every priority message anywhere wakes me".

A message addressed to a role this waiter holds is a directed wake, so a directed-only waiter still wakes promptly when its role — not just its instance name — is addressed.

Parameters:

Name Type Description Default
target str

The recipient field of the message.

required
name str

The waiter's own identity.

required
directed_only bool

When True, suppress routine broadcasts (wake only on a directed, priority, or priority-sender message).

required
sender str

The message's sender, matched against :data:PRIORITY_SENDERS.

''
priority bool

Whether the message carries an explicit priority flag.

False
roles Iterable[str]

Full <project>/<role> names this waiter answers to; a directed message to one of them wakes it. Empty by default.

()

Returns:

Type Description
bool

Whether the waiter should wake on this message.

decode_lite(lite)

Reconstruct a full message envelope from a compact relay event.

Version-2 rows retain structured payloads and non-core fields. Legacy version-1 rows keep their historical string-payload semantics and have no extension data to reconstruct. Because both versions store timestamps as millisecond integers, the reconstructed timestamp is precise only to the millisecond. Missing or malformed short keys fall back to the same core defaults :func:encode_lite emits.

Parameters:

Name Type Description Default
lite dict[str, Any]

A short-key envelope as produced by :func:encode_lite or a historical version-1 encoder.

required

Returns:

Type Description
dict[str, Any]

A full envelope with the core fields and, for a version-2 row, every non-core field encoded under x.

encode_lite(message)

Pack a full Synapse message into a short-key relay envelope.

Parameters:

Name Type Description Default
message dict[str, Any]

A full message envelope as produced by :func:synapse_channel.core.protocol.build_envelope.

required

Returns:

Type Description
dict[str, Any]

A mapping with compact keys: v (version), i (message id), ty (type), s (sender), to (target), p (payload), t (millisecond timestamp), h (hub id), c (channel), and optional x (all non-core fields). JSON payload types are retained. Malformed timestamp and msg_id fields fall back to the current time and zero, respectively.

paths_overlap(a, b)

Return whether two declared paths cover any common file.

Two paths overlap when, after normalisation, they are equal or one is an ancestor directory of the other. The empty (root) path covers the whole tree and therefore overlaps everything.

Parameters:

Name Type Description Default
a str

Declared file or directory paths.

required
b str

Declared file or directory paths.

required

Returns:

Type Description
bool

True if the paths share at least one file.

scopes_conflict(worktree_a, paths_a, worktree_b, paths_b)

Return whether two claim scopes contend for the same files.

Scopes in different worktrees never conflict. Within the same worktree, an empty path set means the claim owns the whole tree (conflicts with any other claim there); otherwise the scopes conflict when any declared path of one overlaps any declared path of the other.

Parameters:

Name Type Description Default
worktree_a str

Worktree labels of the two claims.

required
worktree_b str

Worktree labels of the two claims.

required
paths_a Sequence[str]

Declared paths of the two claims (empty means the whole worktree).

required
paths_b Sequence[str]

Declared paths of the two claims (empty means the whole worktree).

required

Returns:

Type Description
bool

True if the two scopes contend for at least one file.

__getattr__(name)

Resolve a public name on first access and cache it on the package.