Runs and events
How one attempt at a task is recorded, and how status streams back from the machine that did the work
Not built yet. There is no Run table and no Event table. packages/database still ships a
single stub model inherited from the template this repository forked. This page describes the
intended shape, taken from spec/01-domain-model.md and spec/02-agent-interfaces.md.
Nothing executes inside this application. Work happens on a remote workstation running Claude Code or Codex. A Run is the record of one such attempt, and the Event timeline is how that attempt becomes visible to everyone else.
Run
One attempt at a Task by an Agent on a Machine. A Task has many Runs; a Run has exactly one Task. Runs are how "what actually happened" is reconstructed after the fact, including the attempts that failed.
| Field | Notes |
|---|---|
agentId, machineId | Who attempted it and where. |
startedAt, endedAt | Server timestamps, never client clocks. |
status | running, succeeded, failed, abandoned, cancelled. |
exitReason | Why it ended, in machine-readable form. |
branch, prUrl | The work product in the repository. |
tokensIn, tokensOut, costUsd | What the attempt cost. |
logArtifactId | The full log, stored as an Artifact rather than inlined. |
abandoned is the status a Run reaches without anyone reporting it. A runner that stops
heartbeating past the lease TTL is assumed dead, not done: the lease expires, the Run is marked
abandoned, and the Task returns to todo. See
approvals and leasing.
Event
An append-only timeline. Every meaningful thing that happens writes one row.
| Field | Notes |
|---|---|
actorType, actorId | user, agent or system, plus the identity. |
verb | task.claimed, run.started, artifact.attached, approval.requested, and so on. |
taskId, runId | What the event is about. |
payload | jsonb, the details of that verb. |
at | Server timestamp. |
Events are immutable. They are never updated and never deleted. One table drives three things: the realtime UI feed, the audit log, and agent catch-up.
Every state transition writes an Event inside the same transaction as the write it describes, so partial state is never observable. If the Event is missing, the write did not happen.
Catch-up and resumption
Because Event ids are sortable ULIDs, "everything after this point" is unambiguous. An agent that crashed and restarted asks for events since the last one it saw, rather than re-reading the whole task. That single query is what makes a runner resumable without any in-memory state:
whoami → open leases → tasks_get → events_stream?since=<lastEventId> → back to workComments
A Comment is a threaded conversation on a Task between humans and agents. Agents post progress
notes there, and questions. It is the human-readable channel next to the machine-readable Event
timeline. A comment posted with blocking: true and left unanswered moves the Task to blocked.
The write-back contract
The product depends on remote runners reporting honestly. The minimum a runner sends over the course of one Run:
tasks_claimto get the lease and the working context.runs_heartbeatat least everyleaseTtl / 3, carrying a progress percentage and a one-line human-readable status.comments_poston any decision a human would want to see, and on every blocker.artifacts_attachfor the diff, the pull request link, and any screenshot proving the work.runs_finishwith the true outcome: the real exit reason, the real usage numbers, and acceptance criteria checkboxes that reflect what was actually verified.documents_upsertfor anything the next agent would otherwise have to rediscover.
A failed run reported as failed is worth more than a green lie. Any feature that makes it easier to report success than to report a blocker is a design defect, and reviewers reject it on that basis.
How events leave the system
Three planned paths, in order of preference:
- Server-sent events.
GET /api/v1/events/streamfor REST, and a GraphQLeventAddedsubscription overgraphql-sse. Both take the samesincecursor. - Webhooks. A
WebhookSubscriptionholds aurl, anevents[]filter, a signingsecret, and a delivery log with retries. This is how a remote runner gets woken instead of polling for work. - Polling. A fallback, not the design.
Details of each door are in the REST interface and the GraphQL interface.