Files
openclaw/docs/web/webchat.md
Josh Lehman 0a8e3604ba refactor: flip sessions and transcripts to sqlite storage (#98236)
* refactor(sessions): migrate runtime storage to sqlite

* test(sessions): fix sqlite CI regressions

* test(sessions): align remaining sqlite fixtures

* fix(codex): require sqlite trajectory recorder

* test(sessions): align orphan recovery sqlite fixture

* test(sessions): align sqlite rebase fixtures

* fix(sessions): finish current-main integration of the sqlite flip

Resolve the whole-store SDK removal across its owner boundary: drop the
loadSessionStore re-export and the registry whole-store wrappers, wire
hasTrackedActiveSessionRun into gateway chat, complete the
preserveLockedHarnessIds cleanup contract, flip the codex thread-history
import to storePath targets, and port remaining main-side tests from
file-store helpers to session accessor reads.

* chore: drop committed pebbles log, revert plugin-inspector bump, refresh generated docs

Remove the 1.8k-line .pebbles/events.jsonl work log from the branch, restore
the plugin-inspector advisory lane to main's pinned 0.3.10 so the supply-chain
bump gets its own review, and regenerate docs_map, the plugin SDK API baseline,
and the export-surface ratchet for the merged tree.

* feat(sessions): keep archived transcripts by default with zstd cold storage

Codex-style retention: deleting or resetting a session archives its
transcript as a zstd-compressed JSONL artifact (plain when the runtime
lacks node:zlib zstd) and keeps it until the disk budget evicts oldest
first. resetArchiveRetention now governs both deleted and reset archives
and defaults to keep; maxDiskBytes defaults to 2gb so retention stays
bounded, with archives evicted before live sessions. The cron reaper
follows the same knob instead of deleting archives on its own timer.

* fix(state): converge agent DB migration lineages and bound database growth

Merge coherence: run both structure-gated legacy memory-schema repairs
(flip-lineage drop, main-lineage identity rebuild) before the flip
migration so pre-flip v1/v2 and pre-merge flip v1/v4 databases all
converge, and hoist foreign_keys=OFF outside the schema transaction
where the pragma was silently ignored and the v1 sessions rebuild
cascade-deleted session_entries.

Growth guards: fresh agent DBs enable auto_vacuum=INCREMENTAL, WAL
maintenance releases freed pages in bounded passes (never a blocking
full VACUUM), and doctor reports state/agent DB bloat from freelist
stats.

* fix(codex): resolve the store path for thread-history import via the SDK

The supervision catalog passed the legacy sessionFile locator to the
storePath-targeted transcript mirror; resolve the agent store path with
the session-store SDK helper instead of a runtime-object seam so test
fakes and headless callers need no extra surface. Drop the obsolete
missing-session-id preprocessing case: sessions rows are NOT NULL on
session_id and upsert repairs id-less patches at write time.

* fix(sessions): fail safe on malformed disk-budget config and doctor stat errors

A malformed explicit maxDiskBytes disables the budget instead of
falling back to the destructive 2gb default the user never chose, and
the doctor bloat check skips databases whose paths stat-fail instead of
aborting doctor.

* fix(sessions): complete sqlite conflict translations

* test(sqlite): align hardening checks with maintenance

* test(sessions): inspect compressed transcript archives

* fix(tests): await session seeds and drop unused helpers flagged by CI lint

The five unawaited writeSessionStoreSeed calls raced their SQLite seeds
against the assertions, failing compact shards; the bloat probe drops a
useless initializer and the merged tests drop now-unused helpers.

* test(sessions): type legacy proof events directly

* test(sessions): align hardening contracts

* perf(sessions): read usage transcript sizes from SQL aggregates

Usage/cost scans walked every session and materialized every transcript
event just to re-stringify it for a byte estimate — the #86718 stall
class reborn on the DB. readTranscriptStatsSync sums stored JSON bytes
in SQLite without loading a single row.

* fix(sessions): re-root foreign-root transcript paths onto the current sessions dir

Restored backups, moved OPENCLAW_STATE_DIR, and rehearsal copies carry
absolute sessionFile paths from the old root; the containment fallback
kept those foreign paths, so migration read (and would archive) files in
the original root and reported local copies missing. Re-root the
canonical agents/<id>/sessions suffix onto the current dir when the file
exists there; genuine cross-root layouts still fall through unchanged.

* test(agents): seed harness admission through sqlite

* fix(sqlite): close agent db on pragma setup failure

* fix(doctor): compact and retrofit incremental auto-vacuum after session import

The migration is the sanctioned offline window: post-import compact
reclaims import churn and applies auto_vacuum=INCREMENTAL to databases
created before the fresh-DB pragma existed, so runtime maintenance can
release pages in bounded passes on every install.

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-07-11 14:50:37 -07:00

7.7 KiB

summary, read_when, title
summary read_when title
Loopback WebChat static host and Gateway WS usage for chat UI
Debugging or configuring WebChat access
WebChat

Status: the macOS/iOS SwiftUI chat UI talks directly to the Gateway WebSocket. No embedded browser, no local static server.

What it is

  • A native chat UI for the gateway.
  • Uses the same sessions and routing rules as other channels.
  • Deterministic routing: replies always go back to WebChat.
  • History is always fetched from the gateway (no local file watching). If the gateway is unreachable, WebChat is read-only.

Quick start

  1. Start the gateway.
  2. Open the WebChat UI (macOS/iOS app) or the Control UI chat tab.
  3. Ensure a valid gateway auth path is configured (shared-secret by default, even on loopback).

How it works

  • The UI connects to the Gateway WebSocket and uses the chat.history, chat.send, chat.inject, and chat.message.get RPC methods.
  • chat.history is bounded for stability: Gateway may truncate long text fields, omit heavy metadata, and replace oversized entries with [chat.history omitted: message too large]. API clients can send a per-request maxChars to override the default limit for one call.
  • When a visible assistant message was truncated in chat.history, Control UI can open a side reader and fetch the full display-normalized entry on demand through chat.message.get, without increasing the default history payload. chat.message.get uses the same transcript branch and display rules as chat.history, but targets one entry by messageId and returns an honest unavailable reason when the full content can no longer be returned.
  • chat.history follows the active transcript branch for append-only session files, so abandoned rewrite branches and superseded prompt copies are not rendered in WebChat.
  • Compaction entries render as a "Compacted history" divider explaining that the compacted transcript is preserved as a checkpoint, with an action to open session checkpoints (branch or restore, when permissions allow).
  • Control UI remembers the backing Gateway sessionId returned by chat.history and includes it on follow-up chat.send calls, so reconnects and page refreshes continue the same stored conversation unless the user starts or resets a session.
  • chat.send takes an idempotency key (Control UI uses the run id); the Gateway dedupes repeated requests that reuse the same key, so retried or duplicate in-flight submits for the same session/message/attachments do not create a second run.
  • Workspace startup files and pending BOOTSTRAP.md instructions are supplied through the agent system prompt's # Project Context section, not copied into the WebChat user message. If bootstrap content is truncated, the system prompt gets a short "Bootstrap Context Notice" instead; detailed counts and config knobs stay on diagnostic surfaces.
  • Display normalization on chat.history strips: runtime-only OpenClaw context, inbound envelope wrappers, inline delivery directive tags such as [[reply_to_current]], [[reply_to:<id>]], and [[audio_as_voice]], plain-text tool-call XML payloads (<tool_call>, <function_call>, <tool_calls>, <function_calls>, including truncated blocks), and leaked ASCII/full-width model control tokens. Assistant entries whose whole visible text is only the silent token NO_REPLY (case-insensitive) are omitted.
  • Reasoning-flagged reply payloads (isReasoning: true) are excluded from WebChat assistant content, transcript replay text, and audio content blocks, so thinking-only payloads do not surface as visible assistant messages or playable audio.
  • chat.inject appends an assistant note directly to the transcript and broadcasts it to the UI (no agent run).
  • Aborted runs can keep partial assistant output visible in the UI. Gateway persists that partial text into transcript history when buffered output exists, and marks the entry with abort metadata.

Transcript and delivery model

WebChat has two separate data paths:

  • The SQLite transcript rows are the durable model/runtime transcript. For normal agent runs, the embedded OpenClaw runtime persists model-visible user, assistant, and toolResult messages through the session accessor. WebChat does not write arbitrary delivery, status, or helper text into that transcript.
  • Gateway ReplyPayload events are the live delivery projection: normalized for WebChat/channel display, block streaming, directive tags, media embedding, TTS/audio flags, and UI fallback behavior. They are not themselves the canonical session log.
  • Harnesses that require visible replies through tools.message still use WebChat as a current-run internal source reply sink. A targetless message.send from that active WebChat run is projected into the same chat and mirrored to the session transcript; WebChat does not become a reusable outbound channel and never inherits lastChannel.
  • WebChat injects assistant transcript entries only when the Gateway owns a displayed message outside a normal embedded agent turn: chat.inject, non-agent command replies, aborted partial output, and WebChat-managed media transcript supplements.
  • If live assistant text appears during a run but disappears after history reload, check in order: whether the SQLite transcript contains the assistant text, whether chat.history display projection stripped it, then whether the Control UI optimistic-tail merge replaced local delivery state with the persisted snapshot.

Normal agent-run final answers should be durable because the embedded runtime writes the assistant message_end. Any fallback that mirrors a delivered final payload into the transcript must first avoid duplicating an assistant turn that the embedded runtime already wrote.

Control UI agents tools panel

  • The Control UI /agents Tools panel has an "Available Right Now" view backed by tools.effective(sessionKey=...): a server-derived, read-only projection of the current session's tool inventory, including core, plugin, channel-owned, and already-discovered MCP server tools.
  • A separate config-editing view (backed by tools.catalog) covers profiles, per-agent overrides, and catalog semantics.
  • Runtime availability is session-scoped. Switching sessions on the same agent can change the "Available Right Now" list. If configured MCP servers have not been connected or changed since the last discovery, the panel shows a notice instead of silently starting MCP transports from the read path.
  • The config editor does not imply runtime availability; effective access still follows policy precedence (allow/deny, per-agent and provider/channel overrides).

Remote use

  • Remote mode tunnels the gateway WebSocket over SSH/Tailscale.
  • You do not need to run a separate WebChat server.

Configuration reference (WebChat)

Full configuration: Configuration

WebChat has no persisted config section. Gateway uses the built-in chat.history display limit; API clients can send per-request maxChars to override it for a single call. Legacy channels.webchat and gateway.webchat config is retired; run openclaw doctor --fix to remove it.

Related global options:

  • gateway.port, gateway.bind: WebSocket host/port.
  • gateway.auth.mode, gateway.auth.token, gateway.auth.password: shared-secret WebSocket auth.
  • gateway.auth.allowTailscale: browser Control UI chat tab can use Tailscale Serve identity headers when enabled.
  • gateway.auth.mode: "trusted-proxy": reverse-proxy auth for browser clients behind an identity-aware non-loopback proxy source (see Trusted Proxy Auth).
  • gateway.remote.url, gateway.remote.token, gateway.remote.password: remote gateway target.
  • session.*: session storage and main key defaults.