Event Streaming
How Compozy records session events, streams them over SSE, and persists them in per-session SQLite databases.
- Audience
- Operators running durable agent work
- Focus
- Sessions guidance shaped for scanability, day-two clarity, and operator context.
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
Compozy stores session events in a per-session SQLite database and exposes them through both query and SSE surfaces.
Two event surfaces
Compozy exposes two different streaming models for session work:
| Surface | Endpoint | What you get |
|---|---|---|
| Persisted session stream | GET /api/workspaces/:workspace_id/sessions/:session_id/stream | Assembled transcript frames by default; raw stored event rows when frames=raw is supplied |
| Prompt stream | POST /api/workspaces/:workspace_id/sessions/:session_id/prompt | Raw 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.
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,
"raw": {
"status": "completed"
}
},
"timestamp": "2026-04-16T01:10:06Z"
}Two IDs matter here:
- outer
session_id: the stable Compozy 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 Compozy are:
| Type | When it is recorded | Key content fields |
|---|---|---|
user_message | Before Compozy submits a prompt to ACP | schema, type, session_id, turn_id, timestamp, text |
agent_message | Agent text chunk | text, plus the shared canonical fields |
thought | Agent reasoning chunk | text, plus the shared canonical fields |
tool_call | Tool call started or updated before completion | title, tool_name, tool_kind, tool_call_id, optional tool_input, optional raw |
tool_result | Tool call completed or failed | tool_call_id, tool_name, tool_result, tool_error, optional raw |
plan | ACP plan update | optional raw; Compozy stores the canonical envelope even when the payload is sparse |
permission | ACP permission request or resolved decision | request_id, action, resource, decision, title, tool_call_id, raw |
usage | Token or context usage update | usage with token counts, context counts, cost fields, and timestamp |
runtime_progress | Low-frequency long-running prompt progress update | text, runtime activity payload |
runtime_warning | Runtime supervision warning or inactivity timeout notice | text, runtime activity payload, optional raw |
session.compaction_fired | Pressure compaction admitted for a complete prior-turn sequence span | raw with workspace/session/turn IDs, sequence bounds, usage, pressure, strategy |
transcript_marker.created | Durable runtime evidence added to the transcript | marker kind, summary, occurrence time, and optional evidence |
transcript_marker.redacted | Durable marker whose diagnostic evidence was redacted | marker kind, summary, occurrence time, and redaction metadata |
system | Available-command updates, mode updates, or other non-chat ACP system updates | title, optional raw |
done | End of a prompt turn | stop_reason, optional usage, optional raw |
error | Prompt processing error | error, optional raw |
session_stopped | Session finalization | stop_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.
Compozy 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, Compozy cancels the active prompt
cooperatively. If the prompt does not finish within timeout_cancel_grace, Compozy 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:
| Field | Meaning |
|---|---|
schema | Currently compozy.session.event.v1 |
type | Inner event type |
session_id | ACP session ID from the runtime event |
turn_id | Prompt turn ID |
request_id | Permission request ID |
timestamp | Event timestamp |
text | User text, assistant text, or thought text |
title | Tool or update title |
tool_name | Derived tool name |
tool_kind | Canonical ACP tool kind, including edit for file-mutation reduction |
tool_call_id | Stable tool correlation ID |
tool_input | Structured tool input when present |
tool_result | Structured tool output (stdout, stderr, file_path, content, structured_patch, error, raw_output) |
tool_error | Whether Compozy classified the tool result as failed |
stop_reason | Turn or session termination reason |
failure | Typed lifecycle failure object with kind, redacted summary, and optional crash_bundle_path |
action | Permission action name |
resource | Permission resource target |
decision | Permission decision |
error | Error text |
usage | Token and cost payload |
runtime | Long-running prompt activity payload with current tool, last activity, progress, idle, and elapsed fields |
raw | Raw ACP update payload Compozy preserved |
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: Compozy 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:
| Status | Meaning | Amount |
|---|---|---|
actual | The agent reported the cost. | Present when reported |
estimated | Compozy multiplied reported token counts by merged model-catalog rates. | Present with cost_currency |
included | A native_cli provider owns subscription billing. | Omitted |
unknown | A required rate is missing or aggregate provenance/currency conflicts. | Omitted |
cost_source identifies agent_reported, catalog_config, models_dev, builtin, or none.
Compozy 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; Compozy 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. Compozy 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 20Filter to one type:
compozy session events sess-1234 --type tool_callGroup by turn:
compozy session history sess-1234The HTTP query surfaces accept these filters:
typeagent_nameturn_idsincelimitafter_sequence
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 --followcompozy 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 sequenceevent: transcript_snapshotfor the initial bounded transcript window or an explicit fenced resetevent: transcript_deltafor a bounded batch of materialized entry upserts after the current cursorevent: session_stoppedwhen a stopped session has no more rows to deliverevent: errorwhen 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 numberevent:as the persisted event typeevent: errorwhen the stream fails before it can continue; this frame is terminaldata:as oneSessionEventPayload
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, Compozy 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, Compozy 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, Compozy 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
Compozy 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, Compozy 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.dbThe per-session database contains:
| Table | Purpose |
|---|---|
events | Every persisted session event row |
token_usage | One merged usage record per turn |
hook_runs | Lifecycle hook execution history for that session |
The events table stores:
idsequenceturn_idtypeagent_namecontenttimestamparchived
archived is a replay-projection flag, not deletion. Operator event and history reads include both
archived and unarchived rows. Daemon degraded replay requests only unarchived rows after the
workspace checkpoint has durably covered the archived sequence range.
Queries are returned in ascending sequence order. If you request limit, Compozy 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 historygroups the stored rows byturn_idGET /api/workspaces/:workspace_id/sessions/:session_id/transcriptassembles canonical replay entries from these rowsGET /api/workspaces/:workspace_id/sessions/:session_id/streamemits transcript snapshots and deltas by defaultGET /api/workspaces/:workspace_id/sessions/:session_id/recapreturns a bounded, redacted reorientation snapshottranscript_marker.createdandtranscript_marker.redactedrows 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,
Compozy 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.