Clapilot-Agent Sessions

Native session identity, multi-user behavior, diagnostics, and cross-session memory boundaries.

Generated explanatory diagrams

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.

Session identity mapping
Session identity mapping

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

History replay and compaction
History replay and compaction

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

Session diagnostics and audit
Session diagnostics and audit

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.mjs
  • src/lib/agent-runtime/sessions.ts

Core tables

The session layer writes to:

  • agent_session_state current session identity and runtime state
  • agent_runs one row per execution attempt
  • agent_events lifecycle, tool, channel, and warning/error events
  • agent_context_messages, agent_context_summaries, agent_context_items lossless 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:

ShapeUsed forNotes
agent:<agentId>:openai-user:<session_user>Web chat, team chat, and mapped/approved channel threadsCompatibility key family that keeps continuity with the older OpenClaw-derived chat identity model
agent:<specialistHandle>:openai-user:<chat-boundary>Specialized agent runsIsolates specialist history from the main agent's session
channel:<channel>:<threadKey>Unmapped Telegram/Slack/WhatsApp threadsChannel-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 jobsOne session per job
heartbeat:user:<userId> / heartbeat:teamchatHeartbeat runsDedicated per-scope sessions, see Heartbeat
system sessionsRuntime-owned background/system workchannel_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_key
  • user_id
  • chat_session_id
  • group_room_id
  • channel_type
  • channel_thread_key
  • configured_model
  • effective_model
  • effective_provider_config_id
  • backend_provider
  • prompt_files_hash
  • bootstrap_meta
  • state_json
  • last_run_id
  • last_activity_at
  • created_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_id and chat_session_id are preserved if a later run omits them
  • state_json is 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 from chat_sessions
  • if chat_session_id is missing, it re-resolves it from the compatibility session identity when possible
  • if state_json.currentContext is 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_id and reuse that user's main web-chat session boundary
  • mapped channel groups carry the selected team-room identity and the built-in global_team_service principal
  • 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_user from the compatibility-style session_key

That is why the Sessions admin page can show:

  • native session state
  • matching chat_sessions row
  • 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:

  1. runtime system prompt
  2. bootstrap prompt files
  3. retrieved durable memory
  4. compacted session summary, if one exists
  5. recent completed run history
  6. 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_runs is the audit trail for executions
  • agent_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_scope private, team, workspace, channel
  • subject_refs contextual tags such as mandantId, 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_refs match the active session context
  • when no structured subject_refs match 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.lastFlushedAt
  • memory.flush.lastFilePath
  • session.compaction.compactedThroughRunCount
  • session.compaction.summaryActive
  • session.compaction.lastCompactedAt
  • memory.shared.teamEntries
  • memory.contextGraph.messages
  • memory.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:

PassWhenEffect
Boot recoveryOnce at service startA 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 fallbackAfter recovery claimsA 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 sweepEvery 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, and acp_agent (shown as Clapilot-code (Coding), Claude CLI Bridge, Codex App Server, and Claude 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, or orchestrator_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: