Skip to content

Session lifecycle

How CompozyOS creates, activates, stops, and classifies one durable runtime session.

For people running agent work14 pages in this section

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…

CompozyOS creates an active logical session without starting ACP. Its first prompt binds a runtime; stopping normally finalizes the session through `stopping`.
StateMeaningValid live transition
startingA prompt to a stopped session is restoring the ACP process and durable provider history.starting -> active
activeCompozyOS 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
stoppingCompozyOS has accepted a stop request and is draining the session toward a stopped state.stopping -> stopped
stoppedFinal 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-review

Create 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 ID
  • workspace_path: an absolute filesystem path

Behind that request, CompozyOS:

  1. Resolves the workspace and agent definition.
  2. Creates ~/.compozy/sessions/<session-id>/.
  3. Opens events.db for the session.
  4. Persists the catalog row and session metadata.
  5. Activates the logical session with runtime.status: "unbound".
  6. Returns HTTP/UDS 201 Created with state: "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> and parent_session_id on POST /api/sessions link 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:

KindMeaning
""A root session with no parent relation.
provenanceA user session created with a parent (the default for --parent).
recoveryA user session created to recover a failed parent.
spawnA governed spawned or spawn-role session.
continueA session continued from another session with another agent.
forkA 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 recovery

The 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 fast
curl -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=true

Use --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_id identifies the authored transcript row, while idempotency_key identifies the submission. Keep both values and reuse them together only when retrying the exact same request.
  • runtime is a prompt-bound snapshot of provider, 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_message only 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_at on real ACP events and metadata-only waiting heartbeats, then emits lower-frequency runtime_progress events. 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 by HEARTBEAT.md wake decisions and the compozy session health|status|inspect surfaces.

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.generation and structured runtime.recovery attempt timing
  • durable runtime_recovery_started, runtime_recovery_succeeded, and runtime_recovery_exhausted events

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 json

Automatic 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=true

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

FieldMeaning
supports_load_sessionThe agent can reopen a stored conversation with session/load.
supports_fork_sessionThe agent advertises the unstable session/fork capability.
supports_resume_sessionThe agent advertises the session/resume capability.
prompt_image, prompt_audio, prompt_embedded_contextPrompt content the agent accepts.
supported_modes, config_optionsSession 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.

ModeCLIHTTP shapeResult
queuecompozy session prompt <id> "<text>" --queuePOST /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.
interruptcompozy session prompt <id> "<text>" --interrupt --expected-turn-id turn_1234POST /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".
steercompozy session prompt <id> "<text>" --steer --expected-turn-id turn_1234POST /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_1234

edit 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:

OperationEndpoint
ListGET /api/workspaces/:workspace/sessions/:id/prompt/queue
EditPUT /api/workspaces/:workspace/sessions/:id/prompt/queue/:queue_entry_id
Promote to steerPOST /api/workspaces/:workspace/sessions/:id/prompt/queue/:queue_entry_id/steer with expected_turn_id
CancelDELETE /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_1234

The 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 all

The 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  admitted

HTTP 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_01J9R2N3V0QF
Forked 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  staged

HTTP 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:

StateMeaning
pendingThe clone exists and has not been loaded yet.
loadedThe first prompt loaded the clone; the carried context was not sent.
failedThe 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 json

For 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-1234

The matching HTTP and UDS operation is:

curl -X POST http://localhost:2123/api/workspaces/ws_alpha/sessions/sess-1234/archive

Archive 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 --stop

The 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-1234

The 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:

TimeoutWhat it protects
session.limits.timeoutDeprecated and not enforced; removed in v0.5.0. Ends no session.
session.supervision.quiet_afterSilence in an in-flight turn before one warning; zero disables the watchdog.
session.supervision.stop_graceContinued turn silence after the warning before the standard stop ladder.
session.supervision.idle_release_afterIdle time before the agent process is released; zero disables the release.
session.supervision.prompt_deadlineOptional total prompt budget; zero disables it.
session.supervision.timeout_cancel_graceGrace after explicit prompt-budget cancellation.
session.stop.cooperative_graceCooperative phase of the stop ladder, followed by bounded forced/kill phases.
Session lifecycle timeoutRecorder 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 reasonWhen CompozyOS uses it
completedThe agent finished normally.
owner_releasedA managed owner, such as a Loop, no longer needs the session. This records automatic retirement; the task and Loop keep their independent outcomes.
user_canceledA normal user stop request.
max_iterationsA user stop request carried max_iterations detail.
loop_detectedA user stop request carried loop_detected detail.
budget_exceededA user stop request carried budget_exceeded detail.
timeoutAn explicit runtime budget expired, or a silent in-flight turn completed its warning/grace path (stop_detail: inactivity).
errorStart or stop failed, or ACP reported a transport failure and the subprocess then exited successfully with code 0.
agent_crashedThe 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_stoppedA required lifecycle hook denied continuation.
shutdownThe 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-1234

Removing persisted history

Remove a session only when its durable history is no longer needed:

compozy session remove sess-1234
curl -X DELETE http://localhost:2123/api/workspaces/ws_alpha/sessions/sess-1234

Removal 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 kind
  • summary: bounded redacted diagnostic text
  • crash_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 kindMeaning
startup_failureCompozyOS could not finish preparing, launching, or initializing the provider.
handshake_failureThe subprocess launched but ACP initialization did not complete.
load_session_failureAn internal daemon-owned provider reload path failed. This is not resume attach.
protocol_failureACP returned a structured protocol/request error outside a prompt-specific path.
prompt_failureACP prompt submission or prompt streaming failed.
cancellationThe operation was canceled by user action or context cancellation.
permission_failureA lifecycle hook or permission boundary denied continuation.
process_exitThe provider subprocess exited unexpectedly or returned a wait error.
transport_failureThe ACP stdio transport closed or failed while CompozyOS expected protocol traffic.
timeoutRuntime supervision or lifecycle timeout expired.
unknown_failureCompozyOS 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 active becomes stopped with agent_crashed, process_exit, and detail daemon crashed while session active
  • stale stopping becomes stopped with agent_crashed, process_exit, and detail stop did not complete
  • stale starting becomes stopped with error, startup_failure, and detail start 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 ID
  • events.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, or spawned, such as the dream type of the removed memory feature (reason: unknown_session_type)
  • an internal spawn role of the removed memory feature, memory-extractor or checkpoint-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:

TypeMeaning
userNormal interactive work created by a person or client.
systemInternal CompozyOS-managed work.
coordinatorManaged autonomy coordinator for workspace-scoped coordinated task runs.
spawnedChild 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.

On this page