Define the canonical TypeScript interfaces and types for every persistent artifact in the store layer.
Data Models
Core persistence type system for OpenClaw’s infrastructure layer
Every value that OpenClaw writes to disk or holds in memory conforms to one of these types. Understanding them is the fastest way to navigate the codebase.
📚 What is this & why does it matter?
Data models define the shape of everything persisted to disk or held in memory across the store layer. Getting the types right prevents runtime errors, makes the codebase self-documenting, and ensures that every reader of a JSON file or SQLite row knows exactly what fields to expect and what invariants hold.
💡 Real-world analogy
Think of data models like architectural blueprints for a building. Every room (data field) has a purpose, every door (interface) connects rooms in specific ways, and every load-bearing wall (required field) must remain in place. Change a blueprint incorrectly and walls don’t align—rename a field without updating consumers and the app crashes at runtime.
Contents
Overview
Raw JSON from disk, SQLite rows, environment variables, and in-memory state from upstream modules.
Versioned schemas (version: 1) enable forward-compatible migrations. Optional fields use ? to distinguish missing from empty.
Typed objects consumed by query builders, migration scripts, delivery queues, and session managers throughout the infra layer.
erDiagram
DeviceAuthStore ||--o{ DeviceAuthEntry : contains
DeviceAuthStore {
number version
string deviceId
record tokens
}
DeviceAuthEntry {
string tokenKey
unknown tokenValue
}
QueuedDeliveryPayload ||--|{ QueuedDelivery : extends
QueuedDeliveryPayload {
string channel
string to
string accountId
array payloads
string threadId
string replyToId
boolean bestEffort
boolean gifPlayback
boolean forceDocument
boolean silent
boolean mirror
}
QueuedDelivery {
string id
number enqueuedAt
number retryCount
number lastAttemptAt
string lastError
}
SessionEntry {
string sessionId
number updatedAt
string sessionFile
string channel
string chatType
string execHost
string modelOverride
number inputTokens
number outputTokens
string spawnedBy
number spawnDepth
string subagentRole
}
ExecApprovalsFile ||--o{ ExecApprovalRequest : tracks
ExecApprovalsFile {
number version
string socket
object defaults
object agents
}
ExecApprovalRequest {
string id
object request
number createdAtMs
number expiresAtMs
}
ExecApprovalResolved {
string id
string decision
string resolvedBy
number ts
}
The ER diagram above shows how the six core model families relate. DeviceAuthStore aggregates token entries, QueuedDelivery extends the payload envelope with retry metadata, and the approvals file tracks pending and resolved execution requests.
Decision Points
| Decision | What controls it | Why it matters |
|---|---|---|
| Schema version check | version field on DeviceAuthStore / ExecApprovalsFile | Allows the migration layer to detect stale formats and upgrade them before any field access occurs. |
| Optional vs. required fields | TypeScript ? modifier on interface members | Distinguishes “not yet set” from “explicitly empty,” preventing null-pointer bugs in downstream consumers. |
| Retry gating | retryCount and lastAttemptAt on QueuedDelivery | Delivery queue uses these to implement exponential back-off without an external scheduler. |
| Approval expiry | expiresAtMs on ExecApprovalRequest | Stale approval requests are garbage-collected, preventing orphan records from accumulating. |
How It Works
Every model in the store layer follows the same lifecycle: define a TypeScript interface, export it, and consume it in exactly one reader function and one writer function. This ensures a single source of truth for every field name and type.
Device Auth Store
The DeviceAuthStore interface wraps all device-level credentials into a single versioned record. The tokens map uses Record<string, unknown> to allow heterogeneous token formats across providers.
/** Versioned device credential schema */
interface DeviceAuthStore {
version: 1;
deviceId: string;
tokens: Record<string, unknown>;
}
/** Individual token entry with CRUD helpers */
interface DeviceAuthEntry {
tokenKey: string;
tokenValue: unknown;
}
function loadDeviceAuthToken(
store: DeviceAuthStore,
key: string
): DeviceAuthEntry | undefined {
const value = store.tokens[key];
if (value === undefined) return undefined;
return { tokenKey: key, tokenValue: value };
}
function storeDeviceAuthToken(
store: DeviceAuthStore,
key: string,
value: unknown
): void {
store.tokens[key] = value;
}
function clearDeviceAuthToken(
store: DeviceAuthStore,
key: string
): boolean {
if (!(key in store.tokens)) return false;
delete store.tokens[key];
return true;
}
version: 1 literal type ensures that code handling a DeviceAuthStore object will not compile if handed a future schema version without an explicit migration step.
Delivery Queue Models
The delivery system uses a two-layer type hierarchy. QueuedDeliveryPayload captures the intent (what to deliver, where, and how), while QueuedDelivery extends it with operational metadata used by the retry engine.
interface QueuedDeliveryPayload {
channel: string;
to: string;
accountId?: string;
payloads: unknown[];
threadId?: string;
replyToId?: string;
bestEffort?: boolean;
gifPlayback?: boolean;
forceDocument?: boolean;
silent?: boolean;
mirror?: boolean;
}
interface QueuedDelivery extends QueuedDeliveryPayload {
id: string;
enqueuedAt: number;
retryCount: number;
lastAttemptAt?: number;
lastError?: string;
}
retryCount exceeds the configured maximum, the delivery is moved to a dead-letter directory rather than deleted, preserving the payload for manual inspection.
Session Entry
Each active or historical session is represented by a SessionEntry record. The store layer persists these to a JSON5-formatted session store file, keyed by sessionId.
interface SessionEntry {
sessionId: string;
updatedAt: number;
sessionFile?: string;
channel?: string;
chatType?: string;
execHost?: string;
modelOverride?: string;
inputTokens?: number;
outputTokens?: number;
spawnedBy?: string;
spawnDepth?: number;
subagentRole?: string;
}
Execution Approvals
The approvals subsystem tracks pending and resolved execution requests. ExecApprovalsFile is the top-level container persisted to disk, while request and resolution records are indexed by id.
Utility Types
Two small but critical utility types round out the model layer:
| Type | Fields | Purpose |
|---|---|---|
ArchiveExtractLimits | maxArchiveBytes?, maxEntries?, maxExtractedBytes?, maxEntryBytes? | Safety constraints passed to the archive extraction pipeline to prevent zip-bomb attacks. |
SafeOpenResult | handle: FileHandle, realPath: string, stat: Stats | Returned by openFileWithinRoot() after path validation, symlink rejection, and size checks. |
SystemPresence | host?, ip?, version?, platform?, text, ts | Broadcast record used by the presence heartbeat system to announce node status. |
HeartbeatEventPayload | ts, status, to?, channel?, indicatorType? | Event tracking payload emitted on every heartbeat tick for monitoring dashboards. |
Validation Pipeline
flowchart TD
A["Raw JSON from disk"] --> B["JSON.parse / JSON5.parse"]
B --> C{"version field present?"}
C -- Yes --> D["Match version to schema"]
C -- No --> E["Reject: invalid format"]
D --> F{"Schema matches current?"}
F -- Yes --> G["Zod validation pass"]
F -- No --> H["Run migration pipeline"]
H --> G
G --> I{"All required fields valid?"}
I -- Yes --> J["Return typed object"]
I -- No --> K["Return validation error"]
E --> K
The validation pipeline runs every time a model is loaded from disk. Raw JSON is parsed, version-checked, optionally migrated, then validated through a Zod schema. Only fully validated objects are returned to callers; everything else surfaces as a structured error.
Annotated Code Walkthrough
Walk through how the approval models are defined and consumed end-to-end.
Define the top-level approvals container with a version discriminator and optional configuration sections:
interface ExecApprovalsFile {
version: 1;
socket?: string;
defaults?: Record<string, unknown>;
agents?: Record<string, unknown>;
}
interface ExecApprovalRequest {
id: string;
request: ExecApprovalRequestPayload;
createdAtMs: number;
expiresAtMs: number;
}
interface ExecApprovalResolved {
id: string;
decision: "approved" | "denied";
resolvedBy?: string;
ts: number;
}
The load function reads the approvals file, validates its version, and returns a typed result or null:
async function loadApprovalsFile(
filePath: string
): Promise<ExecApprovalsFile | null> {
const raw = await readJsonFile<unknown>(filePath);
if (!raw || typeof raw !== "object") return null;
const obj = raw as Record<string, unknown>;
if (obj.version !== 1) return null;
return obj as ExecApprovalsFile;
}
version !== 1 check ensures that if a future version introduces breaking changes, the current code fails fast rather than silently misinterpreting fields.
Presence and heartbeat records are emitted by the broadcast system and consumed by monitoring dashboards:
interface SystemPresence {
host?: string;
ip?: string;
version?: string;
platform?: string;
text: string;
ts: number;
}
interface HeartbeatEventPayload {
ts: number;
status: string;
to?: string;
channel?: string;
indicatorType?: string;
}
Edge Cases & Error Handling
| Scenario | Behavior | Code Location |
|---|---|---|
Missing version field | Loader returns null; caller logs warning and creates a fresh default store | store/device-auth.ts:42 |
Unknown token key in DeviceAuthStore | loadDeviceAuthToken() returns undefined; no error thrown | store/device-auth.ts:58 |
retryCount exceeds max | Delivery moved to dead-letter queue with lastError preserved | store/delivery-queue.ts:91 |
| Expired approval request | Garbage collector removes records where Date.now() > expiresAtMs | store/exec-approvals.ts:115 |
Archive exceeds maxArchiveBytes | Extraction aborted immediately; partial files cleaned up | store/archive.ts:67 |
Configuration
| Knob / Parameter | Default | Description |
|---|---|---|
maxArchiveBytes | 100 * 1024 * 1024 | Upper bound on compressed archive size before extraction is refused. |
maxEntries | 10000 | Maximum number of entries to extract from a single archive. |
maxExtractedBytes | 500 * 1024 * 1024 | Cumulative size limit for all extracted content. |
maxEntryBytes | 50 * 1024 * 1024 | Size limit for any single extracted file. |
Related Topics
See also:
- store overview — the parent sub-module page
- State Migrations — how legacy formats are detected and upgraded
- Query Builder — atomic I/O operations that read and write these models
Directory Map
Source files for this topic
src/infra/store/
├── device-auth.ts ← DeviceAuthStore & DeviceAuthEntry types + CRUD helpers
├── delivery-queue.ts ← QueuedDeliveryPayload, QueuedDelivery, enqueue/ack/fail
├── session-store.ts ← SessionEntry type & JSON5 session store I/O
├── exec-approvals.ts ← ExecApprovalsFile, ExecApprovalRequest, ExecApprovalResolved
├── archive.ts ← ArchiveExtractLimits type & extraction pipeline
├── fs-safe.ts ← SafeOpenResult type & validated file handle helpers
├── presence.ts ← SystemPresence broadcast record
└── heartbeat.ts ← HeartbeatEventPayload event tracking type