Reference
Runtime daemon
Connect a local client to the durable task scheduler, frame commands, resume events, and recover interrupted tasks.
View as textOn this page
The daemon is a per-user Unix-socket service for client developers. It is not the HTTP server, and its protocol is not newline-delimited JSON. The shipped runner launches one-shot CLI processes; an embedding application can supply a typed TaskRunner instead.
Start and locate the daemon
nix shell .#agent-cli .#agent-runtime-daemon
agent-runtime-daemonThe endpoint is ~/.haskell-agent/runtime/daemon.sock. Set HASKELL_AGENT_RUNTIME_DIR before startup to relocate its directory; journal storage is beside the socket. HASKELL_AGENT_CLI selects the executable used for tasks, otherwise agent-cli is found on PATH. Keep the daemon in the foreground while developing a client. Stop it with Ctrl+C before changing its configuration.
The runtime directory is mode 0700 and socket mode 0600. Connections are authenticated using Unix peer credentials. A symlinked directory, foreign-owned socket or existing non-socket endpoint is rejected. Do not fix startup by making the directory public or deleting an unrelated live socket. An exclusive lock serializes listener startup and stale-socket cleanup.
Framing and handshake
Encode each message as UTF-8 JSON, prefixing its byte length as an unsigned four-byte big-endian integer. The default maximum frame is 1 MiB. A plain echo or HTTP request is not a valid client. Send this object as the first framed message; keep a stable client ID:
{"type":"hello","hello":{"clientId":"documentation-client","versions":[3],"resumeAfter":null}}Version 3 is currently supported. Expect welcome with negotiated version, current sequence and heartbeat interval, or version_rejected. Do not send task commands before completing negotiation.
Snapshots, events and reconnect
- Without a cursor, receive a snapshot. With
resumeAfter, receive retained events after the last durably applied sequence. - For
snapshot_chunkorevent_chunk, decode each base64 slice, concatenate by zero-based chunk index, then parse the combined JSON. Do not parse each slice as a whole snapshot. - Apply events in sequence order, persist your cursor, then send
{"type":"ack","sequence":42}. - Answer a heartbeat with
{"type":"pong","sequence":42}using its sequence. Reconnect with the last applied cursor after disconnection.
A cursor outside retention receives a fresh snapshot. Replace stale local state before applying subsequent events. Queues and reads/writes have bounds and deadlines; a slow client can be disconnected. Acknowledging data before durably applying it can make your client skip state after a crash.
Command reference
Wrap every command in a client-generated command ID. For a read-only first check:
{"type":"command","id":"list-1","command":{"version":1,"type":"list"}}The reply is command_result correlated with that ID. Successful task results contain version: 1. Unknown command versions/types, invalid fields and a full command queue fail without changing scheduler state.
| Type | Fields | Behavior |
|---|---|---|
submit | task_id, prompt, cwd; optional session_id, provider, model, effort, worktree | Create a task. Task IDs cannot be reused. Tasks resuming one session are serialized. |
cancel | task_id | Cancel queued or executing work; return durable task state. Completed side effects are not reversed. |
list | None | Return retained durable tasks, not an unbounded historical archive. |
set_limit | limit | Set concurrency to an integer from 1 through 32. |
retry | task_id | Retry eligible failed/cancelled/interrupted work only while original input remains in this process. |
approval | task_id, approval_id, decision | Recognized but unsupported by the shipped runner; fails visibly and never resolves approval. |
Submit a bounded task
{"type":"command","id":"submit-1","command":{"version":1,"type":"submit","task_id":"inspect-1","prompt":"Describe the top-level files. Do not modify anything.","cwd":"/absolute/path/to/project","worktree":false}}Replace the project path. Provider and model must either both be present or both absent. Worktree creation is valid only for a fresh session. Task/session/approval IDs have a 256-character maximum, prompt 8,192 and path 4,096. An omitted session means a fresh session. A successful mutation is journaled as task_changed before the command reports success.
The runner executes direct arguments without a shell, closes stdin and always passes --no-yolo. A task requiring interactive approval fails closed. The accepted approval decision spellings (approve, deny, approve_session) do not make the unsupported approval command functional. Use an interactive client for work requiring a human decision.
Output and execution limits
Process tasks have a six-hour wall-clock deadline. Stdout and stderr are drained concurrently with bounded output and log storage. Descendants retaining pipes after the leader exits are subject to bounded TERM/KILL cleanup. A saturated log queue records [output truncated: scheduler log queue was full]; missing log text is not evidence that the corresponding operation did not occur.
Restart and retry safely
Snapshots and events are durably flushed. On startup queued/running tasks become interrupted; they are never automatically rerun. Original unredacted prompts are retained only in memory for eligible retries, not persisted as recoverable task input. After restart, retry reports task input is unavailable. Review filesystem and external-service outcomes, then submit the intended input under a new task ID.
Completed or active tasks cannot be retried. Completion discards their raw input. Corrupt/oversized recovery data and invalid journal ownership fail startup closed. Stop the service and preserve the journal for diagnosis; do not edit sequence numbers or erase evidence merely to make startup succeed. Back up the journal only when the writer is stopped or through a coordinated filesystem snapshot.
Embedding and verification
A read-only Python client
Save this as inspect-daemon.py and run it with Python 3 and the socket path as its sole argument. Start the daemon first under the same user. It negotiates version 3, requests a task list, services heartbeat messages and exits on the matching command result. It does not submit model work or resolve approvals. This is a source-reviewed protocol example, not a recorded live run.
import json, socket, struct, sys
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as sock:
sock.settimeout(15)
sock.connect(sys.argv[1])
def send(value):
data = json.dumps(value).encode('utf-8')
sock.sendall(struct.pack('>I', len(data)) + data)
def exact(size):
data = bytearray()
while len(data) < size:
part = sock.recv(size - len(data))
if not part:
raise EOFError('daemon disconnected')
data.extend(part)
return data
def receive():
size, = struct.unpack('>I', exact(4))
if not 0 < size <= 1024 * 1024:
raise ValueError('invalid frame length')
return json.loads(exact(size))
send({'type': 'hello', 'hello': {'clientId': 'docs-inspection',
'versions': [3], 'resumeAfter': None}})
welcome = receive()
if welcome['type'] != 'welcome' or welcome['welcome']['version'] != 3:
raise RuntimeError(welcome)
send({'type': 'command', 'id': 'list-1',
'command': {'version': 1, 'type': 'list'}})
for _ in range(10000):
message = receive()
if message['type'] == 'heartbeat':
send({'type': 'pong', 'sequence': message['sequence']})
elif message['type'] == 'command_result' and message['id'] == 'list-1':
print(json.dumps(message, indent=2))
if not message['ok']:
raise SystemExit(1)
break
else:
raise RuntimeError('no list result within message bound')The example intentionally discards replay/snapshot messages: it only prints the independent list result, so it sends no replay acknowledgments and must not save a resume cursor. A stateful client must assemble chunks and apply snapshots and events before acknowledging their sequence. Never acknowledge data merely because bytes arrived. Timeout/EOF is not proof that an earlier mutation failed; reconnect and inspect its task ID before retrying.
Supervise one writer per user
Use a per-user service manager with a pinned daemon executable, a fixed HASKELL_AGENT_CLI path, the same HOME and socket configuration as clients, and a restart-on-failure policy. Do not run a second writer against the same socket/journal. Preserve the owner-only directory and socket modes rather than making the socket group/world writable to fix access failures.
Before an upgrade, stop admission in clients, wait for tasks to settle or cancel and inspect them, stop the daemon, back up its complete journal directory, then replace the pinned executable and restart. Verify handshake and the read-only list request before admitting work. Roll back executable and journal together only when no newer writes must be preserved; interruption is not automatic task replay.
Haskell clients can use the types in Agent.Runtime.Daemon.Protocol. An embedder can supply TaskRunner while retaining scheduling, persistence and wire protocol. There is intentionally no approval-resolver hook in this adapter. Pin the package revision and test handshake, reconnect, cancellation, full queues, interrupted recovery and unsupported approval before shipping a client. This reference is source-verified; it is not a live daemon interoperability report.