mirror of
https://github.com/qwibitai/nanoclaw.git
synced 2026-06-04 10:14:47 +08:00
1c7623ca41
- CLAUDE.md: new key files, updated groups verbs, rewritten self-mod section, new Container Config and Container Restart sections - db-central.md: container_configs table (§1.15), migrations 014+015 - db-session.md: messages_in schema with trigger, source_session_id, on_wake columns - schema.ts: comment no longer references disk-based config - cli.instructions.md: rewritten for scope-aware usage, auto-fill, restart/config ops, group-scoped examples Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
187 lines
8.2 KiB
Markdown
187 lines
8.2 KiB
Markdown
# NanoClaw — Per-Session DB Schema
|
|
|
|
Reference for the two SQLite files each session owns: `inbound.db` (host writes, container reads) and `outbound.db` (container writes, host reads). Start with [db.md](db.md) for the three-DB overview, the single-writer rule, and the cross-mount visibility constraints.
|
|
|
|
Schemas live in `src/db/schema.ts` as the `INBOUND_SCHEMA` and `OUTBOUND_SCHEMA` constants. Both files are created by `ensureSchema()` in `src/session-manager.ts` when a new session folder is provisioned.
|
|
|
|
---
|
|
|
|
## 1. Session folder layout
|
|
|
|
```
|
|
data/v2-sessions/<agent_group_id>/<session_id>/
|
|
inbound.db ← host writes, container reads (read-only mount)
|
|
outbound.db ← container writes, host reads (read-only open)
|
|
.heartbeat ← mtime touched by container (not a DB write)
|
|
inbox/<message_id>/ ← user attachments, decoded from inbound message content
|
|
outbox/<message_id>/ ← attachments the agent produced
|
|
```
|
|
|
|
One session = one folder = one pair of DBs. The `agent_group_id` parent directory also holds per-group state (`.claude-shared/`, `agent-runner-src/`) that is shared across every session of that agent group.
|
|
|
|
Path helpers in `src/session-manager.ts`: `sessionDir()`, `inboundDbPath()`, `outboundDbPath()`, `heartbeatPath()`.
|
|
|
|
---
|
|
|
|
## 2. Inbound DB (`inbound.db`)
|
|
|
|
Host-owned, container-read-only. Schema constant: `INBOUND_SCHEMA` in `src/db/schema.ts`.
|
|
|
|
### 2.1 `messages_in`
|
|
|
|
Every message landing in the session: user chat, scheduled task, recurring task, question response, internal system message.
|
|
|
|
```sql
|
|
CREATE TABLE messages_in (
|
|
id TEXT PRIMARY KEY,
|
|
seq INTEGER UNIQUE, -- EVEN only (host assigns) — see §3
|
|
kind TEXT NOT NULL,
|
|
timestamp TEXT NOT NULL,
|
|
status TEXT DEFAULT 'pending', -- pending|completed|failed|paused
|
|
process_after TEXT,
|
|
recurrence TEXT, -- cron expr for recurring
|
|
series_id TEXT, -- groups occurrences of a recurring task
|
|
tries INTEGER DEFAULT 0,
|
|
trigger INTEGER NOT NULL DEFAULT 1, -- 0 = context only (don't wake), 1 = wake agent
|
|
platform_id TEXT,
|
|
channel_type TEXT,
|
|
thread_id TEXT,
|
|
content TEXT NOT NULL, -- JSON; shape depends on kind
|
|
source_session_id TEXT, -- agent-to-agent return path
|
|
on_wake INTEGER NOT NULL DEFAULT 0 -- 1 = only deliver on container's first poll
|
|
);
|
|
CREATE INDEX idx_messages_in_series ON messages_in(series_id);
|
|
```
|
|
|
|
Content shapes: see [api-details.md §Session DB Schema Details](api-details.md#session-db-schema-details).
|
|
|
|
**Writers (host):** `insertMessage()`, `insertTask()`, `insertRecurrence()` — all in `src/db/session-db.ts`. Each calls `nextEvenSeq()`.
|
|
**Reader (container):** `container/agent-runner/src/db/messages-in.ts` — polls `status='pending' AND (process_after IS NULL OR process_after <= now)`.
|
|
|
|
### 2.2 `delivered`
|
|
|
|
Host writes here after handing a `messages_out` row to the channel adapter. Container reads `platform_message_id` to target edits and reactions.
|
|
|
|
```sql
|
|
CREATE TABLE delivered (
|
|
message_out_id TEXT PRIMARY KEY,
|
|
platform_message_id TEXT,
|
|
status TEXT NOT NULL DEFAULT 'delivered', -- delivered|failed
|
|
delivered_at TEXT NOT NULL
|
|
);
|
|
```
|
|
|
|
Writer: `markDelivered()` / `markDeliveryFailed()` in `src/db/session-db.ts`. Older session DBs are brought up to schema lazily by `migrateDeliveredTable()`.
|
|
|
|
### 2.3 `destinations`
|
|
|
|
Projection of the central `agent_destinations` table (see [db-central.md §1.10](db-central.md#110-agent_destinations)) for this session's agent. The container resolves `to="name"` against this table; if the row is absent, the send is rejected as `unknown destination`.
|
|
|
|
```sql
|
|
CREATE TABLE destinations (
|
|
name TEXT PRIMARY KEY,
|
|
display_name TEXT,
|
|
type TEXT NOT NULL, -- 'channel' | 'agent'
|
|
channel_type TEXT, -- for type='channel'
|
|
platform_id TEXT, -- for type='channel'
|
|
agent_group_id TEXT -- for type='agent'
|
|
);
|
|
```
|
|
|
|
Rewritten wholesale (DELETE + INSERT in a transaction) by `writeDestinations()` on every container wake and on demand when wiring changes mid-session. The comment on the table in `src/db/schema.ts` is the canonical statement of the refresh semantics.
|
|
|
|
### 2.4 `session_routing`
|
|
|
|
Single-row (`id=1`) default routing: where outbound messages go when the agent doesn't specify a destination.
|
|
|
|
```sql
|
|
CREATE TABLE session_routing (
|
|
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
channel_type TEXT,
|
|
platform_id TEXT,
|
|
thread_id TEXT
|
|
);
|
|
```
|
|
|
|
Written by `writeSessionRouting()` on every container wake, derived from `sessions.messaging_group_id` + `sessions.thread_id`.
|
|
|
|
---
|
|
|
|
## 3. Sequence numbering invariant
|
|
|
|
Every message (in or out) gets a monotonic integer `seq`, unique *within the session* across both tables.
|
|
|
|
- **Host writes even seq** (2, 4, 6, …) to `messages_in` — `nextEvenSeq()` at `src/db/session-db.ts:75`.
|
|
- **Container writes odd seq** (1, 3, 5, …) to `messages_out` — logic at `container/agent-runner/src/db/messages-out.ts:54` (`max % 2 === 0 ? max + 1 : max + 2`), reading `MAX(seq)` across *both* tables to preserve global ordering.
|
|
|
|
Why disjoint? `seq` is the agent-facing message ID. When the agent calls `edit_message(seq=5)` or `add_reaction(seq=6)`, `getMessageIdBySeq()` uses the parity to route the lookup: odd → `messages_out`, even → `messages_in`. The parity alone disambiguates without a join. Collisions would break editing.
|
|
|
|
If you add a code path that writes to either table, preserve parity — the invariant isn't enforced by a constraint, only by the two helper functions.
|
|
|
|
---
|
|
|
|
## 4. Outbound DB (`outbound.db`)
|
|
|
|
Container-owned, host reads only. Schema constant: `OUTBOUND_SCHEMA` in `src/db/schema.ts`.
|
|
|
|
### 4.1 `messages_out`
|
|
|
|
Everything the agent produces: chat replies, edits, reactions, cards, question sends, agent-to-agent messages, system actions.
|
|
|
|
```sql
|
|
CREATE TABLE messages_out (
|
|
id TEXT PRIMARY KEY,
|
|
seq INTEGER UNIQUE, -- ODD only (container assigns) — see §3
|
|
in_reply_to TEXT,
|
|
timestamp TEXT NOT NULL,
|
|
deliver_after TEXT,
|
|
recurrence TEXT,
|
|
kind TEXT NOT NULL, -- chat|chat-sdk|system|…
|
|
platform_id TEXT,
|
|
channel_type TEXT,
|
|
thread_id TEXT,
|
|
content TEXT NOT NULL -- JSON; operation lives inside (edit/reaction/card/…)
|
|
);
|
|
```
|
|
|
|
Content shapes: see [api-details.md §Session DB Schema Details](api-details.md#session-db-schema-details).
|
|
|
|
**Writer (container):** `writeMessageOut()` in `container/agent-runner/src/db/messages-out.ts`.
|
|
**Readers (host):** `src/delivery.ts` (polling delivery), `getMessageIdBySeq()` / `getRoutingBySeq()` for edit/reaction targeting.
|
|
|
|
### 4.2 `processing_ack`
|
|
|
|
Container-side status for each `messages_in.id` it has touched. The host polls this and syncs status back into `messages_in` — this avoids the container ever writing to `inbound.db`.
|
|
|
|
```sql
|
|
CREATE TABLE processing_ack (
|
|
message_id TEXT PRIMARY KEY,
|
|
status TEXT NOT NULL, -- processing|completed|failed
|
|
status_changed TEXT NOT NULL
|
|
);
|
|
```
|
|
|
|
Crash recovery: on container startup, stale `processing` entries get cleared. Host-side sync: `syncProcessingAcks()` in `src/host-sweep.ts`.
|
|
|
|
### 4.3 `session_state`
|
|
|
|
Persistent container-owned KV store. Main consumer is the Chat SDK session ID — storing it here lets the agent's conversation resume across container restarts. Cleared by `/clear`.
|
|
|
|
```sql
|
|
CREATE TABLE session_state (
|
|
key TEXT PRIMARY KEY,
|
|
value TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL
|
|
);
|
|
```
|
|
|
|
Access: `container/agent-runner/src/db/session-state.ts`.
|
|
|
|
---
|
|
|
|
## 5. Schema evolution
|
|
|
|
Unlike the central DB, session DBs do **not** go through numbered migrations. Both `INBOUND_SCHEMA` and `OUTBOUND_SCHEMA` use `CREATE TABLE IF NOT EXISTS`, so a fresh session always gets the current shape. For session folders created under older builds, column-level gaps are patched lazily on open — e.g. `migrateDeliveredTable()` in `src/db/session-db.ts` adds `platform_message_id` and `status` to the `delivered` table if missing.
|
|
|
|
If you add a column to either schema, add a matching lazy migration for existing session folders, and prefer nullable columns or defaulted values so no data backfill is required.
|