Clapilot-Agent Sessions
Native session identity, multi-user behavior, diagnostics, and cross-session memory boundaries.
Session architecture explained visually
These raster diagrams are generated documentation assets and are paired with exact captions below so the page is readable even when a tiny in-image label is hard to inspect.

Maps web chats, team rooms, channel threads, jobs, and system automations into concrete native session keys and session state.

Explains how completed runs become replay context, when memory flush and compaction happen, and how the next provider prompt is assembled.

Connects session state to runs, events, context graph tables, chat fallbacks, bridge diagnostics, and the admin Sessions inspector.
Sessions are the native runtime’s continuity boundary. A session answers a practical question: "Should this new run remember the same things as the previous run?"
That boundary decides which runs belong together, which history is replayed, which compaction state is active (compaction = rewriting older turns into a persisted summary so long conversations still fit the model budget), which lossless context graph belongs to the conversation, and which shared memories are visible.
In day-to-day debugging, session identity is the first thing to verify when a chat, channel, or background job appears to remember too much, forget too much, or continue the wrong conversation.
Primary implementation:
services/clapilot-agent/src/sessions/index.mjssrc/lib/agent-runtime/sessions.ts
Core tables
The session layer writes to:
agent_session_statecurrent session identity and runtime stateagent_runsone row per execution attemptagent_eventslifecycle, tool, channel, and warning/error eventsagent_context_messages,agent_context_summaries,agent_context_itemslossless per-session context graph
For channel sessions it also uses:
agent_channel_threads
Session key formats
A session key is the string identity that groups runs into one conversation. The runtime supports multiple native session-key shapes:
| Shape | Used for | Notes |
|---|---|---|
agent:<agentId>:openai-user:<session_user> | Web chat, team chat, and mapped/approved channel threads | Compatibility key family that keeps continuity with the older OpenClaw-derived chat identity model |
agent:<specialistHandle>:openai-user:<chat-boundary> | Specialized agent runs | Isolates specialist history from the main agent's session |
channel:<channel>:<threadKey> | Unmapped Telegram/Slack/WhatsApp threads | Channel-local fallback identity before an approval maps the thread |
clapilot:group:<roomId> | Team-chat rooms (also the target of mapped channel groups) | Used as the session_user boundary inside the compatibility key |
native-job:<id> | Scheduled native jobs | One session per job |
heartbeat:user:<userId> / heartbeat:teamchat | Heartbeat runs | Dedicated per-scope sessions, see Heartbeat |
| system sessions | Runtime-owned background/system work | channel_type = system |
Examples of channel sessions: a Telegram chat/thread, a Slack thread, a WhatsApp chat or group JID, a Telegram DM mapped onto a specific user's main chat, or a Telegram/Slack/WhatsApp group mapped onto clapilot:group:<roomId>.
Session state lifecycle
Every run upserts a row in agent_session_state.
Important persisted fields:
session_keyuser_idchat_session_idgroup_room_idchannel_typechannel_thread_keyconfigured_modeleffective_modeleffective_provider_config_idbackend_providerprompt_files_hashbootstrap_metastate_jsonlast_run_idlast_activity_atcreated_at,updated_at
state_json is where the runtime stores operational session state such as:
- memory flush metadata
- compaction summary and counters
- other evolving maintenance state
The upsert behavior is additive:
- known fields such as
user_idandchat_session_idare preserved if a later run omits them state_jsonis merged instead of replaced
The runtime now also performs best-effort session self-healing on every new run:
- if an older session row is missing
user_id, it re-resolves the owning user fromchat_sessions - if
chat_session_idis missing, it re-resolves it from the compatibility session identity when possible - if
state_json.currentContextis stale or empty, it backfills a useful context from the current request or the most recent usable context for the same user
This matters because old sessions created before a context fix should recover on the next turn instead of forcing users to start a brand new chat.
Multi-user support
Multi-user support exists at the session table level through user_id.
Current behavior:
- web chat sessions usually carry a real Clapilot
user_id - unmapped channel sessions operate purely by external thread identity
- mapped channel DMs carry the linked Clapilot
user_idand reuse that user's main web-chat session boundary - mapped channel groups carry the selected team-room identity and the built-in
global_team_serviceprincipal - session summaries can also store
user_id
This means the runtime can distinguish:
- one user with multiple web sessions
- many external channel threads with no authenticated app user
- system and job sessions outside normal user chat
Relation to chat_sessions
The native runtime does not replace Clapilot’s chat_sessions; it layers on top of them.
For web chat diagnostics, the app resolves the native session back to chat_sessions either by:
- direct
chat_session_id, or - deriving
openclaw_session_userfrom the compatibility-stylesession_key
That is why the Sessions admin page can show:
- native session state
- matching
chat_sessionsrow - fallback transcript from
chat_nachrichten - recent native runs and events together
Session keys on the filesystem
The runtime never uses raw session keys as filesystem names. Where per-session state must be written to disk — for example embedded-run attachment staging under .clapilotaicore/attachments/ — the directory name is a 16-hex-character SHA-256 prefix of the native session_key. This keeps filesystem state stable across restarts without exposing raw user/session identifiers as file names.
History replay
At run time, the session layer assembles context from:
- runtime system prompt
- bootstrap prompt files
- retrieved durable memory
- compacted session summary, if one exists
- recent completed run history
- current user message
Recent history is loaded from agent_runs, not only from the app chat transcript.
That matters because native channel and job sessions may never touch chat_sessions or chat_nachrichten.
Text-only native runs also persist input_payload.promptCacheReplay: the exact augmented provider turn containing that turn's runtime-generated time/retrieval/learning context and raw user request, together with a hash of the stable model/system/tool prefix. When the hash still matches, history replay uses the augmented turn instead of reconstructing its context, so the next provider request extends the previous serialized prompt and can reuse local KV/prefix cache. A changed stable prefix, compaction boundary, model, page context, or tool surface falls back to raw user/assistant replay and begins a new cache lineage. Older runs without replay metadata remain compatible through the same raw fallback.
Externally delivered assistant posts are merged into replayed run history when they were recorded in agent_context_messages with run_id IS NULL, role = assistant, and metadata.direction = output. This covers approved channel sends, team-chat automation delivery, and personal-chat automation delivery that land in a conversation without being the final output of that conversation's own run. These posts do not change run-count compaction bookkeeping, but their text and sent-media file references are included in prompt context.
Persistent subscription bridges also count those external output posts when deciding whether the conversation moved on after the last bridge turn. If a Codex or Claude CLI thread is resumed after an external assistant post, the runtime rebuilds the visible transcript instead of continuing the stale bridge thread with only the latest user message.
If the Codex app-server reports a persisted bridge thread as missing (thread not found / missing rollout), the run treats that as a recoverable stale provider binding. The stale agent_external_sessions row is closed and its provider thread/session reference is cleared only when the stored thread id still matches the failed thread, the original turn is replayed once on a newly created Codex bridge thread, and agent_events records run.thread_recovered with the old and new thread ids plus the recovery attempt count. A second stale-thread failure in the same run is not retried.
Lossless session graph
In addition to agent_runs, the runtime now keeps a lossless per-session context graph.
The graph stores:
- raw normalized turn items in
agent_context_messages - leaf and condensed summaries in
agent_context_summaries - ordered assembly state in
agent_context_items
The graph is not the same as agent_runs:
agent_runsis the audit trail for executionsagent_context_*is the context-management layer for later search and expansion
Cross-session shared memory
Cross-session recall no longer depends only on memory_scope.
agent_memories now also carries:
visibility_scopeprivate,team,workspace,channelsubject_refscontextual tags such asmandantId,documentId,taskId,moduleSlug,routePath,groupRoomId,channelThreadKey
Practical behavior:
- a finished session can write team-visible durable facts
- another authenticated session or active channel session can see them on the next model turn
- retrieval prefers entries whose
subject_refsmatch the active session context - when no structured
subject_refsmatch exists, strong lexical/entity matches can still surface team facts across sessions - unrelated contextual team memory is still de-prioritized
Session maintenance
Two maintenance passes run in the detached post-response pipeline when needed:
- memory flush
- history compaction
History compaction uses provider turns capped at 45 seconds and 4,000 input tokens by default. If a provider still times out or fails, the runtime writes a bounded deterministic continuity summary for that chunk, checkpoints it, and completes compaction instead of discarding the maintenance pass. lastDeterministicFallbackCount records that degraded-but-complete path in session state.
Neither pass may delay run.started or the foreground provider request. Runs use the last completed compaction summary and independently cap and token-fit recent history. Maintenance is single-flight per session; requests arriving during an active pass coalesce into one trailing pass with the latest completed transcript. Both passes write their status back into agent_session_state.state_json, and session.maintenance.completed records their background outcome.
The most important diagnostics exposed by session_status are:
- current channel/model
- runtime path / maintenance owner
- recent history depth
memory.flush.lastFlushedAtmemory.flush.lastFilePathsession.compaction.compactedThroughRunCountsession.compaction.summaryActivesession.compaction.lastCompactedAtmemory.shared.teamEntriesmemory.contextGraph.messagesmemory.contextGraph.summaries
Runs and events
agent_runs stores execution records such as:
- source type
chat,job,channel,email,heartbeat,api - status
queued,running,completed,failed,cancelled,timed_out - requested and effective model
- immutable initiating actor (
actor_user_id) when a user triggered the run - prompt files used
- tool calls
- output text
- usage
- error state (
error_code,error_message)
agent_events stores:
- channel ingress
- lifecycle markers
- tool events
- warnings and errors
This is the main audit trail for native runtime debugging.
agent_session_state.actor_user_id is intentionally mutable for a shared Team Chat session because it represents the member driving the current turn. Each new agent_runs row therefore snapshots that verified actor into its own actor_user_id; retries may fill a previously reserved null value but never replace an existing actor. Personal export authorization and tool-result redaction use this per-run identity, not the session's latest actor. Migration 255 backfills only recovery-envelope actors and stable non-Team session owners, leaving ambiguous historical Team Chat runs unattributed instead of guessing.
Interactive native chat/channel turns are serialized per session_key. If a
second turn arrives while another turn for the same session is active, the
runtime keeps it in a FIFO queue and starts it automatically after the active
turn completes instead of returning "Session verarbeitet bereits einen Turn".
Webchat rows remain visible as assistant_pending while their queued turn waits.
When the caller provides a stored chat messageId/idempotencyKey, agent_runs
stores it in input_payload.idempotencyKey; duplicates for the same session
reuse the existing in-flight or completed run instead of executing the message
twice.
Run lifecycle reconciliation
Because clapilot-agent is the only executor of agent_runs, a row that is still queued or running after a runtime restart can never complete on its own. The run reconciler (services/clapilot-agent/src/jobs/run-reconciler.mjs) keeps run state honest:
| Pass | When | Effect |
|---|---|---|
| Boot recovery | Once at service start | A pre-boot queued/running run with an idempotency key and persisted input_payload.recoveryEnvelope is atomically claimed with FOR UPDATE SKIP LOCKED, requeued on the same agent_runs.id, and resumed through the normal per-session FIFO. Recovery is attempted at most twice. Actual execution emits run.restart_recovery.attempted followed by completed or failed; a pre-execution abort emits skipped instead. These events include attempt, maxAttempts, and reason; completion also reports status and deduped, while failure/skip events include error and errorCode. A superseded orchestrator turn is cancelled with error_code = 'restart_recovery_superseded'; other execution failures use restart_recovery_failed. Heartbeat policy skips remain cancellation-only. |
| Boot reconcile fallback | After recovery claims | A run without a replay-safe envelope is marked failed with error_code = 'orphaned_restart'; an envelope that exhausts recovery is marked failed with error_code = 'restart_recovery_exhausted' and emits run.restart_recovery.exhausted without an attempt event. |
| Stuck sweep | Every CLAPILOT_AGENT_RUN_SWEEP_MS (default 10 min, min 60 s) | Runs whose updated_at has not moved for CLAPILOT_AGENT_RUN_MAX_AGE_MINUTES (default 360, min 30) are marked failed with error_code = 'stuck_timeout' |
Long tool-heavy runs keep refreshing updated_at through run patches, so the stuck sweep only catches genuinely dead executions (hung provider call, lost worker).
The recovery envelope contains normalized messages, identity/session scope, model,
tool profile, and UI context, but no stream callbacks or provider secrets. Graceful
shutdown changes recoverable active rows back to queued instead of terminally
cancelling them. A recovered result includes a localized restart notice, and the
existing chat read-time reconciliation backfills that result when the original
browser stream no longer exists. Mutating operations still require their own stable
idempotency boundary: Canvas PDF export uses the durable run ID as
dokumente.agent_idempotency_key, a unique owner-scoped database key, and a stable
PDF path so replay cannot create a second Documents row. Canvas file creation and
template rendering likewise derive a deterministic file path from the durable run
and operation arguments, so replay updates the original artifact instead of
allocating a second filename.
The app side complements this with read-time self-healing of chat placeholders: when a personal or group chat transcript is loaded, assistant messages still flagged assistant_pending whose row has not been touched for 30 minutes (personal chat, chat_nachrichten) or 15 minutes (group chat, chat_group_messages) are expired — the pending flag is cleared, an interruption notice is filled in when the answer is empty, and message_meta.assistantRunExpired is set. "Preparing response" bubbles therefore cannot survive web or agent-runtime restarts forever.
Session admin page
The ClapilotAICore Sessions page is designed as a diagnostics view over the native session layer plus channel-bound interactive bridge sessions.
In addition to rows backed by agent_session_state, the page now also includes thread-bound Agent Orchestrator / Codex app-server sessions from agent_external_sessions when a Telegram, Slack, or WhatsApp thread was routed into the interactive coding path before a native agent_session_state row existed.
It exposes:
- session identity classification
webchat,group,channel,job,system - execution-harness diagnostics in addition to the session runtime path
the persisted bridge provider adapters are
embedded_pi,claude_cli_bridge,codex_app_server, andacp_agent(shown asClapilot-code (Coding),Claude CLI Bridge,Codex App Server, andClaude Code Agent); the internal adapter value remains unchanged - bridge-session diagnostics when applicable provider adapter, bridge status, preferred harness session id, repo, workspace, external thread id, and external session id
- user and chat-session mapping
- current model assignment
- runtime path
native,embedded_pi, ororchestrator_bridge; the first two unchanged wire values are displayed as Clapilot-code (Assistant) and Clapilot-code (Coding) - run counters and last status
- last observed prompt-budget telemetry context-window size, prompt-budget size, estimated prompt load, replayed-tail size, provider token usage, and model-limit source
- session-maintenance telemetry compaction counts, compaction engine, last trigger, last compacted turn counts, memory-flush counts, skip reasons, and summary-active state
- current safeguard strategy snapshot shipped model-fallback usage vs generic emergency fallback, flush threshold, preserved-tail size, compaction summary target, retry guard, and lossless-context thresholds
- shared fact counts
- lossless context graph counts
- persisted compaction summary preview
- raw
bootstrap_meta - raw
state_json - recent runs/events
- fallback app transcript when applicable
Use this page when you need to answer questions like:
- why did a channel thread reuse an old context?
- which model actually answered in this session?
- did compaction already happen?
- how full was the prompt budget on the last observed run?
- how often did memory flush / compaction fire for this session?
- did the native runtime compact, or did the embedded runtime own the compaction cycle?
- which coding harness actually answered this session, and what external session/thread id is it currently bound to?
- is this session tied to a Clapilot user or only to an external thread?
Related docs:
