Files
openclaw/docs/concepts/agent-loop.md
Peter Steinberger 4273ca9dbd refactor(sessions): remove file-era transcript runtime (#113233)
* refactor(sessions): keep helper transcripts in memory

* refactor(sessions): remove file-era transcript storage

* test(sessions): use SQLite identity in attempt persistence

* test(codex): isolate legacy transcript fixtures

* fix(sessions): preserve SQLite transcript identity

* fix(sessions): harden transcript lifecycle invariants

* fix(sessions): validate transcript identities

* fix(sessions): close identity compatibility gaps

* fix(sessions): preserve leaf and plugin identities

* fix(sessions): retain dispatch transcript targets

* fix(sessions): preserve active transcript context

* fix(sessions): isolate artifact accounting

* fix(sessions): bound SQLite usage accounting

* fix(sessions): retain bounded latest usage

* fix(sessions): align rebased transcript targets

* test(sessions): align accessor scope fixture

* fix(telegram): derive SQLite transcript identity

* refactor(sessions): remove file-era compaction residue

* chore(sessions): lower max-lines baseline

* fix(sessions): preserve structured transcript identity

* test(sessions): align doctor identity assertions

* fix(sessions): isolate default SDK database

* refactor(sessions): remove dead file-era exports

* fix(sessions): reconcile SQLite transcript identity

* fix(sessions): pass checkpoint identity explicitly

* test(sessions): make entry field probe explicit

* test(sessions): satisfy transcript cleanup lint

* test(sessions): align diagnostics identity proof

* fix(sessions): finish transcript runtime teardown

* fix(sessions): preserve transcript identity invariants

* fix(sessions): harden transcript compatibility edges

* fix(sessions): preserve checkpoint transcript anchors

* fix(sessions): preserve SQLite lifecycle invariants

* fix(sessions): retarget compaction successors

* test(sessions): preserve transcript fixture semantics

* feat(plugin-sdk): add command transcript targets

* fix(sessions): serialize transcript rewrites

* fix(sessions): validate legacy successor identity

* fix(sessions): normalize compaction ownership

* fix(sessions): validate successor identity before adoption

* fix(sessions): preserve plugin transcript ownership

* fix(sessions): carry transcript identity through commands

* fix(sessions): import legacy checkpoint artifacts into SQLite

* fix(sessions): preserve successor transcript ownership

* fix(sessions): align transcript consumers with target identity

* fix(sessions): scope transcript token estimates

* fix(sessions): retain agent identity across lifecycle hooks

* fix(sessions): resolve scoped SQLite targets

* fix(sessions): isolate lifecycle transcript targets

* fix(sessions): validate compaction agent ownership

* fix(sessions): preserve reset and cleanup lifecycle

* fix(sessions): serialize prompt cleanup lifecycle

* fix(sessions): remove stale lock import

* fix(sessions): preserve reset target context

* fix(sessions): fence prompt reload takeover

* fix(sessions): unblock abort and default lifecycle reads

* fix(sessions): validate legacy successor scope

* fix(sessions): reject metadata-only runtime rows

* fix(sessions): propagate custom transcript stores

* fix(sessions): preserve adopted retry targets

* fix(sessions): allow unkeyed usage reads

* fix(sessions): harden runtime target boundaries

* fix(sessions): serialize retry transcript writes

* fix(sessions): bound prompt reload disposal

* fix(sessions): complete retry marker identity

* fix(sessions): keep legacy marker identity minimal

* test(sessions): tighten teardown fixture types

* fix(sessions): preserve compatibility target identity

* test(sessions): persist post-checkpoint boundary turn

* test(sessions): align runtime store mock contracts

* style(sessions): simplify persisted identity guard

* fix(sessions): prefer complete typed targets

* fix(sessions): recover legacy marker targets

* test(sessions): align marker lookup fixture scope

* fix(sessions): validate partial transcript targets

* fix(sessions): reconcile partial transcript identities

* fix(sessions): canonicalize compatibility identities

* test(sessions): cover compatibility aliases

* fix(sessions): adopt legacy successor identity

* fix(sessions): preserve usage read identity

* fix(sessions): preserve partial marker compatibility

* fix(sessions): validate legacy successor mappings

* fix(sessions): reconcile marker store mappings

* fix(sessions): preserve legacy fallback identity

* fix(sessions): harden marker alias resolution

* fix(sessions): prefer verified successor aliases

* fix(sessions): resolve preferred marker aliases

* fix(sessions): serialize cleanup admission

* fix(sessions): align marker lookup scopes

* fix(codex): type marker alias summaries

* style(sessions): satisfy changed lint

* test(sessions): align structured target assertions

* fix(sessions): reconcile latest identity contracts

* fix(sessions): validate transcript identity boundaries

* docs(sessions): explain stable registry keys

* fix(sessions): harden compatibility target round trips

* fix(sessions): port usage identity to split modules

* test(sessions): align subagent transcript identity

* fix(sessions): finish transcript identity migration

* fix(agents): route subagent completion capture through transcript targets

* fix(agents): settle SQLite prompt handoff during cleanup

* chore: shrink max-lines baseline after teardown

* fix(sessions): port teardown across split runtime owners

* fix(sessions): carry transcript targets through split owners

* test(agents): use SQLite compaction target in abort coverage

* chore: retain unrelated max-lines suppressions

* chore: shrink max-lines baseline after main splits

* style(agents): const compaction checkpoint locals

* fix(sessions): harden SQLite teardown boundaries

* test(sessions): use typed metadata in predicate isolation fixture

* test(agents): cover malformed settlement rejections lint-safely

* fix(sessions): close remaining SQLite identity races

* fix(agents): fail closed on incomplete successor targets

* fix(sessions): preserve transcript identity fallbacks

* fix(agents): preserve session-key abort admission

* fix(trajectory): validate incomplete export targets

* test(sessions): drop retired pricing cache imports

* fix(sessions): validate partial transcript identities

* fix(sessions): close transcript identity edge cases

* fix(plugins): reserve retired transcript locator slot

* fix(sessions): scope transcript locks by target

* style(sessions): simplify SDK initialization error

* fix(sessions): preserve initialized transcript state

* fix(codex): verify mirrored history session keys

* fix(sessions): reject stale transcript ownership

* fix(sessions): anchor asynchronous transcript ownership

* fix(sessions): measure active transcript state

* fix(sessions): preserve scoped transcript compaction

* fix(sessions): harden transcript identity and lifecycle

* fix(sessions): resolve scoped command transcript stores

* fix(sessions): make transcript appends failure-atomic

* fix(sessions): enforce scoped transcript ownership

* fix(sessions): reject cross-owner transcript handoffs

* fix(sessions): fence cleanup transcript ownership

* fix(sessions): retire stale write ownership contexts

* fix(sessions): preserve pending session migration state

* fix(sessions): validate migrated transcript ownership

* fix(sessions): validate usage transcript targets

* fix(sessions): clear predecessor transcript metadata

* fix(sessions): align durable session event targets

* fix(sessions): fence late prompt handoffs

* fix(sessions): fence lifecycle transcript fallbacks

* fix(sessions): bound zero-length memory capture

* fix(sessions): preserve transcript teardown ownership

* fix(sessions): reject duplicate cleanup ownership

* fix(sessions): serialize runtime writes with sqlite leases

* fix(sessions): close sqlite teardown concurrency gaps

* fix(sessions): preserve nested lifecycle failures

* fix(sessions): canonicalize sqlite transcript ownership

* fix(sessions): settle disposed prompt handoffs

* fix(sessions): resolve canonical attempt lock targets

* test(sessions): align canonical target fixtures

* test(sessions): retire redundant jsonl parser coverage

* refactor(sessions): split active transcript cursors

* test(memory): retire legacy marker fixture

* fix(sessions): preserve canonical transcript access after rebase

* fix(sessions): fence prompt lease and return transcript targets

* fix(sessions): colocate transcript leases with target store

* fix(sessions): canonicalize transcript lease and worker targets

* fix(sessions): preserve plugin and fork identity markers

* fix(sessions): complete sqlite transcript target migration

* fix(sessions): integrate canonical followup identity

* fix(sessions): preserve bounded transcript topology

* fix(sessions): validate transcript identity boundaries

* fix(context): separate caller and successor targets

* test(sessions): split persistence compatibility coverage

* test(sessions): preserve fixture topology efficiently

* chore(sdk): refresh plugin api baseline

* test(agents): align compaction lock target mocks

* test(sessions): seed malformed transcript fixtures directly

* fix(agents): canonicalize transcript compatibility inputs

* fix(agents): type optional tool result ids

* test(ci): stabilize loaded process timing

* test(tui): wait for collect queue admission
2026-07-27 22:33:24 -04:00

21 KiB

summary, read_when, title
summary read_when title
Agent loop lifecycle, streams, and wait semantics
You need an exact walkthrough of the agent loop or lifecycle events
You are changing session queueing, transcript writes, or session write lock behavior
Agent loop

The agent loop is the serialized, per-session run that turns a message into actions and a reply: intake, context assembly, model inference, tool execution, streaming, persistence.

Entry points

  • Gateway RPC: agent and agent.wait.
  • CLI: openclaw agent.

Run sequence

  1. agent RPC validates params, resolves the session (sessionKey/sessionId), persists session metadata, and returns { runId, acceptedAt } immediately.
  2. agentCommand runs the turn: resolves model + thinking/verbose/trace defaults, loads the skills snapshot, calls runEmbeddedAgent, and emits a fallback lifecycle end/error if the embedded loop did not already emit one.
  3. runEmbeddedAgent: serializes runs via per-session and global queues, resolves model + auth profile, builds the OpenClaw session, subscribes to runtime events, streams assistant/tool deltas, enforces the run timeout (aborting on expiry), and returns payloads plus usage metadata. For Codex app-server turns it also aborts an accepted turn that stops producing app-server progress before a terminal event.
  4. subscribeEmbeddedAgentSession bridges runtime events to the agent stream: tool events to stream: "tool", assistant deltas to stream: "assistant", lifecycle events to stream: "lifecycle" (phase: "start" | "end" | "error").
  5. agent.wait (waitForAgentRun) waits for lifecycle end/error on a runId and returns { status: ok|error|timeout, startedAt, endedAt, error? }.

Queueing and concurrency

Runs are serialized per session key (session lane) and optionally through a global lane, preventing tool/session races. Messaging channels choose a queue mode (steer/followup/collect/interrupt) that feeds this lane system; see Command Queue.

Transcript writes are serialized by the per-session lane and the SQLite writer queue. Each append or rewrite validates the current session identity inside its synchronous commit transaction, so a stale run cannot overwrite a newer session generation.

Session and workspace preparation

  • Workspace is resolved and created; sandboxed runs may redirect to a sandbox workspace root.
  • Skills are loaded (or reused from a snapshot) and injected into env and prompt.
  • Bootstrap/context files are resolved and injected into the system prompt.
  • A session write lock is acquired and the session transcript target is prepared before streaming starts. Any later transcript rewrite, compaction, or truncation path must take the same lock before mutating the SQLite transcript rows.

Prompt assembly

System prompt is built from OpenClaw's base prompt, skills prompt, bootstrap context, and per-run overrides. Model-specific limits and compaction reserve tokens are enforced. See System prompt for what the model sees.

Hooks

OpenClaw has two hook systems:

  • Internal hooks (Gateway hooks): event-driven scripts for commands and lifecycle events.
  • Plugin hooks: extension points inside the agent/tool lifecycle and gateway pipeline.

Internal hooks (Gateway hooks)

  • agent:bootstrap: runs while building bootstrap files before the system prompt is finalized. Use it to add or remove bootstrap context files.
  • Command hooks: /new, /reset, /stop, and other command events (see the Hooks doc).

See Hooks for setup and examples.

Plugin hooks

These run inside the agent loop or gateway pipeline:

Hook Runs
before_model_resolve Pre-session (no messages), to deterministically override provider/model before resolution.
before_prompt_build After session load (with messages), to inject prependContext, systemPrompt, prependSystemContext, or appendSystemContext before submission. Use prependContext for per-turn dynamic text and the system-context fields for stable guidance that belongs in system prompt space.
before_agent_reply After inline actions, before the LLM call. Lets a plugin claim the turn and return a synthetic reply or silence it entirely.
agent_end After completion, with the final message list and run metadata.
before_compaction / after_compaction Observe or annotate compaction cycles.
before_tool_call / after_tool_call Intercept tool params/results.
before_install After operator install policy runs, on staged skill/plugin install material, when plugin hooks are loaded in the current process.
tool_result_persist Synchronously transforms tool results before they are written to an OpenClaw-owned session transcript.
message_received / message_sending / message_sent Inbound and outbound message hooks.
session_start / session_end Session lifecycle boundaries.
gateway_start / gateway_stop Gateway lifecycle events.

Hook decision rules for outbound/tool guards:

  • before_tool_call: { block: true } is terminal and stops lower-priority handlers. { block: false } is a no-op and does not clear a prior block.
  • before_install: same terminal/no-op semantics as above. Use security.installPolicy, not before_install, for operator-owned install allow/block decisions that must cover CLI install and update paths.
  • message_sending: { cancel: true } is terminal and stops lower-priority handlers. { cancel: false } is a no-op and does not clear a prior cancel.

See Plugin hooks for the hook API and registration details.

Harnesses can adapt these hooks. The Codex app-server harness keeps OpenClaw plugin hooks as the compatibility contract for documented mirrored surfaces; Codex native hooks are a separate, lower-level Codex mechanism.

Streaming

  • Assistant deltas stream from the agent runtime as assistant events.
  • Block streaming can emit partial replies on text_end or message_end.
  • Reasoning streaming can be a separate stream or block replies.
  • See Streaming for chunking and block reply behavior.

Tool execution

  • Tool start/update/end events emit on the tool stream.
  • Tool results are sanitized for size and image payloads before logging/emitting.
  • Messaging tool sends are tracked to suppress duplicate assistant confirmations.

Reply shaping

Final payloads are assembled from assistant text (plus optional reasoning), inline tool summaries (when verbose and allowed), and assistant error text when the model errors.

  • The exact silent token NO_REPLY is filtered from outgoing payloads.
  • Messaging tool duplicates are removed from the final payload list.
  • If no renderable payloads remain and a tool errored, a fallback tool error reply is emitted unless a messaging tool already sent a user-visible reply.

Compaction and retries

Auto-compaction emits compaction stream events and can trigger a retry. On retry, in-memory buffers and tool summaries reset to avoid duplicate output. See Compaction.

Event streams

  • lifecycle: emitted by subscribeEmbeddedAgentSession (and as a fallback by agentCommand).
  • assistant: streamed deltas from the agent runtime.
  • tool: streamed tool events from the agent runtime.

The Gateway projects lifecycle and tool start/terminal events into the bounded, metadata-only audit ledger. This projection records provenance and result codes without copying prompts, messages, tool arguments, tool results, or raw errors out of the transcript/runtime path.

Chat channel handling

Assistant deltas buffer into chat delta messages. A chat final is emitted on lifecycle end/error.

Timeouts

Timeout Default Notes
agent.wait 30s Wait-only; timeoutMs param overrides. Does not stop the underlying run.
Agent runtime (agents.defaults.timeoutSeconds) 172800s (48h) Enforced by runEmbeddedAgent's abort timer. Set 0 for an unlimited run budget; model stream liveness watchdogs still apply.
CLI backend no-output watchdog computed per fresh/resumed CLI run Separate from the agent runtime and owned by the registered backend plugin. A CLI-internal background task shares the parent subprocess and does not outlive an overall agent timeout.
Cron isolated agent turn owned by cron The scheduler starts its own timer when execution begins, aborts the run at the configured deadline, then runs bounded cleanup before recording the timeout so a stale child session cannot keep the lane stuck.
Model idle timeout Cloud 120s; self-hosted 300s OpenClaw aborts a model request when no response chunks arrive before the idle window. models.providers.<id>.timeoutSeconds extends this idle watchdog for slow local/self-hosted providers, but stays bounded by any lower finite agents.defaults.timeoutSeconds or run-specific timeout, since those govern the whole agent run. Unlimited run budgets still keep the provider-class idle watchdog. Cron-triggered cloud model runs with no explicit model/agent timeout use the same default; with an explicit cron run timeout, cloud model stream stalls cap at 60s so configured model fallbacks can still run before the outer cron deadline. Cron-triggered runs on genuinely local endpoints (loopback/private baseUrl) keep the local idle opt-out; self-hosted providers on network baseUrls get the 300s implicit watchdog. With an explicit cron run timeout, local/self-hosted stalls cap at that timeout. Set models.providers.<id>.timeoutSeconds for slow local providers.
Provider HTTP request timeout models.providers.<id>.timeoutSeconds Covers connect, headers, body, SDK request timeout, guarded-fetch abort handling, and the model stream idle watchdog for that provider. Use for slow local/self-hosted providers (for example Ollama) before raising the whole agent runtime timeout; keep the agent/runtime timeout at least as high when the model request needs to run longer.

Stuck session diagnostics

With diagnostics enabled, a built-in two-minute threshold classifies long processing sessions with no observed reply, tool, status, block, or ACP progress:

  • Active embedded runs, model calls, and tool calls report as session.long_running. Owned silent model calls stay session.long_running until the abort threshold so slow or non-streaming providers are not flagged as stalled too early.
  • Active work with no recent progress reports as session.stalled. Owned model calls switch to session.stalled at or after the abort threshold; ownerless stale model/tool activity is not hidden as long-running.
  • session.stuck is reserved for recoverable stale session bookkeeping, including idle queued sessions with stale ownerless model/tool activity.

The abort threshold is at least 5 minutes and 3x the warning threshold. Stale session bookkeeping releases the affected session lane immediately after recovery gates pass; stalled embedded runs are abort-drained only after the abort threshold, so queued work resumes without cutting off merely slow runs. Recovery emits structured requested/completed outcomes; diagnostic state is marked idle only if the same processing generation is still current, and repeated session.stuck diagnostics back off while the session stays unchanged.

Where things can end early

  • Agent timeout (abort)
  • AbortSignal (cancel)
  • Gateway disconnect or RPC timeout
  • agent.wait timeout (wait-only, does not stop the agent)
  • Tools - available agent tools
  • Hooks - event-driven scripts triggered by agent lifecycle events
  • Compaction - how long conversations are summarized
  • Exec Approvals - approval gates for shell commands
  • Thinking - thinking/reasoning level configuration