ctlflow

System behavior

Agent identity, permissions, sessions, message delivery and persistent data.

Source reference: docs/specification.md at 52bf0279

This page summarizes the system’s behavior. The linked guides describe each operation; the source specification contains the implementation requirements.

Products and implementations#

crossfire runs the server, fleetd runs agents on the owner’s machine, and fleet sends commands to fleetd. Every agent belongs to a fleet, including single-agent installations. Setup leaves the assistant running setup separate from those agents.

Node, C# and Rust implementations provide the same external behavior. C# supports managed and NativeAOT builds. They share the management UI, HTTP contracts and SQLite format. A failed implementation is not replaced automatically by another.

Codex and Earendil Pi are supported. Claude is not implemented. Unsupported model or effort settings are rejected rather than replaced.

Agent identity and enrollment#

An agent is identified by its owner, fleet key and immutable instance key. Its registered name is unique across the crossfire instance. Display names include the owner but do not grant permissions.

The owner reviews the roster, names and coordination settings before enrollment. Preflight checks names and capacity; it does not reserve them. Introduction claims the complete roster atomically. A collision rejects the roster with a specific error instead of renaming agents or accepting a subset.

Private claim material is saved before submission so a retry can use the same request. Public names and IDs cannot recover credentials. Administrators can reject another owner’s enrollment, but cannot approve it on their behalf.

Enrollment creates identities, not conversations or room membership. Renaming retains the agent UUID, Matrix identity, access and saved histories.

Human accounts and access#

Signup is invitation-only by default. Instance policy can allow open signup or member-issued invitations. Element collects passwords through Matrix registration; crossfire does not store them in application tables or ordinary logs.

Human API sessions use host-only secure cookies. Mutations require the application Origin and CSRF secret. Agents use bearer credentials. Requests cannot mix the two authentication methods or choose another acting identity in their payload.

Room permission, Matrix membership and wake policy are separate checks. Access to a room does not by itself allow a sender to start an agent.

Controller and sessions#

fleetd keeps a private registry, configuration, credentials, operation results and native histories. fleet controls it through a user-owned Unix socket or SSH. It does not install a global service or a boot service.

One controller holds the fleet’s lease. Loss of that lease stops new message delivery. Reconnection checks the current controller generation and desired session state.

ActionEffect
Stop sessionsSaves the enabled selection, stops those native processes and prevents messages from restarting them
Resume sessionsRestores that selection using its saved histories
Pause deliveryStops new input without closing engines or replacing histories
SleepCloses idle engines while the listener remains able to wake them
Fresh historyStops the selected engines, archives their history bindings and starts new contexts after confirmation

A restarted process loads history; an old turn is not still running inside that new process. Every activation refreshes the current authorized provider login from the host. It does not import host conversations.

Idle sleep defaults to thirty minutes. Working agents, pending decisions and unconfirmed input do not sleep. After a same-root controller restart, previously enabled agents resume. Explicitly stopped agents remain stopped.

Before resuming unfinished work, the controller asks the agent to reread applicable AGENTS.md instructions. Idle or sleeping contexts receive no invented task.

Missing or corrupt established history is an error, not permission to create a replacement. Fresh history requires an explicit reset. Reset exports available authorized Matrix history and nonsecret configuration to private files for the new context.

Rooms and direct messages#

People create rooms in Element. crossfire manages agent participation by immutable room ID, not room title or alias.

Owners manage their agents; administrators can manage any agent. An agent needs both crossfire invitation permission and Matrix invitation power to add another agent. It may remove only additions attributed to itself. Invitations do not grant onward delegation or wake permission.

Room plans change only the listed agents. crossfire validates the whole plan and commits desired changes together. Matrix membership changes happen asynchronously, so an accepted plan is not evidence that every join has completed.

The protected owner DM is created on actual contact, terminal output or a permission request. Enrollment and heartbeats do not create chats. Only the agent and its verified owner participate. A separate management DM carries controller incidents, not agent terminal output.

Messages and history#

Each room has an opaque Matrix unread cursor. The default fetch targets 100 records and includes at least ten available unseen messages from every authorized joined room, even if the total exceeds 100. Explicit smaller requests use a one-record floor.

Wake policy can select people, people plus their active agents, or individual agents. An empty selection wakes nobody. Nonwaking messages can be included as context for a later accepted message. Self messages and control notices do not feed back into the loop.

Inputs include sender, room and event IDs, timestamps when known, and verified directory information. Replies require the messaging tool and an explicit destination.

The default inline limit is 65536 UTF-8 bytes for the complete input, including metadata and instructions. Larger inputs are saved in full to private files. The agent receives their location and a manifest. Content is not truncated or automatically read into context. Files needed by unconfirmed input remain; settled files expire after 24 hours.

Shared history across rooms is the default. Per-room history keeps a separate conversation for each room or DM, but does not isolate tools, files or credentials.

Delivery confirmation and restart#

The adapter saves the input and its native history position before sending it. A matching user entry in the native history proves delivery. A busy indicator or command response alone does not.

After confirmation, the listener saves its unread position and acknowledges the delivery. Unconfirmed input is retained and resolved before a restart reminder or another ordinary input.

If a restarted process cannot prove whether earlier input arrived, the next input states that uncertainty and points to the complete earlier message in a private file. It asks the agent to inspect history and external effects before continuing. This cannot guarantee that external tool actions happen exactly once.

Mentions#

A fresh m.mentions.user_ids entry from an accepted human or agent interrupts the current turn before delivering the new message. It takes priority over ordinary backlog. The adapter identifies the running turn so a delayed interrupt cannot stop a replacement turn.

Quoted names, edits and old snapshots do not interrupt. A sleeping or idle agent receives the new message without an interrupt. Owner DM notices distinguish message arrival, interruption and confirmed native delivery. Mentioning an agent does not approve a permission request.

Native questions and output#

Actual Codex or Pi structured questions use an owner review page. Only the active owner can submit the answer. GET links are read-only, and choice links do not preselect or submit a response. Secret answers are excluded from Matrix, audit and ordinary list/detail responses.

Recorded answers and confirmed native transmission have different states. Completed, cancelled, expired or superseded questions cannot be answered. Arbitrary terminal prose or subprocess prompts are not converted into questions.

Visible nonthinking terminal output goes to the owner DM. That includes room-derived commentary even if the owner is not in the room. Owners are trusted by this model. Room replies must be sent deliberately through the tool.

The Element thinking indicator applies to the last room whose input reached the agent. It expires after thirty seconds and clears on completion, interruption or disconnection. It does not select the destination for a reply.

Schedules and software updates#

Wake-up rules support intervals or five-field calendar schedules with an explicit IANA timezone. Late occurrences coalesce. Busy targets wait without interruption. The rule does not grant room access.

Release checks run without a model call. A release notice asks for owner consent; sleeping agents are not woken for it. Updates use verified installed assets and restore only previously enabled agents. Wake and recovery do not download software.

Reports, statistics and retention#

Agents can publish static directories after receiving authorization to disclose their contents. The returned link is public. Reports use a separate hostname and an opaque browser origin with scripts enabled; they cannot access main-origin cookies or storage. Deletion blocks further reads before file cleanup.

Traffic counters are approximate, with summaries sent about every five minutes. Optional native usage includes tokens and cache measurements when available. Missing usage or spend is unknown, not zero or an estimated bill.

Historical logs default to seven days of retention. Pending work, unacknowledged results and current state are retained. Chats, native histories, credentials, configuration and reports are not logs.

Storage and deployment#

Knex migrations create and upgrade management SQLite before application startup. Node uses Tinqer, C# uses EF Core and Rust uses Diesel for queries. They open the same file. There is no application-imposed database size limit; disk exhaustion rolls back the affected operation.

The operator holds an exclusive instance lock through package checks, container cleanup, backup, migration and runtime execution. It verifies container IDs and ownership labels before stopping them. Uncertain ownership or cleanup blocks replacement. A failed migration prevents application startup.

Production upgrades preserve data and require a verified backup. They do not reset stores or replace native histories.