docs: drop v2 framing across CLAUDE.md and 12 docs

Renamed 12 docs/v2-*.md → docs/*.md (already in index from earlier git mv).
Rewrote CLAUDE.md to describe the codebase as just "the codebase" rather
than "v2"; added a "Channels and Providers (skill-installed)" section
reflecting the new model and updated the docs index links.

Agent (general-purpose) cleaned the 12 doc bodies:
- Dropped "NanoClaw v2" / "v2 schema" / "(v2)" prose throughout
- Rewrote inter-doc cross-references docs/v2-X.md → docs/X.md
- Architecture, agent-runner-details: collapsed v1↔v2 comparison tables
  into present-tense facts; added notes that trunk only ships `claude`
  and that channel adapters are skill-installed from the `channels` branch
- Setup-wiring, checklist: dropped v1→v2 migration items that no longer
  apply
- Frozen runtime paths preserved: data/v2.db, data/v2-sessions/,
  container name nanoclaw-v2

git grep confirms remaining `\bv2\b` matches in docs/ are only those
runtime paths.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gavrielc
2026-04-17 14:53:21 +03:00
parent c37609ffc8
commit bfc626be82
13 changed files with 138 additions and 173 deletions
@@ -1,6 +1,6 @@
# NanoClaw v2 Agent-Runner Details
# NanoClaw Agent-Runner Details
Implementation-level details for the agent-runner inside the container. See [v2-architecture-draft.md](v2-architecture-draft.md) for the high-level design.
Implementation-level details for the agent-runner inside the container. See [architecture.md](architecture.md) for the high-level design.
## Separation of Concerns
@@ -8,7 +8,7 @@ The agent-runner has two layers:
1. **Agent-runner core** — owns the poll loop, message formatting, DB reads/writes, MCP tool implementations, routing, status management, media handling. This is NanoClaw-specific and shared across all providers.
2. **Agent provider** — owns the SDK interaction. Takes formatted prompts, pushes them to the SDK, yields events back. Each SDK (Claude, Codex, OpenCode) gets its own provider implementation.
2. **Agent provider** — owns the SDK interaction. Takes formatted prompts, pushes them to the SDK, yields events back. Trunk ships the `claude` provider; additional providers (OpenCode, Codex, etc.) are installed by `/add-<provider>` skills from the `providers` branch.
The boundary: the agent-runner decides **what** to send and **what to do** with results. The provider decides **how** to talk to the SDK.
@@ -91,6 +91,8 @@ type ProviderEvent =
## Provider Implementations
Only the `claude` provider ships in trunk. The Codex and OpenCode sections below document the provider interface for reference and for skills that install additional providers — they are not baked into the core image.
### Claude Provider
Wraps `@anthropic-ai/claude-agent-sdk`'s `query()`.
@@ -445,7 +447,7 @@ pending → processing → completed
### MCP Tools
The agent-runner runs an MCP server (same as v1) that exposes NanoClaw tools to the agent. In v2, all tools write to the session DB instead of IPC files.
The agent-runner runs an MCP server that exposes NanoClaw tools to the agent. All tools write to the session DB.
**DB path:** The MCP server receives the session DB path via environment variable. It opens a second connection to the same SQLite file (WAL mode allows concurrent access).
@@ -691,8 +693,6 @@ For `task` kind messages with a `script` field in the content:
4. If `wakeAgent === false`: mark message as completed, don't invoke the provider
5. If `wakeAgent === true`: enrich the prompt with script output, then invoke the provider
Same as v1 behavior.
### Transcript Archiving
The agent-runner archives conversation transcripts before context compaction. For Claude, this is handled via the PreCompact hook (provider-internal). For other providers that don't have hooks, the agent-runner archives after each query completes based on the provider's output.
@@ -721,15 +721,13 @@ The agent-runner reads config, creates the provider, and enters the poll loop. N
### Provider Factory
```typescript
type ProviderName = 'claude' | 'codex' | 'opencode';
type ProviderName = 'claude' | string;
function createProvider(name: ProviderName, config: ProviderConfig): AgentProvider {
switch (name) {
case 'claude': return new ClaudeProvider(config);
case 'codex': return new CodexProvider(config);
case 'opencode': return new OpenCodeProvider(config);
default: throw new Error(`Unknown provider: ${name}`);
}
// Trunk registers 'claude'; additional providers self-register when installed via skills.
const factory = providerRegistry.get(name);
if (!factory) throw new Error(`Unknown provider: ${name}`);
return factory(config);
}
```
@@ -737,7 +735,7 @@ The provider name comes from the container's environment (`AGENT_PROVIDER` env v
`ProviderConfig` contains provider-specific settings (API keys, model overrides, etc.) passed via environment variables — not via the interface. Each provider reads what it needs from `env`.
## What Stays From v1
## Agent-Runner Properties
- MCP server is a separate Node process spawned by the provider (via `mcpServers` config)
- The MCP server binary is shared across providers — same tools, same DB access
@@ -745,21 +743,7 @@ The provider name comes from the container's environment (`AGENT_PROVIDER` env v
- Additional directories discovery (`/workspace/extra/*`)
- Logging via stderr (`[agent-runner] ...`)
## What Changes From v1
| v1 | v2 |
|----|----|
| stdin JSON envelope | Poll session DB |
| IPC input files for follow-ups | Same DB poll + `provider.push()` |
| stdout markers for output | Write messages_out rows |
| MCP tools write IPC files | MCP tools write DB rows |
| `_close` sentinel for shutdown | Host kills container externally |
| `runQuery()` function with inline Claude SDK | `AgentProvider` interface + per-SDK implementations |
| Single provider (Claude) | Pluggable providers (Claude, Codex, OpenCode, future) |
| `ContainerInput` via stdin | Provider config via env vars + session DB for messages |
| IPC polling for follow-ups | DB polling + provider.push() |
## Related Documents
- **[v2-architecture-draft.md](v2-architecture-draft.md)** — High-level architecture (session DB schema, central DB, channel adapters, message flow)
- **[v2-api-details.md](v2-api-details.md)** — Channel adapter interface, message content examples, host delivery logic
- **[architecture.md](architecture.md)** — High-level architecture (session DB schema, central DB, channel adapters, message flow)
- **[api-details.md](api-details.md)** — Channel adapter interface, message content examples, host delivery logic
@@ -1,10 +1,10 @@
# NanoClaw v2 API Details
# NanoClaw API Details
Implementation-level details for the v2 architecture. See [v2-architecture-draft.md](v2-architecture-draft.md) for the high-level design.
Implementation-level details for the architecture. See [architecture.md](architecture.md) for the high-level design.
## Channel Adapter Interface
### NanoClaw Channel Interface (v2)
### NanoClaw Channel Interface
```typescript
interface ChannelSetup {
@@ -59,7 +59,7 @@ interface OutboundMessage {
### Chat SDK Bridge
Wraps a Chat SDK adapter + Chat instance to conform to the NanoClaw ChannelAdapter interface.
Wraps a Chat SDK adapter + Chat instance to conform to the NanoClaw ChannelAdapter interface. Trunk ships the bridge and the channel registry only — platform-specific Chat SDK adapters (Discord, Slack, Telegram, etc.) and native adapters (WhatsApp/Baileys) are installed by the `/add-<channel>` skills from the `channels` branch.
```typescript
function createChatSdkBridge(
@@ -170,7 +170,7 @@ function createChatSdkBridge(
### Native NanoClaw Channel (no Chat SDK)
Native channels implement the ChannelAdapter interface directly. Example structure for WhatsApp/Baileys:
Native channels implement the ChannelAdapter interface directly. The WhatsApp/Baileys adapter is the canonical example — it ships via the `/add-whatsapp` skill, not in trunk:
```typescript
function createWhatsAppChannel(): ChannelAdapter {
@@ -293,14 +293,14 @@ function createWhatsAppChannel(): ChannelAdapter {
"type": "card",
"title": "Deployment Approval",
"children": [
{ "type": "text", "content": "Deploy v2.1.0 to production?" },
{ "type": "text", "content": "Deploy 2.1.0 to production?" },
{ "type": "actions", "children": [
{ "type": "button", "id": "approve", "label": "Approve", "style": "primary" },
{ "type": "button", "id": "reject", "label": "Reject", "style": "danger" }
]}
]
},
"fallbackText": "Deployment Approval: Deploy v2.1.0 to production? [Approve] [Reject]"
"fallbackText": "Deployment Approval: Deploy 2.1.0 to production? [Approve] [Reject]"
}
```
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>NanoClaw v2 Architecture</title>
<title>NanoClaw Architecture</title>
<script src="https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js"></script>
<style>
:root {
@@ -128,7 +128,7 @@
</head>
<body>
<header>
<h1>NanoClaw v2 Architecture</h1>
<h1>NanoClaw Architecture</h1>
<div class="sub">Session-DB messaging model · Chat SDK bridge · OneCLI credential gateway · per-session containers</div>
<nav>
<a href="#overview">1 · Overview</a>
@@ -396,7 +396,7 @@ flowchart LR
</div>
</section>
<footer>NanoClaw v2 · branch <code>v2</code> · generated from docs/v2-checklist.md, v2-architecture-draft.md, v2-isolation-model.md, v2-setup-wiring.md</footer>
<footer>NanoClaw · generated from docs/checklist.md, architecture.md, isolation-model.md, setup-wiring.md</footer>
</main>
<script>
@@ -1,4 +1,4 @@
# NanoClaw v2 Architecture Diagram
# NanoClaw Architecture Diagram
## System Overview
@@ -1,4 +1,4 @@
# NanoClaw v2 Architecture (Draft)
# NanoClaw Architecture (Draft)
## Core Idea
@@ -176,7 +176,7 @@ messages_out content references filenames only:
No paths in the DB — the convention is the contract. The host reads files from `outbox/{message_id}/` in the mounted session folder and delivers them via the adapter (Chat SDK `FileUpload` with buffer data, or platform-specific upload for native channels). Host cleans up the outbox directory after successful delivery.
Outbound files use a dedicated `send_file` MCP tool (separate from `send_message`). See [v2-agent-runner-details.md](v2-agent-runner-details.md) for the tool interface.
Outbound files use a dedicated `send_file` MCP tool (separate from `send_message`). See [agent-runner-details.md](agent-runner-details.md) for the tool interface.
### Message Deduplication
@@ -390,28 +390,21 @@ The receiving agent gets a normal chat message. It doesn't need to know the sour
This is documented as a pattern, not a built-in feature.
## What Stays the Same
## Core Properties
- Container isolation via filesystem mounts
- Credential proxy (OneCLI)
- Per-agent-group workspace (folder, CLAUDE.md, skills)
- Polling-based (not event-driven)
- Per-agent-group agent-runner recompilation on container startup (agent can modify its own source, request rebuild/restart, changes persist across teardowns)
## What Changes
| Component | v1 | v2 |
|-----------|----|----|
| Host ↔ container IO | stdin + IPC files | Mounted session DB (messages_in / messages_out) |
| Container input | Prompt string piped to stdin | Agent-runner polls messages_in |
| Container output | stdout markers | Agent-runner writes to messages_out |
| Agent commands | IPC JSON files | messages_out with `kind: 'system'` |
| Agent-to-agent | Not supported | messages_out with target agent routing |
| Scheduling | Separate scheduler + task table | `process_after` / `deliver_after` + `recurrence` on messages |
| Media | Not supported | Signed URLs, downloaded in container |
| Channel adapters | Custom per-platform | Chat SDK bridge + standard interface |
| Routing | Host checks registeredGroups map | Channel adapter extracts IDs, host maps to entities |
| Concurrency | GroupQueue (in-memory) | Chat SDK per-channel + container limits |
| Session scoping | One session per agent group folder | Per-session DB, multiple sessions per agent group |
- Host ↔ container IO through mounted session DBs (`messages_in` / `messages_out`) — no stdin piping, no IPC files
- Agent commands are `messages_out` rows with `kind: 'system'`
- Agent-to-agent supported via target-agent routing on `messages_out`
- Scheduling uses `process_after` / `deliver_after` + `recurrence` on the same message tables
- Media via signed URLs, downloaded in the container
- Channel adapters use the Chat SDK bridge + a standard interface (trunk ships only the bridge/registry; platform adapters install via `/add-<channel>` skills)
- Routing: channel adapter extracts IDs, host maps to entities
- Concurrency: Chat SDK per-channel + container limits
- Session scoping: per-session DB, multiple sessions per agent group
## Design Decisions
@@ -463,7 +456,7 @@ Typing indicators: host sets typing when a container is active for a session, cl
### Message Batching
When multiple messages arrive while the container is down, they accumulate as `handled = 0` rows in messages_in. When the container wakes up, the agent-runner queries all unhandled messages and processes them as a batch — same as v1 where multiple messages are formatted into a single `<messages>` XML block.
When multiple messages arrive while the container is down, they accumulate as `handled = 0` rows in messages_in. When the container wakes up, the agent-runner queries all unhandled messages and processes them as a batch — multiple messages are formatted into a single `<messages>` XML block.
### Message Lifecycle
@@ -523,11 +516,11 @@ NanoClaw is customized via skills — branches that get merged into the user's i
- One line in the barrel file (`channels/index.ts`) to import the self-registering module
- Zero changes to routing, formatting, delivery, or container code
### v1 Conflict Hotspots and v2 Solutions
### Conflict Hotspots and Solutions
Analysis of 33 skill branches shows these files cause the most merge conflicts:
| v1 hotspot | Why it conflicts | v2 solution |
| Hotspot | Why it conflicts | Solution |
|-----------|-----------------|-------------|
| `src/index.ts` (2000 LOC) | Every skill patches the main loop, imports, init logic | Thin index that wires modules. Logic lives in purpose-specific files (router, delivery, session-manager, host-sweep). |
| `src/config.ts` | Every skill adds env vars to a central file | Config declared where it's used. Each module reads its own env vars. No central config registry that every skill edits. |
@@ -566,9 +559,9 @@ Shared config (DATA_DIR, TIMEZONE, MAX_CONCURRENT_CONTAINERS) stays in `config.t
### Code Style
**Line width: 120 characters.** v1 uses the prettier default of 80, which breaks simple log calls and function signatures across 3-4 lines. v2 uses 120 — most statements fit on one line without sacrificing readability.
**Line width: 120 characters.** Most statements fit on one line without sacrificing readability.
**Concise logging.** v1 has 138 log calls, many spanning 3-4 lines due to pino's structured API + 80-char wrapping. v2 uses a thin wrapper so every log call is one line:
**Concise logging.** A thin wrapper keeps every log call on one line:
```typescript
log.info('IPC message sent', { chatJid, sourceGroup });
@@ -578,7 +571,7 @@ log.error('Error processing', { file, err });
### DB File Structure
v1's DB is one 750-line file with all tables, all CRUD functions, and all migrations inline. v2 splits by entity:
The DB layer is split by entity rather than kept in one monolithic file:
```
src/db/
@@ -586,7 +579,7 @@ src/db/
schema.ts ← CREATE TABLE statements (current state, for reference)
migrations/
index.ts ← runner: checks version, applies pending
001-initial.ts ← v2 initial schema
001-initial.ts ← initial schema
002-pending-questions.ts ← example: adds pending_questions table
... ← skills append new numbered files
agent-groups.ts ← CRUD for agent_groups
@@ -598,7 +591,7 @@ src/db/
**Principles:**
- **Split by entity, not by layer.** Each entity file has its own CRUD functions (~50-100 lines). A skill that adds a column to messaging_groups edits `messaging-groups.ts` — doesn't touch sessions or agent groups.
- **Schema as current state + migrations as history.** `schema.ts` documents what the DB looks like now (read this to understand the schema). Migrations are append-only numbered files that describe how we got here.
- **No inline ALTER TABLE.** v1 accumulates `try { ALTER TABLE } catch { /* exists */ }` blocks forever. v2 uses a migration runner with a `schema_version` table. On startup, it checks the current version and applies pending migrations in order. Each migration is a function: `(db: Database) => void`.
- **No inline ALTER TABLE.** A migration runner with a `schema_version` table replaces `try { ALTER TABLE } catch { /* exists */ }` blocks. On startup, it checks the current version and applies pending migrations in order. Each migration is a function: `(db: Database) => void`.
- **Skills add migrations.** A skill that needs a new column adds a new numbered migration file. No conflicts with other skills' migrations as long as numbers don't collide (use timestamps or high-enough numbers for skill branches).
**Agent-runner session DB** uses the same pattern but lighter — no migrations needed since session DBs are created fresh by the host:
@@ -795,18 +788,6 @@ stopped → running → idle → stopped
- **idle**: Done processing, container still warm (up to 30 min timeout). Polled at 1s so new messages are picked up quickly.
- After idle timeout → host kills container → stopped.
### Migration from v1
| v1 table | v2 |
|----------|-----|
| `registered_groups` | Split into `agent_groups` + `messaging_groups` + `messaging_group_agents` |
| `chats` | Absorbed into `messaging_groups` |
| `messages` | Content moves to per-session DBs (messages_in) |
| `sessions` (folder → sdk_session_id) | New `sessions` table (folder derived from ID) |
| `scheduled_tasks` | Moved to per-session DBs (messages_in with recurrence) |
| `task_run_logs` | Dropped — results are in session DB messages_out |
| `router_state` | Dropped — replaced by message status in session DBs |
## Agent-Runner Architecture
The agent-runner is the process inside the container. It mediates between the session DB and the Claude SDK — polling for work, formatting messages for the agent, translating tool calls into DB rows, and managing the agent lifecycle.
@@ -815,13 +796,10 @@ The agent-runner is the process inside the container. It mediates between the se
All IO goes through the session DB. No stdin, no stdout markers, no IPC files.
| v1 | v2 |
|----|----|
| Initial input from stdin (JSON envelope) | Poll `messages_in` |
| Follow-up messages from IPC files | Same poll — new rows appear |
| Output via stdout markers | Write `messages_out` rows |
| MCP tools write IPC files | MCP tools write DB rows |
| `_close` sentinel signals shutdown | Host kills container (idle timeout) or agent-runner exits when no pending work |
- Initial input and follow-ups: poll `messages_in`
- Output: write `messages_out` rows
- MCP tools: write DB rows (no IPC files)
- Shutdown: host kills the container on idle timeout, or the agent-runner exits when there's no pending work
### Poll Loop
@@ -837,9 +815,9 @@ All IO goes through the session DB. No stdin, no stdout markers, no IPC files.
Agent-runner strips routing fields (`platform_id`, `channel_type`, `thread_id`) before formatting. The agent never sees routing info — it only sees content.
- **`chat`** — format into `<messages>` XML block (same as v1)
- **`chat`** — format into `<messages>` XML block
- **`chat-sdk`** — extract text, author, attachments from serialized message; format into `<messages>` XML
- **`task`** — format as `[SCHEDULED TASK]` prefix + prompt. Run pre-script if present (same as v1).
- **`task`** — format as `[SCHEDULED TASK]` prefix + prompt. Run pre-script if present.
- **`webhook`** — format as `[WEBHOOK: source/event]` + JSON payload
- **`system`** — host action results (e.g., "register_group succeeded"). Format as system context, not chat.
@@ -847,9 +825,9 @@ Mixed batches (e.g., a chat message + a system result both pending) are combined
### MCP Tools
All v1 IPC-file-based tools are replaced with direct DB writes.
MCP tools write directly to the session DB.
**Carried over (new implementation):**
**Core tools:**
| Tool | What it does |
|------|-------------|
@@ -870,7 +848,7 @@ All v1 IPC-file-based tools are replaced with direct DB writes.
| `send_to_agent` | Write `messages_out` with `channel_type: 'agent'`, `platform_id: '{target}'` |
| `send_card` | Write `messages_out` with card structure |
See [v2-agent-runner-details.md](v2-agent-runner-details.md) for full MCP tool parameter definitions.
See [agent-runner-details.md](agent-runner-details.md) for full MCP tool parameter definitions.
### Cards
@@ -904,7 +882,7 @@ The command lists are hardcoded in the agent-runner. Admin verification: the hos
The agent-runner processes recurring task messages like any other messages_in row. After the agent-runner marks a recurring message as `completed`, the **host** handles inserting the next occurrence (new messages_in row with `process_after` advanced to next cron time). The agent-runner doesn't manage recurrence — it just processes what it finds.
Pre-scripts work the same as v1: if a task message has a `script` field, run it first. If `wakeAgent = false`, mark completed without invoking Claude.
Pre-scripts: if a task message has a `script` field, run it first. If `wakeAgent = false`, mark completed without invoking Claude.
### Agent-to-Agent Messaging
@@ -912,9 +890,9 @@ Pre-scripts work the same as v1: if a task message has a `script` field, run it
**Inbound:** Messages from other agents arrive as normal `chat` messages_in rows. The content includes `sender` and `senderId` (e.g., `"senderId": "agent:pr-admin"`). No special formatting — the agent sees it as a chat message.
### What Stays From v1
### Agent-Runner Properties
- AgentProvider interface wraps SDK-specific query logic (Claude, Codex, OpenCode)
- AgentProvider interface wraps SDK-specific query logic (trunk ships the `claude` provider; additional providers like OpenCode install via `/add-<provider>` skills)
- Session resume via provider-specific mechanisms
- System prompt loading from CLAUDE.md files
- PreCompact hook for transcript archiving (Claude provider)
@@ -929,5 +907,5 @@ Pre-scripts work the same as v1: if a task message has a `script` field, run it
## Related Documents
- **[v2-api-details.md](v2-api-details.md)** — Channel adapter interface (NanoClaw + Chat SDK bridge), message content examples, host delivery logic
- **[v2-agent-runner-details.md](v2-agent-runner-details.md)** — AgentProvider interface, MCP tools, message formatting, media handling, provider implementations (Claude, Codex, OpenCode)
- **[api-details.md](api-details.md)** — Channel adapter interface (NanoClaw + Chat SDK bridge), message content examples, host delivery logic
- **[agent-runner-details.md](agent-runner-details.md)** — AgentProvider interface, MCP tools, message formatting, media handling, provider implementations
+2 -5
View File
@@ -1,4 +1,4 @@
# NanoClaw v2 Checklist
# NanoClaw Checklist
Status: [x] done, [~] partial, [ ] not started
@@ -55,7 +55,7 @@ Status: [x] done, [~] partial, [ ] not started
- [~] iMessage via Chat SDK (adapter + skill written, not tested)
- [x] Backward compatibility with native channels (old adapters still work)
- [x] Channel barrel wired (src/index.ts imports barrel, skills uncomment)
- [x] Setup flow wired to v2 channels (channel skills + /manage-channels for registration + verify.ts checks all tokens)
- [x] Setup flow wired to channels (channel skills + /manage-channels for registration + verify.ts checks all tokens)
- [x] Channel Info metadata in each channel skill (type, terminology, how-to-find-id, isolation defaults)
- [x] /manage-channels skill (wire channels to agent groups with three isolation levels)
- [x] /init-first-agent skill (standalone first-agent bootstrap; walks the operator through channel pick → identity lookup → DM platform_id resolution → wire → welcome DM; fallback to telegram pair-code or "DM the bot first" lookup for channels without cold DM)
@@ -253,9 +253,6 @@ Container skills live inside agent containers at runtime (`container/skills/`) a
## Migration
- [ ] v1 -> v2 migration skill
- [ ] Database migration (v1 SQLite -> v2 central DB + session DBs)
- [ ] Channel credential preservation
- [ ] Custom skill/code porting
- [ ] OneCLI migration check — determine if existing installs need OneCLI re-init (credentials re-scoped to new `agent_group.id` identifier, new SDK version, approval handler registered). If needed, add a migration step to `/update-nanoclaw` or a dedicated skill.
+9 -9
View File
@@ -1,6 +1,6 @@
# NanoClaw v2 — Central DB Schema
# NanoClaw — Central DB Schema
Complete reference for `data/v2.db`, the host-owned admin-plane database. Start with [v2-db.md](v2-db.md) for the three-DB overview, the map, and the cross-mount rules.
Complete reference for `data/v2.db`, the host-owned admin-plane database. Start with [db.md](db.md) for the three-DB overview, the map, and the cross-mount rules.
Access layer: `src/db/`. Authoritative schema reference: `src/db/schema.ts` (comments only — actual creation runs via migrations in `src/db/migrations/`).
@@ -48,7 +48,7 @@ CREATE TABLE messaging_groups (
### 1.3 `messaging_group_agents`
Wiring: which agent group handles which messaging group. Many-to-many — the same channel can route to multiple agents (see [v2-isolation-model.md](v2-isolation-model.md)).
Wiring: which agent group handles which messaging group. Many-to-many — the same channel can route to multiple agents (see [isolation-model.md](isolation-model.md)).
```sql
CREATE TABLE messaging_group_agents (
@@ -157,7 +157,7 @@ CREATE INDEX idx_sessions_lookup ON sessions(messaging_group_id, thread_id);
```
- **Resolved by:** `resolveSession()` in `src/session-manager.ts`.
- Creating a session also provisions the session folder and both session DBs via `initSessionFolder()` — see [v2-db-session.md](v2-db-session.md).
- Creating a session also provisions the session folder and both session DBs via `initSessionFolder()` — see [db-session.md](db-session.md).
### 1.9 `pending_questions`
@@ -193,7 +193,7 @@ CREATE TABLE agent_destinations (
CREATE INDEX idx_agent_dest_target ON agent_destinations(target_type, target_id);
```
**Projection invariant (load-bearing).** The central table is the source of truth, but each running container reads from a projection in its own `inbound.db` (see [v2-db-session.md §2.3](v2-db-session.md#23-destinations)). Any code that mutates `agent_destinations` while a container is running must also call `writeDestinations()` (`src/session-manager.ts`) or the container will reject sends with stale data. Known call sites: `createMessagingGroupAgent()` in `src/db/messaging-groups.ts`, the `create_agent` system action in `src/delivery.ts`.
**Projection invariant (load-bearing).** The central table is the source of truth, but each running container reads from a projection in its own `inbound.db` (see [db-session.md §2.3](db-session.md#23-destinations)). Any code that mutates `agent_destinations` while a container is running must also call `writeDestinations()` (`src/session-manager.ts`) or the container will reject sends with stale data. Known call sites: `createMessagingGroupAgent()` in `src/db/messaging-groups.ts`, the `create_agent` system action in `src/delivery.ts`.
Access layer: `src/db/agent-destinations.ts`.
@@ -253,7 +253,7 @@ Writer: `recordDroppedMessage()` in `src/db/dropped-messages.ts`. On conflict, b
### 1.13 Chat SDK bridge tables
State backing the `SqliteStateAdapter` used by the Chat SDK bridge (see [v2-api-details.md](v2-api-details.md)). NanoClaw code rarely touches these directly — they're owned by `src/state-sqlite.ts`.
State backing the `SqliteStateAdapter` used by the Chat SDK bridge (see [api-details.md](api-details.md)). NanoClaw code rarely touches these directly — they're owned by `src/state-sqlite.ts`.
```sql
CREATE TABLE chat_sdk_kv (
@@ -306,7 +306,7 @@ Migrations live in `src/db/migrations/`, one file per migration. Runner: `runMig
| # | File | Introduces |
|---|------|------------|
| 001 | `001-initial.ts` | Core v2 tables: `agent_groups`, `messaging_groups`, `messaging_group_agents`, `users`, `user_roles`, `agent_group_members`, `user_dms`, `sessions`, `pending_questions` |
| 001 | `001-initial.ts` | Core tables: `agent_groups`, `messaging_groups`, `messaging_group_agents`, `users`, `user_roles`, `agent_group_members`, `user_dms`, `sessions`, `pending_questions` |
| 002 | `002-chat-sdk-state.ts` | `chat_sdk_kv`, `chat_sdk_subscriptions`, `chat_sdk_locks`, `chat_sdk_lists` |
| 003 | `003-pending-approvals.ts` | `pending_approvals` (session-bound + OneCLI fields) |
| 004 | `004-agent-destinations.ts` | `agent_destinations` + backfill from existing `messaging_group_agents` wirings |
@@ -314,6 +314,6 @@ Migrations live in `src/db/migrations/`, one file per migration. Runner: `runMig
| 008 | `008-dropped-messages.ts` | `unregistered_senders` |
| 009 | `009-drop-pending-credentials.ts` | Drop the defunct `pending_credentials` table |
Numbers 005 and 006 are intentionally absent — migrations were renumbered during v2 development.
Numbers 005 and 006 are intentionally absent — migrations were renumbered during early development.
Session DB schemas (`INBOUND_SCHEMA`, `OUTBOUND_SCHEMA`) are **not** versioned here. They're `CREATE TABLE IF NOT EXISTS` so new columns land via the session-DB lazy migration helpers (`migrateDeliveredTable()` etc.) when a session file from an older build is reopened. See [v2-db-session.md](v2-db-session.md).
Session DB schemas (`INBOUND_SCHEMA`, `OUTBOUND_SCHEMA`) are **not** versioned here. They're `CREATE TABLE IF NOT EXISTS` so new columns land via the session-DB lazy migration helpers (`migrateDeliveredTable()` etc.) when a session file from an older build is reopened. See [db-session.md](db-session.md).
+5 -5
View File
@@ -1,6 +1,6 @@
# NanoClaw v2 — Per-Session DB Schema
# 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 [v2-db.md](v2-db.md) for the three-DB overview, the single-writer rule, and the cross-mount visibility constraints.
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.
@@ -50,7 +50,7 @@ CREATE TABLE messages_in (
CREATE INDEX idx_messages_in_series ON messages_in(series_id);
```
Content shapes: see [v2-api-details.md §Session DB Schema Details](v2-api-details.md#session-db-schema-details).
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)`.
@@ -72,7 +72,7 @@ Writer: `markDelivered()` / `markDeliveryFailed()` in `src/db/session-db.ts`. Ol
### 2.3 `destinations`
Projection of the central `agent_destinations` table (see [v2-db-central.md §1.10](v2-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`.
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 (
@@ -141,7 +141,7 @@ CREATE TABLE messages_out (
);
```
Content shapes: see [v2-api-details.md §Session DB Schema Details](v2-api-details.md#session-db-schema-details).
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.
+8 -8
View File
@@ -1,17 +1,17 @@
# NanoClaw v2 Database Architecture — Overview
# NanoClaw Database Architecture — Overview
Orientation for the v2 data model: the three databases, how they fit together, and the invariants that hold across them. For table-level schemas, follow the links below.
Orientation for the data model: the three databases, how they fit together, and the invariants that hold across them. For table-level schemas, follow the links below.
- **[v2-db-central.md](v2-db-central.md)** — every table in `data/v2.db` (identity, wiring, approvals, Chat SDK state) plus the migration system.
- **[v2-db-session.md](v2-db-session.md)** — the per-session `inbound.db` + `outbound.db` pair, seq parity, and session folder layout.
- **[db-central.md](db-central.md)** — every table in `data/v2.db` (identity, wiring, approvals, Chat SDK state) plus the migration system.
- **[db-session.md](db-session.md)** — the per-session `inbound.db` + `outbound.db` pair, seq parity, and session folder layout.
Related: [v2-architecture-draft.md](v2-architecture-draft.md) for the high-level design; [v2-api-details.md](v2-api-details.md) for inbound/outbound message content shapes; [v2-isolation-model.md](v2-isolation-model.md) for channel-to-agent wiring modes.
Related: [architecture.md](architecture.md) for the high-level design; [api-details.md](api-details.md) for inbound/outbound message content shapes; [isolation-model.md](isolation-model.md) for channel-to-agent wiring modes.
---
## 1. The three databases
v2 uses **three kinds of SQLite database**, all on the host filesystem:
NanoClaw uses **three kinds of SQLite database**, all on the host filesystem:
| DB | Location | Writer | Readers | Purpose |
|----|----------|--------|---------|---------|
@@ -73,7 +73,7 @@ Session DBs are bind-mounted into the container. A few rules you need to know be
- **`journal_mode = DELETE`, not WAL.** WAL files don't reliably cross the mount and the container can read stale pages. DELETE mode forces each writer to flush the main file.
- **Open-write-close on the host.** Host-side writes to `inbound.db` open a connection, write, and close it. Keeping a handle open makes cached pages invisible to the container.
- **Container reads read-only.** The container opens `inbound.db` with `readonly: true` and never writes — all container→host state goes through `outbound.db` (see `processing_ack` in [v2-db-session.md](v2-db-session.md#52-processing_ack)).
- **Container reads read-only.** The container opens `inbound.db` with `readonly: true` and never writes — all container→host state goes through `outbound.db` (see `processing_ack` in [db-session.md](db-session.md#52-processing_ack)).
- **Heartbeat is a file touch.** `.heartbeat` mtime is the liveness signal, not a DB column. A DB write per heartbeat would serialize behind other writers.
These rules are enforced by convention in `src/session-manager.ts` and `container/agent-runner/src/db/`. If you change how the DBs are opened, re-read that code first.
@@ -83,7 +83,7 @@ These rules are enforced by convention in `src/session-manager.ts` and `containe
## 5. Design patterns at a glance
1. **Two-DB session split.** `inbound.db` and `outbound.db` each have one writer, one direction of flow — no cross-mount lock contention.
2. **Seq parity.** Even = host, odd = container. Disjoint namespace across both tables lets the agent reference any message by `seq` alone. Details in [v2-db-session.md §3](v2-db-session.md#3-sequence-numbering-invariant).
2. **Seq parity.** Even = host, odd = container. Disjoint namespace across both tables lets the agent reference any message by `seq` alone. Details in [db-session.md §3](db-session.md#3-sequence-numbering-invariant).
3. **Projection pattern.** `agent_destinations` and `session_routing` are projected from the central DB into each session's `inbound.db` on container wake — the container gets a fast, local read path without querying across the mount.
4. **Ack via reverse channel.** Container never writes to `inbound.db`. Status sync happens through `processing_ack` in `outbound.db`, which the host polls and reconciles.
5. **Heartbeat out of band.** File `touch` on `.heartbeat`, not a DB write, so liveness doesn't serialize behind other writers.
@@ -1,6 +1,6 @@
# Channel Isolation Model
NanoClaw v2 decouples messaging channels from agent groups. When you connect a channel (Discord, Telegram, Slack, GitHub, etc.), you decide how it relates to your existing agents. There are three isolation levels.
NanoClaw decouples messaging channels from agent groups. When you connect a channel (Discord, Telegram, Slack, GitHub, etc.), you decide how it relates to your existing agents. There are three isolation levels.
## The Three Levels
@@ -1,6 +1,6 @@
# v2 Setup Wiring — Status & Remaining Work
# Setup Wiring — Status & Remaining Work
Last updated: 2026-04-09, branch `v2`, commit `1dc5750`
Last updated: 2026-04-09
## What's Done
@@ -13,7 +13,7 @@ Last updated: 2026-04-09, branch `v2`, commit `1dc5750`
- Host sweep reads `processing_ack` table + heartbeat file mtime for stale detection
- Container clears stale `processing_ack` entries on startup (crash recovery)
- Files: `src/db/schema.ts` (INBOUND_SCHEMA + OUTBOUND_SCHEMA), `src/session-manager.ts`, `src/delivery.ts`, `src/host-sweep.ts`, `container/agent-runner/src/db/connection.ts`, `messages-in.ts`, `messages-out.ts`, `poll-loop.ts`, `mcp-tools/scheduling.ts`, `mcp-tools/interactive.ts`
- Container image rebuilt with tsconfig excluding v1 (`container/agent-runner/tsconfig.json`)
- Container image rebuilt with tsconfig (`container/agent-runner/tsconfig.json`)
- E2E verified: host → Docker container → Claude responds → "E2E works!" ✓
### OneCLI Integration
@@ -23,14 +23,14 @@ Last updated: 2026-04-09, branch `v2`, commit `1dc5750`
### Channel Barrel
- `src/index.ts` imports `./channels/index.js` (the barrel)
- Channel skills uncomment lines in the barrel to enable channels
- Discord is uncommented by default (it was previously a direct import in index.ts)
- Trunk ships the barrel + Chat SDK bridge only; `/add-<channel>` skills drop adapter files in and register them via the barrel slot
- No channel adapters ship in trunk
### Setup Registration (partially)
- `setup/register.ts` rewritten to create v2 entities (`agent_groups`, `messaging_groups`, `messaging_group_agents`) in `data/v2.db`
- Accepts `--platform-id` (v2) and `--jid` (v1 compat) flags
- Added `getMessagingGroupAgentByPair()` to prevent duplicate wiring
- `setup/verify.ts` updated to check v2 central DB (counts agent groups with wiring)
- `setup/register.ts` creates entities (`agent_groups`, `messaging_groups`, `messaging_group_agents`) in `data/v2.db`
- Accepts `--platform-id` flag
- `getMessagingGroupAgentByPair()` prevents duplicate wiring
- `setup/verify.ts` checks the central DB (counts agent groups with wiring)
### Router Logging
- `src/router.ts` logs `MESSAGE DROPPED` at WARN level when no agents wired, with actionable guidance
@@ -39,27 +39,23 @@ Last updated: 2026-04-09, branch `v2`, commit `1dc5750`
## Previously Open — Now Resolved
### 1. ~~v2 Channel Skills Don't Register Groups~~
### 1. ~~Channel Skills Don't Register Groups~~
Channel skills now point to `/manage-channels` in their "Next Steps" section. Registration is handled by the `/manage-channels` skill, which reads each channel's `## Channel Info` section for platform-specific guidance. Channel skills stay lean (credentials only).
### 2. ~~v1 add-discord Skill is Incompatible~~
Created `/add-discord-v2` skill matching the v2 pattern. Setup SKILL.md updated to reference `/add-discord-v2`.
### 3. ~~Setup SKILL.md Missing Group Registration Step~~
### 2. ~~Setup SKILL.md Missing Group Registration Step~~
Added step 5a "Wire Channels to Agent Groups" between channel installation (step 5) and mount allowlist (step 6). This step invokes `/manage-channels` which handles agent group creation, isolation level decisions, and wiring.
### 4. ~~Channel Skills Should Know Channel Type~~
### 3. ~~Channel Skills Should Know Channel Type~~
Each v2 channel skill now has a `## Channel Info` structured section with: type, terminology, how-to-find-id, supports-threads, typical-use, default-isolation. The `/manage-channels` skill reads this for contextual recommendations.
Each channel skill has a `## Channel Info` structured section with: type, terminology, how-to-find-id, supports-threads, typical-use, default-isolation. The `/manage-channels` skill reads this for contextual recommendations.
### 5. ~~Verify Step Channel Auth Check~~
### 4. ~~Verify Step Channel Auth Check~~
`setup/verify.ts` now checks all v2 channel tokens: DISCORD_BOT_TOKEN, TELEGRAM_BOT_TOKEN, SLACK_BOT_TOKEN+SLACK_APP_TOKEN, GITHUB_TOKEN, LINEAR_API_KEY, GCHAT_CREDENTIALS, TEAMS_APP_ID+TEAMS_APP_PASSWORD, WEBEX_BOT_TOKEN, MATRIX_ACCESS_TOKEN, RESEND_API_KEY, WHATSAPP_ACCESS_TOKEN, IMESSAGE_ENABLED, plus WhatsApp Baileys auth dir.
`setup/verify.ts` checks all channel tokens: DISCORD_BOT_TOKEN, TELEGRAM_BOT_TOKEN, SLACK_BOT_TOKEN+SLACK_APP_TOKEN, GITHUB_TOKEN, LINEAR_API_KEY, GCHAT_CREDENTIALS, TEAMS_APP_ID+TEAMS_APP_PASSWORD, WEBEX_BOT_TOKEN, MATRIX_ACCESS_TOKEN, RESEND_API_KEY, WHATSAPP_ACCESS_TOKEN, IMESSAGE_ENABLED, plus WhatsApp Baileys auth dir.
### 6. Agent-Shared Session Mode ✅
### 5. Agent-Shared Session Mode ✅
Added `session_mode: 'agent-shared'` for cross-channel shared sessions (e.g. GitHub + Slack in one conversation). Session resolution looks up by agent_group_id instead of messaging_group_id when this mode is set.
@@ -67,7 +63,7 @@ Added `session_mode: 'agent-shared'` for cross-channel shared sessions (e.g. Git
## Architecture Reference
### v2 Entity Model
### Entity Model
```
agent_groups (id, name, folder, agent_provider, container_config)
↕ many-to-many
@@ -93,13 +89,13 @@ Channel adapter → routeInbound() → resolve messaging_group → resolve agent
### Key Files
| File | Purpose |
|------|---------|
| `src/index.ts` | v2 entry point, imports channel barrel |
| `src/channels/index.ts` | Channel barrel — uncomment to enable |
| `src/index.ts` | Entry point, imports channel barrel |
| `src/channels/index.ts` | Channel barrel — registry/Chat SDK bridge only in trunk; skills drop adapters in |
| `src/router.ts` | Inbound routing, auto-creates messaging groups |
| `src/session-manager.ts` | Creates inbound.db + outbound.db per session |
| `src/delivery.ts` | Polls outbound.db, delivers, handles system actions |
| `src/host-sweep.ts` | Syncs processing_ack, stale detection, recurrence |
| `src/container-runner.ts` | Spawns containers, OneCLI ensureAgent + applyContainerConfig |
| `setup/register.ts` | Creates v2 entities (agent_group, messaging_group, wiring) |
| `setup/verify.ts` | Checks v2 central DB for registered groups |
| `setup/register.ts` | Creates entities (agent_group, messaging_group, wiring) |
| `setup/verify.ts` | Checks central DB for registered groups |
| `container/agent-runner/src/db/connection.ts` | Two-DB connection layer (inbound read-only, outbound read-write) |