Skip to content
CompozyOS RuntimeSessions

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:

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.

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:

TypeWhen it is recordedKey content fields
user_messageBefore Compozy submits a prompt to ACPschema, type, session_id, turn_id, timestamp, text
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; Compozy 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.

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:

FieldMeaning
schemaCurrently compozy.session.event.v1
typeInner event type
session_idACP session ID from the runtime event
turn_idPrompt turn 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 Compozy 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
rawRaw 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:

StatusMeaningAmount
actualThe agent reported the cost.Present when reported
estimatedCompozy 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. 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 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

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, 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.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 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 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, 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.

On this page