Skip to content

Event Streaming

How CompozyOS records session events, streams them over SSE, and persists them in per-session SQLite databases.

For people running agent work6 pages in this section

Events are the durable record of a session. They drive:

  • live session views in clients
  • replay and transcript reconstruction
  • turn history
  • token usage tracking
  • permission audit trails

CompozyOS stores session events in a per-session SQLite database and exposes them through both query and SSE surfaces.

Two event surfaces

CompozyOS exposes two different streaming models for session work:

SurfaceEndpointWhat you get
Persisted session streamGET /api/workspaces/:workspace_id/sessions/:session_id/streamAssembled transcript frames by default; raw stored event rows when frames=raw is supplied
Prompt streamPOST /api/workspaces/:workspace_id/sessions/:session_id/promptRaw live prompt events in the AI SDK UI stream format (x-vercel-ai-ui-message-stream: v1)

Use the persisted session stream when you need reconnect support or a live transcript projection. Use frames=raw on that stream when you need audit-grade event rows. Use the prompt stream when you are driving one interactive prompt call.

Prompt identity and safe retries

Every prompt and steer request requires two caller-provided values:

  • message_id identifies the authored user message. It is the durable UIMessage.id for that transcript entry, so clients must keep it when they reconnect or reconcile optimistic state.
  • idempotency_key identifies this submission. It lets CompozyOS decide whether a request is the same command or a conflicting reuse of an existing command identity.

For an AI SDK request, message_id must equal the latest user message's id; if top-level message is also supplied, it must equal that message's text. The API rejects missing or mismatched identity with 400.

Repeat an uncertain request with the exact same message_id, idempotency_key, operation, and payload. When its durable result is available, CompozyOS returns the same wrapped prompt result:

{
  "prompt": {
    "status": "queued",
    "message_id": "msg_1234",
    "idempotency_key": "idem_1234",
    "replayed": true,
    "queued": true,
    "queue_entry_id": "queue_1234"
  }
}

Ordinary queued, staged, and replayed prompts use 202 Accepted. Goal-command replays preserve the original Goal response status, such as 200, 404, 409, or 422. No replay starts a second prompt stream or repeats the accepted side effect. A client should reconcile the existing transcript message by its exact message_id, rather than append another user row.

409 Conflict means the identity cannot safely be reused: the idempotency key is bound to a different request, the message ID is bound to a different idempotency key, or the earlier dispatch is indeterminate. For an indeterminate dispatch, do not blindly resend with either the old or new identities. Inspect the session and decide with the operator whether the original work happened; only then submit a deliberate new command with new identities if that is the chosen action.

Outer persisted event envelope

Every stored event row is returned as SessionEventPayload:

{
  "id": "evt-000042",
  "session_id": "sess-1234",
  "sequence": 42,
  "turn_id": "turn-9abc",
  "type": "tool_result",
  "agent_name": "general",
  "workspace_id": "repo-alpha",
  "workspace_path": "/absolute/path/to/repo",
  "content": {
    "schema": "compozy.session.event.v1",
    "type": "tool_result",
    "session_id": "acp-session-77",
    "turn_id": "turn-9abc",
    "timestamp": "2026-04-16T01:10:06Z",
    "tool_call_id": "tool-call-1",
    "tool_name": "Read",
    "tool_result": {
      "content": "package session"
    },
    "tool_error": false,
    "prompt_runtime": {
      "provider": "codex",
      "model": "gpt-5.6-sol",
      "reasoning_effort": "high",
      "speed": "fast"
    },
    "raw": {
      "status": "completed"
    }
  },
  "timestamp": "2026-04-16T01:10:06Z"
}

Two IDs matter here:

  • outer session_id: the stable CompozyOS session ID
  • inner content.session_id: the ACP runtime session ID carried by the underlying event payload

Those IDs can diverge after a resume that falls back to a fresh ACP session.

Event type catalog

The persisted event type values currently emitted by CompozyOS are:

TypeWhen it is recordedKey content fields
user_messageAfter CompozyOS has bound or reconfigured the selected runtime, before it submits a prompt to ACPschema, type, session_id, turn_id, timestamp, text, message_id, attachments, prompt_runtime
agent_messageAgent text chunktext, plus the shared canonical fields
thoughtAgent reasoning chunktext, plus the shared canonical fields
tool_callTool call started or updated before completiontitle, tool_name, tool_kind, tool_call_id, optional tool_input, optional raw
tool_resultTool call completed or failedtool_call_id, tool_name, tool_result, tool_error, optional raw
planACP plan updateoptional raw; CompozyOS stores the canonical envelope even when the payload is sparse
permissionACP permission request or resolved decisionrequest_id, action, resource, decision, title, tool_call_id, raw
usageToken or context usage updateusage with token counts, context counts, cost fields, and timestamp
runtime_progressLow-frequency long-running prompt progress updatetext, runtime activity payload
runtime_warningRuntime supervision warning or inactivity timeout noticetext, runtime activity payload, optional raw
session.compaction_firedPressure compaction admitted for a complete prior-turn sequence spanraw with workspace/session/turn IDs, sequence bounds, usage, pressure, strategy
transcript_marker.createdDurable runtime evidence added to the transcriptmarker kind, summary, occurrence time, and optional evidence
transcript_marker.redactedDurable marker whose diagnostic evidence was redactedmarker kind, summary, occurrence time, and redaction metadata
systemAvailable-command updates, mode updates, or other non-chat ACP system updatestitle, optional raw
doneEnd of a prompt turnstop_reason, optional usage, optional raw
errorPrompt processing errorerror, optional raw
session_stoppedSession finalizationstop_reason, optional failure, optional error, optional text

Runtime supervision events

runtime_progress and runtime_warning are separate from agent_message. They are progress projections for clients and bridges, not assistant-authored text to append to the final answer.

CompozyOS does not write heartbeat ticks into the event log. Heartbeats update session liveness metadata; only lower-frequency progress, warning, and timeout notices become stored events.

Example runtime_progress content:

{
  "schema": "compozy.session.event.v1",
  "type": "runtime_progress",
  "turn_id": "turn-9abc",
  "timestamp": "2026-04-16T02:10:06Z",
  "text": "Still working... (10 min elapsed)",
  "runtime": {
    "turn_id": "turn-9abc",
    "turn_source": "user",
    "last_activity_at": "2026-04-16T02:10:06Z",
    "last_activity_kind": "agent_waiting",
    "last_activity_detail": "waiting for session/prompt response",
    "current_tool": "Bash",
    "tool_call_id": "tool-call-1",
    "last_progress_at": "2026-04-16T02:10:06Z",
    "idle_seconds": 0,
    "elapsed_seconds": 600
  }
}

runtime_warning is emitted once when session.supervision.inactivity_warning_after is crossed. If session.supervision.inactivity_timeout is crossed, CompozyOS cancels the active prompt cooperatively. If the prompt does not finish within timeout_cancel_grace, CompozyOS stops the session with stop reason timeout and stop detail activity_timeout.

Shared canonical fields inside content

The stored canonical envelope can include these fields when they apply:

FieldMeaning
schemaCurrently compozy.session.event.v1
typeInner event type
session_idACP session ID from the runtime event
turn_idPrompt turn ID
message_idDurable authored user-message identity; it becomes the transcript UIMessage.id
attachmentsByte-free attachment metadata: ID, name, MIME, byte count, digest, kind, and optional dimensions; clients derive the scoped URI from the ID
request_idPermission request ID
timestampEvent timestamp
textUser text, assistant text, or thought text
titleTool or update title
tool_nameDerived tool name
tool_kindCanonical ACP tool kind, including edit for file-mutation reduction
tool_call_idStable tool correlation ID
tool_inputStructured tool input when present
tool_resultStructured tool output (stdout, stderr, file_path, content, structured_patch, error, raw_output)
tool_errorWhether CompozyOS classified the tool result as failed
stop_reasonTurn or session termination reason
failureTyped lifecycle failure object with kind, redacted summary, and optional crash_bundle_path
actionPermission action name
resourcePermission resource target
decisionPermission decision
errorError text
usageToken and cost payload
runtimeLong-running prompt activity payload with current tool, last activity, progress, idle, and elapsed fields
prompt_runtimeImmutable prompt-bound runtime snapshot: provider, model, reasoning_effort, and speed
rawRaw ACP update payload CompozyOS preserved

prompt_runtime is carried by every persisted turn event, including the authored user_message. It records the runtime that was accepted for that prompt, not the session's current binding at read time. Queue and interrupt inputs preserve their accepted snapshot until dispatch. If runtime binding, replacement, or rollback fails, CompozyOS does not persist the authored user_message.

CompozyOS records the admitted local user input itself. It drops ACP-provider user_message_chunk echoes before they reach the persisted event stream or transcript, so a provider echo cannot create a second user message. Steering is a daemon-owned replacement prompt fenced to the expected active turn; it is not injected into a provider tool-result boundary.

Usage cost and provenance

Read a session's aggregate token usage from GET /api/workspaces/{workspace_id}/sessions/{session_id}/usage. Token counts and monetary status are independent: CompozyOS keeps counting tokens when cost is included in a subscription or cannot be determined.

For agent-managed inspection over UDS, run compozy session usage <session-id> -o json. The command returns the same usage payload; omit -o json for the operator-readable panel.

The response uses cost_status to say what total_cost means:

StatusMeaningAmount
actualThe agent reported the cost.Present when reported
estimatedCompozyOS multiplied reported token counts by merged model-catalog rates.Present with cost_currency
includedA native_cli provider owns subscription billing.Omitted
unknownA required rate is missing or aggregate provenance/currency conflicts.Omitted

cost_source identifies agent_reported, catalog_config, models_dev, builtin, or none. CompozyOS never adds an agent-reported amount to an estimate. Aggregates keep a monetary amount only when status, source, and currency agree; token totals continue even when the monetary aggregate becomes unknown.

Estimates price input, output, cache-read, cache-write, and reasoning tokens independently. Every nonzero bucket requires its own finite, non-negative catalog rate; CompozyOS never substitutes an input rate for cache tokens or an output rate for reasoning tokens.

included is a billing classification, not a provider account-balance check. CompozyOS does not read native CLI credential stores or expose provider subscription quotas.

Querying stored events

Read recent events from the CLI:

compozy session events sess-1234 --last 20

Filter to one type:

compozy session events sess-1234 --type tool_call

Group by turn:

compozy session history sess-1234

The HTTP query surfaces accept these filters:

  • type
  • agent_name
  • turn_id
  • since
  • limit
  • after_sequence
  • archive (active, archived, or all)

For HTTP, since must be RFC3339 or RFC3339Nano. The CLI is more convenient: compozy session events --since 5m converts the duration into an absolute UTC timestamp before calling the API.

events defaults to the newest 200 matching rows. history defaults to the newest 200 grouped turns after grouping all matching rows. Both are capped at 1000 by limit and reject before_sequence.

The transcript endpoint accepts limit and before_sequence. It returns one bounded chronological page whose stable start cursors are separate from the latest event sequence that shaped each entry:

{
  "entries": [
    {
      "message": {
        "id": "msg-42",
        "role": "assistant",
        "parts": [{ "type": "text", "text": "Done.", "state": "done" }]
      },
      "start_sequence": 40,
      "sequence": 42
    }
  ],
  "epoch": 3,
  "generation": 7,
  "max_sequence": 42,
  "has_older": true,
  "next_before_sequence": 40,
  "limit": 200
}

Use next_before_sequence as the next request's before_sequence while has_older is true. start_sequence is the stable backward-paging cursor; sequence can advance when later events update the same logical entry. The endpoint rejects forward cursors and event-row filters because forward transcript changes belong to the fenced SSE stream.

Subscribing over SSE

Follow persisted events from the CLI:

compozy session events sess-1234 --follow

compozy session events --follow requests frames=raw, because the CLI command is an operational event reader.

Open the default transcript SSE stream directly:

curl -N 'http://localhost:2123/api/workspaces/ws_alpha/sessions/sess-1234/stream?limit=200'

The default stream writes:

  • id: as the transcript cursor sequence
  • event: transcript_snapshot for the initial bounded transcript window or an explicit fenced reset
  • event: transcript_delta for a bounded batch of materialized entry upserts after the current cursor
  • event: session_stopped when a stopped session has no more rows to deliver
  • event: error when the stream fails before it can continue; this frame is terminal

Example transcript snapshot frame:

id: 42
event: transcript_snapshot
data: {"session_id":"sess-1234","workspace_id":"ws_alpha","epoch":3,"generation":7,"entries":[{"message":{"id":"msg-42","role":"assistant","parts":[{"type":"text","text":"Done.","state":"done"}]},"start_sequence":40,"sequence":42}],"max_sequence":42,"has_older":true,"next_before_sequence":40,"reset":false}

Open the raw stored-event stream when you need event rows:

curl -N 'http://localhost:2123/api/workspaces/ws_alpha/sessions/sess-1234/stream?frames=raw'

Raw frames write:

  • id: as the persisted event sequence number
  • event: as the persisted event type
  • event: error when the stream fails before it can continue; this frame is terminal
  • data: as one SessionEventPayload

Example raw SSE frame:

id: 42
event: tool_result
data: {"id":"evt-000042","session_id":"sess-1234","sequence":42,"turn_id":"turn-9abc","type":"tool_result","agent_name":"general","content":{"schema":"compozy.session.event.v1","type":"tool_result","tool_call_id":"tool-call-1","tool_result":{"content":"package session"}},"timestamp":"2026-04-16T01:10:06Z"}

Reconnect with transcript fences

Reconnect from a cursor only with the epoch and generation returned by the page or snapshot that owns it:

curl -N \
  -H "Last-Event-ID: 42" \
  'http://localhost:2123/api/workspaces/ws_alpha/sessions/sess-1234/stream?epoch=3&generation=7&limit=200'

Last-Event-ID must be a non-negative numeric transcript cursor. With matching fences, CompozyOS emits bounded transcript_delta batches. A missing or stale fence, or a cursor beyond the current projection, produces a transcript_snapshot with reset: true and one of these reasons: fence_missing, epoch_mismatch, generation_mismatch, or sequence_reset. Replace only that session's cached transcript when an explicit reset arrives. A new stream without a cursor receives a bounded snapshot with reset: false.

The old replay query is rejected. Raw mode remains a separate stored-event contract: with frames=raw, CompozyOS resumes persisted rows after Last-Event-ID and then follows new rows. Raw cursors do not carry transcript projection fences.

If the session is already stopped and no new stored rows arrive, CompozyOS emits a terminal synthetic session_stopped SSE event and closes the stream. When the stopped session has failure diagnostics, that terminal payload includes the same failure object stored in session metadata.

File mutation verification marker

CompozyOS reduces persisted edit tool events within one turn. When a failed file mutation has no later successful mutation for the same path and the turn persisted assistant text, CompozyOS appends one transcript_marker.file_mutation_unverified marker after the turn completes. Its evidence carries the bounded affected paths and total failure_count; paths_truncated = true reports that more than 32 unresolved paths existed.

The marker is a verification prompt, not proof that the filesystem is wrong. A later successful mutation for the same path suppresses it; failed reads, commands, and non-file tools do not create it.

Example terminal failure payload:

{
  "id": "session-stopped-sess-1234",
  "session_id": "sess-1234",
  "type": "session_stopped",
  "stop_reason": "agent_crashed",
  "failure": {
    "kind": "process_exit",
    "summary": "provider exited with status 1",
    "crash_bundle_path": "/Users/you/.compozy/logs/crash-bundles/sess-1234-process_exit-1770000000000000000.json"
  },
  "timestamp": "2026-04-16T01:10:06Z"
}

SQLite persistence

Each session stores its durable history here:

~/.compozy/sessions/<session-id>/events.db

The per-session database contains:

TablePurpose
eventsEvery persisted session event row
token_usageOne merged usage record per turn
hook_runsLifecycle hook execution history for that session

The events table stores:

  • id
  • sequence
  • turn_id
  • type
  • agent_name
  • content
  • timestamp
  • archived

archived is a replay-projection flag, not deletion. Operator event and history reads default to active rows. Pass archive=archived to inspect only a discarded suffix or archive=all to include both archived and active rows. Daemon degraded replay requests only active rows after the workspace checkpoint has durably covered the archived sequence range.

Queries are returned in ascending sequence order. If you request limit, CompozyOS selects the newest matching rows first and then re-sorts them ascending before returning them.

Relationship to replay

The resume attach and replay path depends on this stored event log:

  • compozy session history groups the stored rows by turn_id
  • GET /api/workspaces/:workspace_id/sessions/:session_id/transcript assembles canonical replay entries from these rows
  • GET /api/workspaces/:workspace_id/sessions/:session_id/stream emits transcript snapshots and deltas by default
  • GET /api/workspaces/:workspace_id/sessions/:session_id/recap returns a bounded, redacted reorientation snapshot
  • transcript_marker.created and transcript_marker.redacted rows preserve interrupts, timeouts, unhealthy state, MCP auth prompts, and unresolved file-mutation verification as durable marker evidence

Pressure compaction ordering

At context pressure, context.pre_compact runs before checkpoint coverage. If it allows the work, CompozyOS writes idempotent checkpoint coverage before marking the selected event range archived, then runs context.post_compact. A summary, provider, archive, or pre-hook failure leaves the original rows available for replay and arms the configured failure cooldown. session.compaction_fired provides durable correlation for the admitted attempt and its exact sequence bounds; it does not mean the later archive transition succeeded.

Next steps

  • Use Session Lifecycle to understand how stop, attach, and repair interact with the state machine.
  • Use Permissions to understand the approval events you will see in this stream.

On this page