openstation

Reference

Events

Inspect event types and the audit log.

OpenStation publishes structured events on an in-process bus and appends them to an append-only log under var/events/. The log is how you find out what an agent did without reading a chat transcript.

Reading the log

bun packages/cli/src/index.ts my-space events -n 20

One line per event: timestamp, type, then the fields that identify it. Real output from a turn where the agent filed a note:

2026-07-25T07:00:02.256Z  workspace.CommitMade     sha=1ff82210023d5c3c77d9220b261442ae859182a8 filesChanged=2 message=agent: workspace update

Filter by exact type:

bun packages/cli/src/index.ts my-space events -t workspace.CommitMade

An empty log prints no events recorded yet.

What is and isn't recorded

Every turn publishes, chat included. turn.TurnStarted and turn.TurnFinished come from the Bridge on the request path as well as from the scheduler and the trigger dispatcher, so a conversation leaves a trace even when it changes nothing. That matters most for a silent: true space: it spends money on every message and, having nothing to say and possibly nothing to commit, used to leave no evidence anywhere.

A turn the runner refused (at_capacity, conversation_busy) is still reported finished, with the refusal as its stoppedReason — otherwise "no event" would mean both "never happened" and "turned away". A message that never became a turn publishes nothing, because no turn existed: an unauthorized sender, a space whose policy does not admit it, a bridge.reset that dropped the session ref, or a message a bridge.inbound stage answered itself. A stage that failed is the exception — see bridge.StageFailed.

What you will actually see, by source:

Source Events
The Bridge (a chat turn) turn.TurnStarted / turn.TurnFinished
A turn that changed the workspace workspace.CommitMade or workspace.CommitVetoed
The file watcher workspace.FileChanged
The trigger dispatcher trigger.TriggerFired, plus turn.TurnStarted / turn.TurnFinished
The scheduler turn.TurnStarted / turn.TurnFinished
The approval gate approval.ApprovalRequested, approval.ApprovalResolved
A bridge.inbound stage that failed bridge.StageFailed

Event types

Every event carries source (who produced it) and ts (an ISO timestamp, stamped by the producer). The fields below are in addition to those.

turn.TurnStarted

A turn was handed to the TurnRunner.

Field Type
scope string — the conversation scope key
agent string

turn.TurnFinished

A turn returned.

Field Type
scope string
agent string
ok boolean
stoppedReason string, optional
durationMs number

workspace.FileChanged

A file in the workspace's artifacts zone changed — what the trigger dispatcher watches.

Field Type
path string
changeType created | modified | deleted
artifactType string, optional — the artifact's declared kind

artifactType is the type: field of the file's YAML frontmatter, which OKF requires on every artifact. The watcher reads a capped head of the file (4 KB, markdown only) at the moment it sees the change, so the event says what changed and not only where:

2026-07-26T09:12:44.101Z  workspace.FileChanged  path=okf/issues/b2c2-heartbeat.md changeType=modified artifactType=Issue

It is absent, not empty, in four cases: a deletion (nothing left to read), a non-markdown file, markdown with no frontmatter, and an unreadable file. That absence is load-bearing — an OKF index.md or log.md carries no frontmatter, so it can never match a trigger that names an artifactType:, which is how a rule watches real artifacts without listing the store's own bookkeeping files. See openstation.yaml.

trigger.TriggerFired

A triggers: rule matched an event and dispatched a turn.

Field Type
on string — the rule's subscription
agent string
matchedType string — the event type that matched

workspace.CommitMade

The agent's work was committed.

Field Type
sha string
filesChanged number
message string

workspace.CommitVetoed

Validation refused a commit. Recorded so a veto is auditable — the commit that did not happen is often what you need to see.

Field Type
reason string
filesChanged number

approval.ApprovalRequested

A turn was held for a human decision.

Field Type
id string
scope string
reason string

approval.ApprovalResolved

A held turn was approved, denied, or expired.

Field Type
id string
scope string
status approved | denied | expired

bridge.StageFailed

A declared bridge.inbound stage produced no usable result. The message still becomes a turn — this event is what keeps a broken stage distinguishable from an agent that ignores people.

Field Type
stage string — the stage's run: as declared
scope string — the conversation the message arrived in, matching turn.*'s scope
reason exit | unparseable | timeout | spawn
detail string, optional — the child's own diagnostic, capped at 300 characters
2026-07-30T21:14:02.884Z  bridge.StageFailed       stage=openbook ingest --stage scope=telegram:-1001234567890 reason=spawn detail=Executable not found in $PATH: "openbook"

stage is the command as declared, never the argv it ran with — that carries the paths of files someone sent. reason=exit is the one that does not mean the stage was skipped: a non-zero exit is advisory, so the stage is reported and whatever it printed is honoured.

There is deliberately no bridge.StageRan. Stage-level tracing is observability work with no consumer, and a per-photo event on a holiday dump is noise.

On-disk format

JSONL, one file per UTC day:

var/events/YYYY-MM-DD.jsonl

Events that name an agent (turns and triggers) are also mirrored into that agent's own diary, one directory per agent:

var/events/agents/<agent>/YYYY-MM-DD.jsonl

The shared stream stays complete either way — the mirror is a per-agent view for tailing, not a partition.

One JSON object per line, matching the schemas above. Lines are schema-validated on the way back out, so a hand-edited file will be rejected on read rather than silently misparsed.

var/ is platform state and belongs outside git — the scaffolded .gitignore excludes it, and commit automation never stages it even when it isn't ignored.

Appends are fire-and-forget by contract: a log write failure must never break a turn. A failed append prints [event-log] append failed for <type>: and the turn continues, so the log is an audit aid rather than a guaranteed-complete ledger.

var/logs/ is the application log — what the process did, written by dev and serve (shared openstation.log, plus one file per agent under var/logs/agents/). This event log records what happened to the workspace. See cli.md for openstation logs.

View Markdown source on GitHub ↗