Session lifecycle
How CompozyOS creates, activates, stops, and classifies one durable runtime session.
A session is the runtime object CompozyOS manages. It ties together:
- one logical agent session, which can bind an ACP-compatible agent subprocess when a prompt runs
- one workspace boundary
- one per-session SQLite event store
- one permission policy
- one durable CompozyOS session ID
That CompozyOS session ID identifies one durable lifecycle and its retained record.
State machine
Rendering diagram…
| State | Meaning | Valid live transition |
|---|---|---|
starting | A prompt to a stopped session is restoring the ACP process and durable provider history. | starting -> active |
active | CompozyOS has durably allocated the session ID, prepared its storage, and accepts prompts. Before the first prompt, its runtime is unbound; after binding, it can emit ACP events. | active -> stopping |
stopping | CompozyOS has accepted a stop request and is draining the session toward a stopped state. | stopping -> stopped |
stopped | Final metadata is written and the recorder is closed. Reads remain available. A normal prompt can restore an eligible stopped public session; a terminal process_exit remains read-only history and must be forked into a new session. | stopped -> starting |
Creating a session
Create a new session from the CLI:
compozy session new \
--agent general \
--cwd "$PWD" \
--name code-reviewCreate the same session over HTTP:
curl -X POST http://localhost:2123/api/sessions \
-H "Content-Type: application/json" \
-d '{
"agent_name": "general",
"name": "code-review",
"workspace_path": "/absolute/path/to/repo"
}'name is optional for user sessions. With roles.auto_title.enabled = true, CompozyOS starts one bounded
title pass after the first assistant response is persisted, then stores a successful title in
session metadata and the catalog. An explicit name always wins any race. If generation is disabled
or fails, the session remains unnamed; daemon-managed session types are never eligible. Configure
the title pass agent, provider, model, reasoning, and fallback routes under [roles.auto_title].
The request must include exactly one of:
workspace: a registered workspace name or IDworkspace_path: an absolute filesystem path
Behind that request, CompozyOS:
- Resolves the workspace and agent definition.
- Creates
~/.compozy/sessions/<session-id>/. - Opens
events.dbfor the session. - Persists the catalog row and session metadata.
- Activates the logical session with
runtime.status: "unbound". - Returns HTTP/UDS
201 Createdwithstate: "active"and no ACP process or ACP session ID.
Creation never accepts a prompt or runtime selector. It only creates the durable logical session. The agent definition supplies the defaults that the first prompt can use, but CompozyOS does not launch ACP or negotiate provider options until that prompt arrives.
If the agent declares a fallback_chain, the first prompt binds the session on the first route the
provider accepts. Routes refused before ACP acceptance are recorded as session.fallback.used events and
provider_failure transcript markers; runtime.effective on GET /api/sessions/{id} shows the route that
was accepted. See provider recovery.
Creation provenance
A session created from inside another session records that origin in its lineage:
compozy__session_create(native tool) links the calling session automatically. The link is server-derived from the bound caller, never a tool input, and is recorded only when the new session lands in the caller's workspace.compozy session new --parent <session-id>andparent_session_idonPOST /api/sessionslink explicitly. The parent must exist in the target workspace; it does not need to be active.- An HTTP/UDS create carrying validated agent identity headers and no explicit parent infers the caller's session as the parent.
Provenance sets lineage.parent_session_id, inherits root_session_id from the parent's tree, and
computes spawn_depth server-side. The session stays type: "user" with no TTL, auto-stop, spawn
budget, or permission narrowing — governed children come only from
safe spawn. Query the hierarchy with the catalog filters parent=<id>
(direct children) and root=<id> (the whole tree, root included), exposed as
compozy session list --parent/--root and on compozy__session_list.
Lineage kind
Every session read (GET …/sessions/{id}, compozy session status, and each row of
compozy session list -o json) carries lineage.kind, which says how the session relates to its
parent:
| Kind | Meaning |
|---|---|
"" | A root session with no parent relation. |
provenance | A user session created with a parent (the default for --parent). |
recovery | A user session created to recover a failed parent. |
spawn | A governed spawned or spawn-role session. |
continue | A session continued from another session with another agent. |
fork | A session forked from another session. |
continue and fork sessions also carry origin_agent_name, and forks carry origin_message_id.
Their parent_session_id names the source as provenance only: a continued or forked session is a new
top-level session, so session lists show it as its own row rather than under its source, and it is not
counted as the source's child (it never keeps the source alive). Spawned, provenance, and recovery
sessions are children of their parent. The parent=/root= catalog filters match the stored fields
as-is, so they include continued and forked sessions; read lineage.kind to tell them apart.
A plain create accepts only provenance or recovery, and only together with a parent:
compozy session new --agent codex --parent sess-1234 --lineage-kind recoveryThe HTTP/UDS equivalent is "lineage_kind": "recovery" next to parent_session_id; any other value, or
a kind without a parent, returns 400.
Sessions created before lineage kinds existed are upgraded automatically: spawned and spawn-role sessions
read spawn, other parented sessions read provenance, and root sessions read "". The upgrade applies
both to the session catalog and to each session's metadata file. Reading an older metadata file never
rewrites it; the kind is saved the next time the session's lifecycle state changes.
Running an active session
Once a session is active, a prompt supplies an immutable runtime snapshot. The first prompt binds
the logical session to that runtime; later prompts can choose a different snapshot:
compozy session prompt sess-1234 "Review the current changes and report correctness risks." \
--provider cursor \
--model grok-4.6 \
--reasoning-effort high \
--speed fastcurl -N -X POST http://localhost:2123/api/workspaces/ws_alpha/sessions/sess-1234/prompt \
-H "Content-Type: application/json" \
-d '{
"message": "Review the current changes and report correctness risks.",
"message_id": "msg_1234",
"idempotency_key": "idem_1234",
"runtime": {
"provider": "cursor",
"model": "grok-4.6",
"reasoning_effort": "high",
"speed": "fast"
}
}'Pass other advertised select and boolean options with repeatable typed flags. For example, Cursor's Opus 5 variants can expose a Thinking toggle:
compozy session prompt sess-1234 "Review with extended thinking." \
--provider cursor \
--model claude-opus-5 \
--reasoning-effort high \
--speed fast \
--acp-toggle thinking=trueUse --acp-option id=value for selects and --acp-toggle id=true|false for booleans. HTTP and UDS
send the same data as runtime.acp_options, with exactly one of value_id or bool_value per entry.
Two operational details matter here:
message_ididentifies the authored transcript row, whileidempotency_keyidentifies the submission. Keep both values and reuse them together only when retrying the exact same request.runtimeis a prompt-bound snapshot ofprovider,model,reasoning_effort,speed, and typed ACP options. The accepted snapshot is retained with the turn and is not rewritten by later runtime changes.- If a snapshot keeps the same provider, CompozyOS configures the existing ACP session in place when the provider exposes the needed live options. A provider or harness change replaces the process and replays the stored transcript into the replacement ACP session as a bounded replay.
- A Cursor model, Reasoning, Fast, or Thinking change resolves a different private launch alias and atomically replaces the process. Public session data continues to expose only the logical model ID.
- CompozyOS records
user_messageonly after runtime binding or reconfiguration succeeds. If binding, replacement, or its rollback fails, the authored message is not persisted as a user turn. - One session processes one prompt turn at a time. Stop logic waits for in-flight prompt setup to finish before it asks the ACP driver to stop the subprocess.
- Long-running prompts stay healthy through runtime activity supervision. CompozyOS updates
activity.last_activity_aton real ACP events and metadata-only waiting heartbeats, then emits lower-frequencyruntime_progressevents. Only actual work evidence prevents silence supervision. - CompozyOS also maintains a separate metadata-only session health
record (
state,health,attachable,eligible_for_wake). Activity supervision feeds health, but the two are distinct authorities — supervision owns timers and persisted progress events, health is consumed byHEARTBEAT.mdwake decisions and thecompozy session health|status|inspectsurfaces.
When the provider disconnects during a prompt
CompozyOS persists each accepted agent event before forwarding it. If the ACP subprocess disconnects after sending part of an answer, the stored assistant chunks remain in events and transcript reads. The daemon then makes three automatic recovery attempts with 1, 2, and 4 second delays. While this is running, session reads expose:
runtime.status: "recovering"runtime.transition: "automatic_recovery"runtime.generationand structuredruntime.recoveryattempt timing- durable
runtime_recovery_started,runtime_recovery_succeeded, andruntime_recovery_exhaustedevents
The replacement process first tries ACP session/load. When that is unavailable, CompozyOS rebuilds
context from the durable transcript as a
bounded replay. It then replays the
interrupted turn with its original turn ID;
the authored user message is not inserted again. A successful replacement continues the same prompt
stream. Viewer disconnects remain detached from execution and do not stop recovery.
Only exhausted recovery emits one terminal error and stops the runtime. A JSONL prompt writes that
error and exits nonzero; compozy__session_prompt returns tool_backend_failed. Inspect the session
before continuing:
compozy session status sess-1234 -o json
compozy session events sess-1234 --last 20 -o jsonAutomatic replay can repeat a tool or external side effect whose completion was not persisted before the disconnect. CompozyOS supplies retained results to the replacement runtime, but cannot prove an external mutation did not complete. Inspect the external system before manually repeating the work.
Selecting the next runtime
Persist next-prompt runtime intent without starting or reconfiguring ACP:
compozy session runtime set sess-1234 \
--provider cursor \
--model claude-opus-5 \
--reasoning-effort high \
--speed fast \
--acp-toggle thinking=trueSession reads expose runtime.selected, runtime.selection_revision, runtime.effective, and
runtime.last_known.
selected survives stops and daemon restarts; effective describes the runtime already applied to
the current process. last_known preserves the recorded provider, model, reasoning effort and speed
after idle release, detachment or stop. It is absent before any runtime identity has been recorded
and does not imply a live process. Session-list rows prefer effective, then last_known, then the
selected runtime or the owning agent’s configured defaults.
Runtime fields resolve from provider/project and agent defaults, then durable
selected, then the prompt snapshot. Each higher layer replaces only the fields or option IDs it sets.
HTTP and UDS use PUT /api/workspaces/:workspace/sessions/:id/runtime with the complete runtime
and current expected_revision. Clear the selection with
DELETE /api/workspaces/:workspace/sessions/:id/runtime?expected_revision=<revision>. A stale
revision returns 409; read the session again before deciding what to save.
Negotiated agent capabilities
While a session has a live agent process, session reads include runtime.acp_caps: what the agent
advertised during the ACP initialize handshake. The field is absent when no process is bound.
| Field | Meaning |
|---|---|
supports_load_session | The agent can reopen a stored conversation with session/load. |
supports_fork_session | The agent advertises the unstable session/fork capability. |
supports_resume_session | The agent advertises the session/resume capability. |
prompt_image, prompt_audio, prompt_embedded_context | Prompt content the agent accepts. |
supported_modes, config_options | Session modes and ACP config options reported at setup. |
Each value is true only when the agent declared it. Traffic that an agent sends for any ACP session
other than the one bound to the CompozyOS session is dropped and logged, so it never appears in that
session's transcript.
Busy input modes
An active session still processes one prompt turn at a time, but CompozyOS accepts explicit busy-input
modes so operators and agents do not have to guess what happens when a prompt is already running.
session.busy_input.default_mode defaults to steer; session.busy_input.queue_cap bounds
the persisted queue.
The Web composer is available for active and eligible stopped user, system, coordinator, and
spawned sessions. While one of these sessions is busy, the Web controls can queue a prompt, steer
or interrupt the current turn, or stop generation. These are turn-level controls: daemon-managed
sessions still do not expose user-session lifecycle actions such as rename, clear, attach, delete,
or stop session. Hidden maintenance sessions remain read-only in the Web session view.
| Mode | CLI | HTTP shape | Result |
|---|---|---|---|
queue | compozy session prompt <id> "<text>" --queue | POST /api/workspaces/:workspace/sessions/:id/prompt with {"mode":"queue","message":"...","message_id":"msg_queue_1234","idempotency_key":"idem_queue_1234","runtime":{...}} | Persists the input and runtime snapshot in FIFO order. The result reports status: "queued", delivery: "after_turn", queue ID, position, and generation. |
interrupt | compozy session prompt <id> "<text>" --interrupt --expected-turn-id turn_1234 | POST /api/workspaces/:workspace/sessions/:id/prompt with {"mode":"interrupt","expected_turn_id":"turn_1234","message":"...","message_id":"msg_interrupt_1234","idempotency_key":"idem_interrupt_1234","runtime":{...}} | Resolves an omitted fence or validates an explicit fence, advances the queue generation, and returns status: "interrupting" with delivery: "interrupt_then_prompt". |
steer | compozy session prompt <id> "<text>" --steer --expected-turn-id turn_1234 | POST /api/workspaces/:workspace/sessions/:id/steer with {"text":"...","message_id":"msg_steer_1234","idempotency_key":"idem_steer_1234","expected_turn_id":"turn_1234"} | Injects into the live turn when supported; otherwise records an interrupt fallback. Reports disposition and steer_delivery. |
The prompt response is authoritative. Use disposition and steer_delivery to inspect the outcome;
transcript markers retain history but are not a second action result channel.
See Session control plane for delivery outcomes, automatic fences, and exact retries.
Managing queued input
Use the queue entry ID from a queued result or from session input list. The daemon owns the list, so
read it again after each mutation instead of keeping a client-side queue.
compozy session input list sess-1234
compozy session input edit sess-1234 queue_entry_1234 "Run the focused test first."
compozy session input steer sess-1234 queue_entry_1234 "Prefer the smaller patch." \
--expected-turn-id turn_1234
compozy session input cancel sess-1234 queue_entry_1234edit and steer author a replacement message_id and idempotency_key; provide both original
values together only to retry the exact same mutation. steer requires --expected-turn-id, which
rejects a request that targets an older turn. HTTP and UDS expose the same daemon-owned operations:
| Operation | Endpoint |
|---|---|
| List | GET /api/workspaces/:workspace/sessions/:id/prompt/queue |
| Edit | PUT /api/workspaces/:workspace/sessions/:id/prompt/queue/:queue_entry_id |
| Promote to steer | POST /api/workspaces/:workspace/sessions/:id/prompt/queue/:queue_entry_id/steer with expected_turn_id |
| Cancel | DELETE /api/workspaces/:workspace/sessions/:id/prompt/queue/:queue_entry_id |
Use POST /api/workspaces/:workspace/sessions/:id/prompt/cancel to stop the current turn without a
replacement. Use prompt mode interrupt or steer with expected_turn_id when replacement text must
run next. Every busy-input transition writes transcript marker evidence for recap, replay, and audit.
Queue and interrupt preserve the runtime snapshot accepted with their replacement input. A queued prompt therefore runs with the selected provider, model, reasoning effort, and speed even if a later prompt changes the session binding first.
Both interrupt and steer submit only the explicit replacement text. They never salvage, reuse, or
combine the canceled authored prompt. The expected turn fence rejects stale actions before they can
affect a newer turn.
Runtime activity supervision
Supervision inspects every live session. Any fresh work signal
keeps the session alive: agent progress, a verified running tool, an active child, a recently
reconciled Loop run, a held task lease, a scheduled wait with a future deadline, or a reply the
session is awaiting (awaiting_reply). A session that sent compozy__session_prompt with
notify_on_complete: true stays alive while the session it asked is live, and while the reply
wake is waiting to be delivered, until the watch expires after session.messaging.reply_watch_ttl
(24 hours by default).
Opening a session, subscribing to its events, and provider-wait heartbeats do not renew work evidence.
Inspect supervision.work_signals, supervision.sources, and supervision.quiet_warning
with compozy session status <id> -o json, the session resource, or compozy__session_describe.
Signals are rebuilt from their owners after restart. They are not persisted as keep-alive flags.
Expired evidence stops counting as work. A stale tool, stopping child, or Loop raises attention;
a source inspection error is unknown, raises attention, and prevents an automatic inactivity stop
until the source can be inspected again.
Silence supervision is a hung-turn watchdog. Silence accrues only while a turn is in flight and waiting on the agent. A session with no turn in flight (the agent finished and is waiting for you) never gets a quiet warning and is never stopped for inactivity. A turn blocked on a pending permission or clarify request does not accrue silence either. When a turn ends, any quiet warning clears.
After session.supervision.quiet_after of observed silence in a turn (default 30m), CompozyOS
records one session.supervision_warning and the open session shows it on the working status line
under the transcript, for example Working for 42m · quiet for 12m. Use Stop generation in the
composer or Stop in the topbar to end the turn yourself. Continued silence for
session.supervision.stop_grace (default 10m) requests the normal stop ladder with cause
inactivity. session.supervision_stopped records verified termination. Persisted stop metadata
uses stop_reason: "timeout" and stop_detail: "inactivity". New work clears the warning and
cancels the pending automatic stop. quiet_after = "0" disables the watchdog;
stop_grace = "0" keeps the warning but disables automatic stop.
Idle runtime release
An idle session keeps its agent process for session.supervision.idle_release_after (default
30m). After that, CompozyOS silently stops the agent process and keeps the session. This happens
only when the session has no turn in flight, no work signal, no unknown signal source, no pending
permission or clarify request, and its agent advertised ACP session/load. Agents without
session/load keep their runtime. idle_release_after = "0" disables the release. In the web
app, Settings → General → Pause idle agents after edits this key.
A released session stays state: "active" with runtime.status: "unbound". It keeps its
transcript and ACP session ID and shows the normal idle badge. The release records no supervision
warning or stop event and dispatches no session.post_stop hook. The process-level agent.stopped
hook fires, as it does for any runtime replacement.
Release allows the driver its bounded stop and drain time. The process identity remains recorded until exit is verified; a failed stop remains visible for recovery instead of being treated as a successful release.
The next prompt from any surface (composer, CLI, HTTP/UDS, a reply delivery, or a wake) binds a runtime again without a Resume step:
- CompozyOS reloads the same ACP session with
session/load. - If the load fails, it starts a fresh ACP session and replays the bounded transcript
(
session.derive.max_replay_bytes). - If the prompt selects a different provider or model, it binds that route and carries the transcript, as a live runtime switch does.
If the daemon exits uncleanly while a runtime is released, boot recovery ends that session
stopped with stop_reason: "shutdown" and the detail
daemon exited while the idle session runtime was released. It is not recorded as a failure, and a
later prompt resumes it with session/load.
A reply can still arrive after the sender stopped, for example when the asked session was itself stopped with the message queued and is resumed later. An idle sender is not stopped while it waits; its runtime may be released, and the reply binds it again and delivers the wake once. If CompozyOS stopped the sender (an inactivity stop of a hung turn, a timeout, daemon shutdown or an agent crash), the reply resumes it and delivers the wake once. A sender you stopped yourself stays stopped; the wake is delivered when you resume it.
Event waits without an authored expiry receive the effective admission horizon, normally 168h.
Existing expiryless waits use their original creation time and pinned configuration when upgraded.
An expired wait follows the existing attention path instead of keeping a session alive forever.
runtime_progress remains a periodic progress projection. Metadata-only activity heartbeats and
progress projections do not themselves prove fresh work. An explicit prompt budget
(session.supervision.prompt_deadline) remains an opt-in control separate from silence supervision.
Rewinding a conversation
Rewind an idle user session when you want to retry from an earlier prompt without creating another
CompozyOS session. The selected durable user message and every later active event are removed from the
active transcript. CompozyOS returns the selected text as draft_text, keeps the same session ID, and
starts a fresh ACP context from the retained prefix.
compozy session rewind sess-1234 \
--message-id msg_1234The CLI reads the current transcript fences immediately before the request. Scripts retrying one
known request can pass all three --expected-* flags together with the original idempotency key.
HTTP and UDS use POST /api/workspaces/:workspace_id/sessions/:session_id/rewind and require those
same fences.
An identical idempotency_key retry returns the original result. A stale fence, busy session, queued
input, pending approval, or daemon-managed session returns 409 without cutting the transcript.
Children created with --parent (provenance), recovery sessions, and continued or forked sessions
rewind like any user session; only governed spawn sessions are refused as daemon-managed.
Rewind changes the conversation only. It does not undo file changes, tool effects, network actions, or external provider actions. Removed events remain archived for audit, and rewind is the only operation that archives events: the agent's own context compaction never does.
compozy session events sess-1234 --archive archived
compozy session history sess-1234 --archive allThe Web action appears on durable user messages. It requires an empty composer, confirms the same
side-effect boundary, reloads the daemon-owned transcript, and places draft_text in the composer.
Continuing and forking a session
Continue a user session with another agent when the current one cannot go on (most often because its provider account hit a limit) or when you want a second agent to pick up the same work. CompozyOS starts a new session in the same workspace, worktree, and working directory, carries the conversation over as historical context, and never changes the source session: no event is appended, no transcript epoch or generation changes, and a running turn is not interrupted.
compozy session continue sess_01J9R2K7M4T1 --agent claude-code \
--message "Carry on with the migration; run the tests first."Continued sess_01J9R2K7M4T1 (codex) into sess_01J9R2Q0W8YB (claude-code)
Origin continue · from sess_01J9R2K7M4T1 · codex · through turn turn_01J9R2N9C3
Context 42 messages · 61.3 KiB · nothing omitted
Seed replay
First prompt admittedHTTP and UDS use POST /api/workspaces/:workspace_id/sessions/:session_id/continue with agent_name
and idempotency_key (required), an optional runtime or route, name, message, and the three
transcript fences together or none. Agents use the compozy__session_continue native tool. Only user
sessions can be continued (session_not_derivable otherwise); archived sources return
session_archived.
The new session reads lineage.kind = "continue", lineage.origin_agent_name, and a derivation
object (kind, source_session_id, seed, first_prompt); compozy session status prints matching
Origin and Derivation lines.
Preview before you continue
GET /api/workspaces/:workspace_id/sessions/:session_id/derive/preview reports what a continue would
carry right now (message_count, replay_bytes, truncated, omitted_count,
source_turn_in_progress), the whole source transcript's source_message_count, and the transcript
fences it was measured at. The preview never writes
anything and never repairs a stopped source.
The carried context
CompozyOS takes one read-only snapshot of the source and carries its pruned transcript through the last settled turn. A later turn that is still running (user or synthetic) is left out, and the carried context ends with a note that it was not included and that its tools may have partially executed.
The carried context is stored on the new session and prepended exactly once whenever that session's history is rebuilt for an agent: its first prompt, a later provider replacement, a recovery without a native session id, or a rewind restart. Every such rebuild uses the same bounded replay described in Context compaction. The first prompt reads:
Context rebuilt from log.
This session continues sess_01J9R2K7M4T1 (agent: codex). Continue this session using the persisted transcript below. It is historical context, not a new user request. Do not repeat completed tool calls solely because they appear in the log. The files and git state in the workspace are authoritative; inspect them before acting on the transcript. Attachments listed in the transcript were not copied to this session.
<compozy_context_replay>
[...]
</compozy_context_replay>
User request:
Carry on with the migration; run the tests first.Without --message the context waits (first_prompt: staged) and is composed in front of the first
prompt you send. With a message, the message is admitted through the ordinary prompt admission
(first_prompt: admitted); delivery and completion are reported by the new session's prompt events.
If the daemon stops before that admission, the new session keeps first_prompt: staged; repeating the
same request (same idempotency_key) admits the message and sends it once.
Continuing a continued session carries that session's own transcript with its carried context flattened
in front, once. Clearing the new session's conversation discards the carried context. Attachments are
carried as metadata only.
The carried context is bounded by [session.derive] (see
config.toml): max_replay_bytes (default 131072) is a
hard bound and max_message_bytes (default 16384) caps each message body. The newest 8 messages are
protected while they fit, and the earliest user message stays pinned ahead of the omission note
[N earlier messages omitted to fit the context budget]. Omissions are stated in the context and
reported as truncated and omitted_count. When messages were omitted and the new session can call
compozy__session_history, the header also points the agent at the source session
(session_id: <source id>) so it can read the earlier messages on demand.
Omissions accumulate along a chain of continues and forks. When the source itself carries a bounded
context, the new session's omitted_count and truncated (in the derivation receipt, the preview,
and the carried-context metadata) include what the earlier hop already dropped, on top of anything the
new bound drops. A nested continue never reports a clean context that an ancestor cut.
Retries
One (workspace, idempotency_key) creates at most one session. Repeating the same request returns the
recorded outcome with replayed: true (HTTP 200 instead of 201, CLI Replayed yes), even when the
source changed since. If the new session was deleted since, the recorded outcome returns with
child_deleted: true and nothing is created. The same key with a different request is
idempotency_conflict. The CLI generates a key when --idempotency-key is omitted. A retry still
returns its recorded outcome after the source session was deleted: the daemon looks up the key before
it checks the source.
Some failures happen after the new session already exists, for example when its first message cannot
be admitted because the runtime is not authenticated (422). The error then carries
child_session_id: open that session, or repeat the request with the same key to get it back. The web
dialog keeps offering Open new session for the rest of that dialog, even after you edit the request
or it is refused again, and the CLI prints the session id and the key to rerun with. When the refusal
names a provider or model problem, its code is the error's code too (for example model_unavailable).
Errors carry a stable code: invalid_request, session_not_found, session_not_derivable,
session_archived, agent_not_found, message_not_found, route_not_found,
session_turn_in_progress, session_fence_conflict, idempotency_conflict,
new_work_admission_unavailable. The CLI prints them as Error: <code>: <message> and exits 1.
When you send fences (--expected-epoch, --expected-generation, --expected-max-sequence, all three
or none), they must match the snapshot; otherwise the request fails with session_fence_conflict.
Without fence flags the CLI reads the source's current fences first, through the same read-only
preview the web dialog uses; looking up the source never changes it. A rerun with the same
--idempotency-key still reaches the daemon when the source can no longer be read.
Continuing on a declared route
--route <n> (API route) selects the 1-based route of the target agent's fallback_chain, that is, a
specific account. It cannot be combined with any runtime flag. An index the agent does not declare
returns route_not_found. The chosen route is saved on the new session and applied at its first bind;
if that route was removed or its command changed by then, the bind fails with
route_not_found: agent "…" declares N route(s); route M does not exist, the session stays unbound, and
nothing runs on the default account. Send a prompt with an explicit runtime or restore the route.
Forking a session
Fork a user session to try another direction with the same agent, runtime, and account. The whole conversation is carried through its last settled turn, or pass a durable user message to fork through that message and its turn: the message, the agent's reply, and its tool work are included; nothing after that turn is.
compozy session fork sess_01J9R2K7M4T1 --message-id msg_01J9R2N3V0QFForked sess_01J9R2K7M4T1 into sess_01J9R2T5H2CD (codex)
Origin fork · from sess_01J9R2K7M4T1 · through msg_01J9R2N3V0QF (turn turn_01J9R2N3V1)
Context 17 messages · 22.8 KiB · nothing omitted
Seed replay
First prompt stagedHTTP and UDS use POST /api/workspaces/:workspace_id/sessions/:session_id/fork with
idempotency_key (required), an optional message_id and name, and the three transcript fences
together or none. Agents use the compozy__session_fork native tool. Forks accept no agent or runtime
fields; retries, fences, and the carried context behave as for continue. The new session reads
lineage.kind = "fork" and, for a message cut, lineage.origin_message_id; the response reports the
cut turn as through_turn_id.
A cut turn must have settled: it ended with done, an error, or a cancellation. A message whose turn
is still running returns session_turn_in_progress; wait or fork from an earlier message. A message
that is not a durable user message in the session returns message_not_found. Messages remain valid
fork points after a rewind followed by new turns; only rewind itself refuses those.
GET …/derive/preview?message_id=… reports the cut as cut.turn_id and cut.turn_settled before you
fork; message_count is what the cut carries and source_message_count is the whole source.
When the source runs on an overridden account (an accepted fallback_chain route), the fork is created
on that same declared route when the agent still declares it compatibly; otherwise it binds the agent's
default route.
The agent's own clone
Some agents can clone their own session (ACP session/fork together with session/load). A
whole-session fork uses that clone when the source is bound on a live process, idle (no prompt in
progress and nothing queued), and the fork resolves to the same provider identity. The preview reports
this as native_fork_possible (false when not possible or unknown). A fork through a message, a
running or stopped source, or an agent without both capabilities uses the carried context instead.
A native fork responds with seed: native_fork, native_state: pending, and the clone's
acp_session_id; the carried context is still stored as a fallback. The clone is loaded by the new
session's own process at its first prompt, and only under the provider identity it was created with.
The session read then reports derivation.native_state as:
| State | Meaning |
|---|---|
pending | The clone exists and has not been loaded yet. |
loaded | The first prompt loaded the clone; the carried context was not sent. |
failed | The clone could not be loaded (native_fork_error, for example session/load: resource not found) or the route differed from the clone's; the first prompt carried the context instead. |
When the clone request itself fails, the fork is created with seed: replay and the redacted error in
native_fork_error. The source never receives the clone's traffic, and its events, fences, and runtime
stay unchanged. A loaded native session replays its history from the agent, so its log is not empty
before your first message; a later provider replacement still prepends the carried context.
Stopping a session
Request a stop immediately, or wait for its verified result:
compozy session stop sess-1234
compozy session stop sess-1234 --wait -o jsonFor a session in a named profile, add --profile <name> to the CLI command or
?profile=<name> to the HTTP/UDS stop route. Web controls use the selected session's own
workspace and profile, including each target in a bulk selection.
The default command returns the updated session resource. --wait returns the stop outcome:
state, verified, escalated, stop_cause, phase, and stopped_after. A verified result has
state: "stopped" and verified: true. If process exit cannot be verified after the final kill,
the outcome keeps state: "stopping", verified: false, and attention: "stop_verification_failed". The diagnostic also persists on the session resource with the nonterminal needs-attention badge, so reconnecting does not lose the retry action. A verified retry clears it. Compact session status exposes lifecycle_state, verified, escalated, and attention; its existing state field retains the health-state vocabulary.
Inspect the process diagnostics and retry the stop; do not treat that outcome as termination.
If a previous version left a session stopping after its runtime was released, retry Stop or Archive after upgrading. Recovery recognizes the unbound runtime with both process identity fields cleared and completes the stop without signaling another process. Partial identities still require proof of exit. Repeated verification causes appear once in the error message.
HTTP and UDS share the same operation:
curl -X POST http://localhost:2123/api/workspaces/ws_alpha/sessions/sess-1234/stop \
-H 'Content-Type: application/json' -d '{"wait":false}'wait:false returns 202 with status: "stopping". wait:true returns 200 with the settled stop
outcome, including the unverified attention branch. An already stopped session returns 200 without
starting another operation. Closing the requesting client does not abandon an accepted stop.
The governed native tool compozy__session_stop accepts the same wait choice and outcome fields.
Its existing approval and same-workspace, non-self target rules still apply.
For compatibility through v0.4, HTTP/UDS requests that omit wait retain synchronous 204 responses
with a migration warning. Native calls that omit wait retain the previous synchronous result and
include deprecation. Send an explicit boolean; implicit synchronous requests are removed in v0.5.0.
The stop path moves active -> stopping, requests cooperative cancellation, then escalates to
process stop and group kill when necessary. session.stop.cooperative_grace configures the first
phase (default 10s); force and kill verification each have a five-second budget. PID and start-time
identity must prove process exit before terminal state is published. Session reads expose verified
and the durable escalated flag. Turn cancellation uses the same ladder but preserves the logical
session and rebinds a replacement process when escalation killed the previous one.
Stopping preserves the session directory, catalog row, metadata, events, and selected runtime. It ends attach eligibility. A later normal prompt restores the provider history and continues under the same CompozyOS session ID; attach, queue, steer, interrupt, and other control operations do not restart it.
Archiving a stopped session
Archive a stopped session when it should leave the default catalog but its history must remain:
compozy session archive sess-1234The matching HTTP and UDS operation is:
curl -X POST http://localhost:2123/api/workspaces/ws_alpha/sessions/sess-1234/archiveArchive is catalog metadata, separate from the session lifecycle state. By default only a stopped
session can be archived; a live session returns 409. Its metadata, events, runtime selection, and
direct read operations remain available. Attach, prompt, and resume operations are rejected while it
is archived.
Stopping and archiving in one step
Pass --stop to archive a session that is still running:
compozy session archive sess-1234 --stopThe HTTP and UDS operation takes an optional stop query parameter:
curl -X POST "http://localhost:2123/api/workspaces/ws_alpha/sessions/sess-1234/archive?stop=true"The native tool compozy__session_archive accepts the same stop: true input. The daemon stops
the session through the normal stop path, which cancels any active turn and stops its subagent
sessions, and then archives it. An already stopped session is archived as usual. If the stop fails,
the session stays unarchived and the request returns 409 with code session_archive_stop_failed.
In the web app, Archive on a running session reads Stop and archive and makes the same call.
Bulk archive accepts a mix of running and stopped sessions and reports each result separately.
Restore the session to the default catalog before restarting it:
compozy session unarchive sess-1234The matching HTTP and UDS endpoint ends in /unarchive. Unarchiving preserves the stopped state, so
a later normal prompt can restart the same logical session. Use removal only when the complete
session record should be deleted.
Timeout behavior
CompozyOS has separate timeout concepts:
| Timeout | What it protects |
|---|---|
session.limits.timeout | Deprecated and not enforced; removed in v0.5.0. Ends no session. |
session.supervision.quiet_after | Silence in an in-flight turn before one warning; zero disables the watchdog. |
session.supervision.stop_grace | Continued turn silence after the warning before the standard stop ladder. |
session.supervision.idle_release_after | Idle time before the agent process is released; zero disables the release. |
session.supervision.prompt_deadline | Optional total prompt budget; zero disables it. |
session.supervision.timeout_cancel_grace | Grace after explicit prompt-budget cancellation. |
session.stop.cooperative_grace | Cooperative phase of the stop ladder, followed by bounded forced/kill phases. |
| Session lifecycle timeout | Recorder close and final cleanup work during stop. |
A session may run indefinitely while fresh work exists and no explicit prompt budget is set.
Stop reasons
The session manager currently emits these persisted stop classifications:
| Stop reason | When CompozyOS uses it |
|---|---|
completed | The agent finished normally. |
owner_released | A managed owner, such as a Loop, no longer needs the session. This records automatic retirement; the task and Loop keep their independent outcomes. |
user_canceled | A normal user stop request. |
max_iterations | A user stop request carried max_iterations detail. |
loop_detected | A user stop request carried loop_detected detail. |
budget_exceeded | A user stop request carried budget_exceeded detail. |
timeout | An explicit runtime budget expired, or a silent in-flight turn completed its warning/grace path (stop_detail: inactivity). |
error | Start or stop failed, or ACP reported a transport failure and the subprocess then exited successfully with code 0. |
agent_crashed | The subprocess exited with a non-zero code, a signal, or a wait error; also used when CompozyOS repairs stale active/stopping metadata after a daemon crash. |
hook_stopped | A required lifecycle hook denied continuation. |
shutdown | The daemon shut the session down, or exited uncleanly while the idle session runtime was released. |
Inspect the current session state and stop classification at any time:
compozy session status sess-1234Removing persisted history
Remove a session only when its durable history is no longer needed:
compozy session remove sess-1234curl -X DELETE http://localhost:2123/api/workspaces/ws_alpha/sessions/sess-1234Removal stops an active session first, deletes its durable catalog row and session directory, and emits a workspace-scoped catalog deletion signal. After a successful removal, later status or detail reads return session not found. If deletion fails before the directory is removed, CompozyOS preserves or restores catalog truth instead of reporting a partially deleted session.
Failure diagnostics
Stop reasons answer "why did the CompozyOS session stop?" Failure diagnostics answer "what kind of
lifecycle failure did CompozyOS classify, and what evidence is available?" When a lifecycle failure occurs,
CompozyOS persists a failure object with:
kind: stable machine-readable failure kindsummary: bounded redacted diagnostic textcrash_bundle_path: path to a redacted crash bundle when CompozyOS captured one
The same failure object is exposed through session status, session list JSON, session SSE terminal
events, and compozy status.
compozy session status sess-1234 -o json | jq '.session.failure'Current failure kinds are:
| Failure kind | Meaning |
|---|---|
startup_failure | CompozyOS could not finish preparing, launching, or initializing the provider. |
handshake_failure | The subprocess launched but ACP initialization did not complete. |
load_session_failure | An internal daemon-owned provider reload path failed. This is not resume attach. |
protocol_failure | ACP returned a structured protocol/request error outside a prompt-specific path. |
prompt_failure | ACP prompt submission or prompt streaming failed. |
cancellation | The operation was canceled by user action or context cancellation. |
permission_failure | A lifecycle hook or permission boundary denied continuation. |
process_exit | The provider subprocess exited unexpectedly or returned a wait error. |
transport_failure | The ACP stdio transport closed or failed while CompozyOS expected protocol traffic. |
timeout | Runtime supervision or lifecycle timeout expired. |
unknown_failure | CompozyOS had an error but no more specific kind was available. |
Crash bundles are written under ~/.compozy/logs/crash-bundles/ by default. They are JSON documents
with schema compozy.session_crash_bundle.v2, session identity, provider identity, failure kind,
process metadata, error text, and captured stderr when available. Completed process metadata also
includes exit_code and the terminating signal when the operating system exposes them. Bundle
contents are bounded and redacted before they are written, and files are created with owner-only
permissions.
Crash classification and transcript repair
CompozyOS repairs stale persisted metadata the next time it reads a session after an unclean daemon exit. That classification keeps status, list, history, and repair consistent after a crash:
- stale
activebecomesstoppedwithagent_crashed,process_exit, and detaildaemon crashed while session active - stale
stoppingbecomesstoppedwithagent_crashed,process_exit, and detailstop did not complete - stale
startingbecomesstoppedwitherror,startup_failure, and detailstart did not complete
During daemon boot, CompozyOS also inspects stopped sessions whose stop reason is agent_crashed or
error. If the final persisted turn was interrupted, CompozyOS appends repair events to terminalize the
transcript: dangling tool calls receive interrupted tool results, then the turn receives a terminal
error event. The repair is append-only; CompozyOS does not truncate, delete, or resequence session events.
Operators and agents can inspect or run the same append-only transcript repair explicitly:
compozy session repair <session-id> --dry-run
compozy session repair <session-id>What gets persisted
Every session owns a directory under ~/.compozy/sessions/<session-id>/:
meta.json: durable session metadata such as state, workspace, session CWD, stop reason, failure diagnostics, and ACP session IDevents.db: persisted event, token usage, and hook-run history
That durable store supports live resume attach, event replay, deterministic recap, and approval audit. Attach requires a live, attachable session; the other reads remain useful after terminal stop.
Session directories the daemon does not recover
At boot the daemon reconciles the global catalog with the session directories on disk. It skips a
directory, and never adds it to the catalog, when its meta.json names either of these:
- a session type other than
user,system,coordinator, orspawned, such as thedreamtype of the removed memory feature (reason: unknown_session_type) - an internal spawn role of the removed memory feature,
memory-extractororcheckpoint-summary(reason: retired_spawn_role)
For each skipped directory the daemon logs one observe.session_recovery_skipped warning per daemon
run, with session_id, session_type, spawn_role, and reason. It never deletes, moves, or rewrites
the directory, so you can copy what you want to keep and delete the rest yourself. A catalog row that
already exists for a skipped directory is marked orphaned, like any directory reconciliation cannot
recover. Sessions with any other spawn role, including custom advisory roles, recover normally.
The upgrade itself deletes the catalog rows of the legacy dream sessions and of the
memory-extractor and checkpoint-summary child sessions, whatever their session type. It leaves
their directories on disk, and this boundary keeps the daemon from cataloging them again.
This is permanent behavior, not a temporary compatibility shim. See Memory removal for everything the upgrade deletes.
Session types
CompozyOS records why a session exists:
| Type | Meaning |
|---|---|
user | Normal interactive work created by a person or client. |
system | Internal CompozyOS-managed work. |
coordinator | Managed autonomy coordinator for workspace-scoped coordinated task runs. |
spawned | Child session created by an agent through safe spawn. |
A directory whose metadata names any other type is not recovered; see Session directories the daemon does not recover.
Coordinator sessions are root lineage rows. They are created only after executable coordinated work is enqueued, not when a task is merely created. Spawned sessions record parent, root, depth, role, TTL, and permission metadata so CompozyOS can reap children when their TTL expires or their parent stops.
System sessions created for a Loop Goal record the session that started the Loop as creation provenance when one is available. That relationship groups the Goal under its origin in session views, but it does not make the Goal a safe-spawn child: it has no inherited spawn permissions, TTL, child cap, or parent-stop behavior. Deleting the origin keeps the Goal session and its recorded lineage; views show it as a root when the recorded parent is no longer available.
Next steps
- Use Resume Attach and Replay when you need to attach to an eligible live session or reconstruct prior work from durable reads.
- Use Event Streaming when you need the exact stored and streamed session record.
- Use Context compaction when a session is rebuilt into a new agent session or the agent compacts its own context.
- Use Runtime Autonomy for coordinator, task lease, and safe-spawn behavior.
- Use Permissions when you need to control or approve agent actions.