Abstract: A source-level tour from chat admission to model tools, SQLite, recovery, and delivery.

Consider a failure after the model has requested a tool but before the final reply reaches the browser. The UI has already reported that the run started, yet the process must now decide what can be recovered or retried.

The system needs more than the prompt. It needs the accepted user turn, a stable run ID, the session binding, knowledge of whether a side effect committed, the transcript position, and enough delivery state to decide whether to resume, retry, suppress, or send. A compact ReAct loop does not answer any of those questions.

SOUL.md defines an agent's voice; the long-lived control plane determines turn ownership, state placement, authority, recovery, and delivery.

Scope and execution architecture

OpenClaw is a local-first agent system organized around an always-on Gateway. The Gateway multiplexes RPC, HTTP endpoints, plugin routes, the Control UI, channel delivery, and agent lifecycle events. Each configured agent has a workspace and a private state domain.

It is not merely:

  • a system prompt with a tool list;
  • a safe container by default;
  • a memory library;
  • or one giant agent loop that also happens to speak to chat services.

The built-in model/tool loop is real and relatively compact. Most of the system exists around it: admission, identity, routing, session ownership, configuration snapshots, policy composition, persistence, streaming, recovery, and delivery.

This article traces the current chat.send path used by the Gateway chat surface. Other channel adapters do not all enter through that exact handler, but they converge on the same reply runtime and many of the same session, agent, and delivery contracts.

End-to-end path.

The execution chain is:

chat.send
  → normalize request and resolve session
  → pre-admission and admission
  → persist restart-safe user turn
  → ACK { runId, status: "started" }
  → detached dispatchInboundMessage
  → prepareAgentCommandExecution
  → acquire session work admission
  → runEmbeddedAgent
  → session lane → global lane → session write lock
  → build workspace context, tools, and system prompt
  → AgentSession.prompt
  → provider stream
  → assistant text or toolCall
  → policy, validation, and tool execution
  → ordered toolResult messages
  → next model turn
  → SessionManager persistence
  → assistant/tool/lifecycle event projection
  → final delivery

The important architectural fact is that no single function owns all of this. Ownership changes deliberately at each boundary.

Admission and acknowledgement.

handleChatSend first normalizes the request, prepares the session, and runs pre-admission and admission. Restart-safe admission defines the relevant durability boundary.

Before returning a successful acknowledgement, OpenClaw persists the user turn and checks that:

  • a durable record exists;
  • the admitted session entry is marked running;
  • and its recovery delivery run ID equals the client run ID.

Only then does the handler respond:

const ackPayload = {
  runId: clientRunId,
  status: "started" as const,
};

respond(true, ackPayload, undefined, { runId: clientRunId });

The expensive work continues in a detached dispatch. This ordering gives started a stronger meaning than “a promise was placed on an in-memory queue.” There is already enough durable identity for a reconnect or restart path to reason about the accepted turn.

The cost is visible in the handler: the ACK path must coordinate persistence, lifecycle generation, abort state, and routing changes. Fast acknowledgement and durable acknowledgement are not the same design.

Run preparation.

prepareAgentCommandExecution resolves the canonical runtime configuration and validates the requested agent and session identity. It then determines:

  • sessionId, sessionKey, and store path;
  • the agent-specific workspace and agent directory;
  • plugin metadata;
  • configured model and thinking behavior;
  • timeout and lane;
  • delivery context;
  • and a stable runId.

That result is a run envelope, not just a prompt.

agentCommand then acquires session work admission and re-reads the current session entry before doing work. This matters because the world can change between preparation and execution: a session may be reset, archived, rebound, or affected by a routing reload. OpenClaw treats stale session identity as an error instead of executing against the old snapshot.

This is one of the recurring patterns in the codebase: derive intent, acquire ownership, then validate the intent again under that ownership.

Scheduling and serialization.

runEmbeddedAgent resolves the effective session target and enters a per-session lane. Inside it, the run waits for deferred maintenance and then enters a global lane. Transcript mutation paths additionally use a session write lock.

The layers serve different purposes:

  • Session lane: prevents concurrent turns from racing on one conversation.
  • Global lane: applies process-level concurrency control.
  • Session write lock: protects transcript mutation, including writers outside the immediate in-process queue.

This makes ordering understandable, but it is not free. A slow tool, large compaction, or long synchronous commit section can create head-of-line blocking. The documentation includes stuck-session diagnostics because serialized ownership turns “who may write?” into “who is holding everyone else up?”

OpenClaw accepts that cost because a long-lived session cannot safely have two contradictory presents.

Prompt assembly.

The embedded attempt prepares skills, bootstrap files, core and plugin tools, MCP/LSP bundle tools, a provider-visible tool catalog, and the system prompt. Workspace context files are ordered deterministically:

AGENTS.md → SOUL.md → IDENTITY.md → USER.md
→ TOOLS.md → BOOTSTRAP.md → MEMORY.md

That ordering is useful for prompt-cache stability, but it also clarifies ownership. SOUL.md contributes persona and tone; it does not define tool authorization. MEMORY.md is durable context; it is not the session runtime row. The workspace is the tool working directory; it is not automatically an isolation boundary.

prepareEmbeddedAttemptAgentSession eventually constructs an AgentSession with the selected model, active tool allowlist, custom tools, SessionManager, settings, and the session write-lock callback.

The actual provider boundary is in src/agents/sessions/sdk.ts. Agent.streamFn resolves fresh authentication and calls:

modelRegistryRuntime.llmRuntime.streamSimple(
  modelResult,
  context,
  options,
);

OpenClaw wraps this boundary with prompt transforms, cache handling, hooks, tool-result truncation, transport behavior, and diagnostics. The model API call is a replaceable component inside a larger lifecycle.

The inner loop.

The key loop lives in packages/agent-core/src/agent-loop.ts. This is a direct excerpt:

const toolCalls = message.content.filter((c) => c.type === "toolCall");
const toolResults: ToolResultMessage[] = [];
hasMoreToolCalls = false;

if (message.stopReason === "toolUse" && toolCalls.length > 0) {
  const executedToolBatch = await executeToolCalls(
    currentContext,
    message,
    config,
    signal,
    emit,
  );
  toolResults.push(...executedToolBatch.messages);
  hasMoreToolCalls = !executedToolBatch.terminate;

  for (const result of toolResults) {
    currentContext.messages.push(result);
    newMessages.push(result);
  }
}

The preceding call streams an assistant response. Only a completed toolUse turn dispatches tool calls. Each call is resolved, its arguments are prepared and schema-validated, and beforeToolCall may block it. Tools may run sequentially or in parallel.

There is a subtle deterministic choice in the parallel path: completion can happen out of order, but Promise.all reconstructs results in the model's original call order before the ToolResultMessage objects are emitted and appended. Runtime speed does not silently rewrite transcript semantics.

The outer while loop then invokes the model again with those tool results. A prepareNextTurn hook may update context, model, or thinking level between turns. Steering and follow-up queues can also feed new messages into the loop.

The division of labor is clean:

  • agent-core owns model → tool → result → next model;
  • AgentSession owns model-visible message persistence and retry/compaction behavior;
  • the embedded runner owns run setup, streaming, timeouts, and fallback;
  • the Gateway owns admission, session routing, recovery coordination, and delivery.

“Small core” does not mean “small system.” It means complexity has an explicit owner.

Persistence and delivery.

Every completed user, assistant, or tool-result message produces message_end. AgentSessionBase handles that event and calls sessionManager.appendMessage(). That path determines what the model can see on the next turn.

At the same time, subscribeEmbeddedAgentSession projects assistant, tool, and lifecycle events toward the Gateway. The Gateway shapes streaming output and delivers the final reply.

The chat handler contains a valuable warning: do not blindly mirror the agent final into the transcript. The agent runtime already persists model-visible turns; duplicating the final at the Gateway would create two assistant messages.

This is more than deduplication. It gives failure a precise meaning:

  • delivery can fail after the answer has been persisted;
  • a UI can display partial streaming text before terminal state commits;
  • reconnect logic can distinguish replaying events from rerunning the model.

One owner for transcript truth, another for live transport, and an explicit bridge between them is much safer than letting every surface “save the conversation.”

SQLite authority and recovery.

Current main stores each agent's runtime database at:

~/.openclaw/agents/<agentId>/agent/openclaw-agent.sqlite

The schema separates concerns:

  • sessions and session_entries hold lifecycle and routing state;
  • transcript_events stores the event sequence;
  • transcript_event_identities tracks event IDs, parent IDs, and message idempotency keys;
  • archived transcript files remain artifacts;
  • legacy sessions.json is a migration source rather than steady-state authority.

This split also prevents three meanings of “memory” from collapsing:

  1. session state says what the runtime is doing;
  2. transcript state says what the model and tools have said;
  3. durable memory says what should be recalled across conversations.

SQLite provides transactions, indexing, identity constraints, and per-agent isolation. It also creates operational obligations: schema migration, doctor tooling, database maintenance, backup, and transaction duration.

A database cannot correct a faulty retry policy. The open maxTurns/maxToolCalls request, issue #9912, documents why prompt instructions are not a hard loop budget. The open cross-boot recovery budget bug, issue #95750, shows how recovery without a durable ceiling can itself become a restart loop. These issue reports were not independently reproduced here, but they expose design costs still under discussion.

Failure and security boundaries

Persist before acknowledging durable work.

If a client receives a run ID, the server should already possess the identity needed to adopt or terminalize that run. The tradeoff is a more expensive ACK critical path.

Revalidate after acquiring ownership.

Preparation reads are provisional. Session identity, routing generation, and archive state are checked again under admission. This converts races into explicit rejection rather than accidental execution.

Do not retry across a committed side effect.

The model-fallback chain checks whether a command-RPC run has committed a side effect. Once it has, result classification stops proposing fallback and canFallbackAfterError becomes false. Availability yields to at-most-once intent where replay would be dangerous.

Separate capability, placement, and escape.

Tool policy decides which named capabilities exist. Sandbox policy decides where allowed tools run. Elevated mode is an exec-only escape hatch.

The distinction prevents several false assurances. Denying write does not make an allowed exec shell read-only. A workspace is not a sandbox. Elevated mode does not grant tools that policy removed.

Keep live transport out of transcript ownership.

Streaming and persistence have different failure modes. Making the Gateway own delivery and AgentSession own model-visible history allows each to recover without manufacturing duplicate turns.

Deployment fit and open questions

OpenClaw's default direct-message scope can collapse DMs into the main session. That is convenient for one owner moving between surfaces. It is not an appropriate tenant boundary for a shared inbox.

Per-peer scopes improve isolation but reduce automatic continuity. identityLinks can deliberately map the same person across channels. Optional private cross-conversation recall retrieves relevant context without merging transcripts, while groups and channels stay excluded from private recall.

There is no default that maximizes both continuity and isolation. A responsible deployment must choose whose convenience it optimizes.

Deployment fit.

OpenClaw makes sense for self-hosters, technical households, independent operators, and small teams that need one agent to span messages, scheduled work, files, browsers, devices, and long-running sessions.

It may be too heavy if the requirement is a request-scoped backend agent that calls two stateless tools. In that case, the Gateway, migration surface, session routing, database, locks, and security policy are real costs with little return.

Compared with a lightweight ReAct library, OpenClaw buys:

  • cross-surface continuity;
  • durable admission and restart reasoning;
  • session-scoped serialization;
  • explicit execution policy;
  • and source-aware delivery.

It pays with a larger fault domain and a much more demanding operational model.

Operational conclusion.

The social change here is not another chat interface. Traditional applications ask for permission at discrete moments. A persistent agent exercises delegated authority repeatedly, sometimes for months and across several surfaces.

The important questions become:

Whom is this agent representing now?
Which session and policy authorize this action?
What side effect has already committed?
Who retries after failure, and what is the budget?

Persistent-agent deployments therefore require human-readable action receipts, per-person session isolation, side-effect-aware audits, bounded recovery, and handoff rules for households and small organizations.

SOUL.md creates attachment. The Gateway creates switching cost. The durable product moat is likely to be the unglamorous machinery that keeps identity, authority, state, and delivery in agreement.

Source map and version

  1. chat.send admission and ACK
  2. Session, workspace, and model preparation
  3. Run ownership and recovery boundary
  4. Session/global lanes and embedded runtime
  5. The model/tool loop
  6. The provider stream boundary
  7. Side-effect-aware fallback
  8. Per-agent SQLite schema

References and version.

Research date: July 23, 2026. Source commit: 67ef07863fbb24d751e20f022e216118924ae15d, whose package version is 2026.7.2. The npm latest tag was still 2026.7.1-2 on the research date. OpenClaw moves quickly; behavior on main is not automatically behavior in the stable package.

Open questions.

  1. After an irreversible side effect, which failures should terminate rather than trigger any fallback?
  2. Should a personal-agent default optimize for cross-channel continuity or minimum accidental disclosure?
  3. Should an action receipt record the tool call, or the real-world side effect a human can understand?