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
+33 -23
View File
@@ -1,15 +1,15 @@
# NanoClaw # NanoClaw
Personal Claude assistant. See [README.md](README.md) for philosophy and setup. Architecture lives in `docs/v2-*.md`. Personal Claude assistant. See [README.md](README.md) for philosophy and setup. Architecture lives in `docs/`.
## Quick Context (v2) ## Quick Context
v2 is the current branch and codebase. v1 still exists under `src/v1/` and `container/agent-runner/src/v1/` for reference but is no longer the runtime. If a file mentions v1 in its comments, it is probably stale.
The host is a single Node process that orchestrates per-session agent containers. Platform messages land via channel adapters, route through an entity model (users → messaging groups → agent groups → sessions), get written into the session's inbound DB, and wake a container. The agent-runner inside the container polls the DB, calls Claude, and writes back to the outbound DB. The host polls the outbound DB and delivers through the same adapter. The host is a single Node process that orchestrates per-session agent containers. Platform messages land via channel adapters, route through an entity model (users → messaging groups → agent groups → sessions), get written into the session's inbound DB, and wake a container. The agent-runner inside the container polls the DB, calls Claude, and writes back to the outbound DB. The host polls the outbound DB and delivers through the same adapter.
**Everything is a message.** There is no IPC, no file watcher, no stdin piping between host and container. The two session DBs are the sole IO surface. **Everything is a message.** There is no IPC, no file watcher, no stdin piping between host and container. The two session DBs are the sole IO surface.
A `src/v1/` tree exists for historical reference and is not part of the runtime — ignore it unless you're explicitly working on a migration.
## Entity Model ## Entity Model
``` ```
@@ -25,7 +25,7 @@ messaging_groups (one chat/channel on one platform; unknown_sender_policy)
sessions (agent_group_id + messaging_group_id + thread_id → per-session container) sessions (agent_group_id + messaging_group_id + thread_id → per-session container)
``` ```
Privilege is user-level (owner/admin), not agent-group-level. See [docs/v2-isolation-model.md](docs/v2-isolation-model.md) for the three isolation levels (`agent-shared`, `shared`, separate agents). Privilege is user-level (owner/admin), not agent-group-level. See [docs/isolation-model.md](docs/isolation-model.md) for the three isolation levels (`agent-shared`, `shared`, separate agents).
## Two-DB Session Split ## Two-DB Session Split
@@ -56,12 +56,22 @@ Exactly one writer per file — no cross-mount lock contention. Heartbeat is a f
| `src/user-dm.ts` | Cold-DM resolution + `user_dms` cache | | `src/user-dm.ts` | Cold-DM resolution + `user_dms` cache |
| `src/group-init.ts` | Per-agent-group filesystem scaffold (CLAUDE.md, skills, agent-runner-src overlay) | | `src/group-init.ts` | Per-agent-group filesystem scaffold (CLAUDE.md, skills, agent-runner-src overlay) |
| `src/db/` | DB layer — agent_groups, messaging_groups, sessions, user_roles, user_dms, pending_*, migrations | | `src/db/` | DB layer — agent_groups, messaging_groups, sessions, user_roles, user_dms, pending_*, migrations |
| `src/channels/` | Channel adapters + Chat SDK bridge | | `src/channels/` | Channel adapter infra (registry, Chat SDK bridge); specific channel adapters are skill-installed from the `channels` branch |
| `src/providers/` | Host-side provider container-config (`claude` baked in; `opencode` etc. installed from the `providers` branch) |
| `container/agent-runner/src/` | Agent-runner: poll loop, formatter, provider abstraction, MCP tools, destinations | | `container/agent-runner/src/` | Agent-runner: poll loop, formatter, provider abstraction, MCP tools, destinations |
| `container/skills/` | Container skills mounted into every agent session | | `container/skills/` | Container skills mounted into every agent session |
| `groups/<folder>/` | Per-agent-group filesystem (CLAUDE.md, skills, per-group `agent-runner-src/` overlay) | | `groups/<folder>/` | Per-agent-group filesystem (CLAUDE.md, skills, per-group `agent-runner-src/` overlay) |
| `scripts/init-first-agent.ts` | Bootstrap the first DM-wired agent (used by `/init-first-agent` skill) | | `scripts/init-first-agent.ts` | Bootstrap the first DM-wired agent (used by `/init-first-agent` skill) |
## Channels and Providers (skill-installed)
Trunk does not ship any specific channel adapter or non-default agent provider. The codebase is the registry/infra; the actual adapters and providers live on long-lived sibling branches and get copied in by skills:
- **`channels` branch** — Discord, Slack, Telegram, WhatsApp, Teams, Linear, GitHub, iMessage, Webex, Resend, Matrix, Google Chat, WhatsApp Cloud (+ helpers, tests, channel-specific setup steps). Installed via `/add-<channel>` skills.
- **`providers` branch** — OpenCode (and any future non-default agent providers). Installed via `/add-opencode`.
Each `/add-<name>` skill is idempotent: `git fetch origin <branch>` → copy module(s) into the standard paths → append a self-registration import to the relevant barrel → `pnpm install <pkg>@<pinned-version>` → build.
## Self-Modification ## Self-Modification
One tier of agent self-modification today: One tier of agent self-modification today:
@@ -78,10 +88,10 @@ API keys, OAuth tokens, and auth credentials are managed by the OneCLI gateway.
Four types of skills. See [CONTRIBUTING.md](CONTRIBUTING.md) for the full taxonomy. Four types of skills. See [CONTRIBUTING.md](CONTRIBUTING.md) for the full taxonomy.
- **Feature skills** — `skill/*` branches merged via `scripts/apply-skill.ts` (e.g. `/add-discord-v2`, `/add-slack-v2`, `/add-whatsapp-v2`) - **Channel/provider install skills** — copy the relevant module(s) in from the `channels` or `providers` branch, wire imports, install pinned deps (e.g. `/add-discord`, `/add-slack`, `/add-whatsapp`, `/add-opencode`).
- **Utility skills** — ship code files alongside `SKILL.md` (e.g. `/claw`) - **Utility skills** — ship code files alongside `SKILL.md` (e.g. `/claw`).
- **Operational skills** — instruction-only workflows (`/setup`, `/debug`, `/customize`, `/init-first-agent`, `/manage-channels`, `/init-onecli`, `/update-nanoclaw`) - **Operational skills** — instruction-only workflows (`/setup`, `/debug`, `/customize`, `/init-first-agent`, `/manage-channels`, `/init-onecli`, `/update-nanoclaw`).
- **Container skills** — loaded inside agent containers at runtime (`container/skills/`: `welcome`, `self-customize`, `agent-browser`, `slack-formatting`) - **Container skills** — loaded inside agent containers at runtime (`container/skills/`: `welcome`, `self-customize`, `agent-browser`, `slack-formatting`).
| Skill | When to Use | | Skill | When to Use |
|-------|-------------| |-------|-------------|
@@ -137,21 +147,21 @@ This project uses pnpm with `minimumReleaseAge: 4320` (3 days) in `pnpm-workspac
- **`onlyBuiltDependencies`**: Never add packages to this list without human approval — build scripts execute arbitrary code during install. - **`onlyBuiltDependencies`**: Never add packages to this list without human approval — build scripts execute arbitrary code during install.
- **`pnpm install --frozen-lockfile`** should be used in CI, automation, and container builds. Never run bare `pnpm install` in those contexts. - **`pnpm install --frozen-lockfile`** should be used in CI, automation, and container builds. Never run bare `pnpm install` in those contexts.
## v2 Docs Index ## Docs Index
| Doc | Purpose | | Doc | Purpose |
|-----|---------| |-----|---------|
| [docs/v2-architecture-draft.md](docs/v2-architecture-draft.md) | Full architecture writeup | | [docs/architecture.md](docs/architecture.md) | Full architecture writeup |
| [docs/v2-api-details.md](docs/v2-api-details.md) | Host API + DB schema details | | [docs/api-details.md](docs/api-details.md) | Host API + DB schema details |
| [docs/v2-db.md](docs/v2-db.md) | DB architecture overview: three-DB model, cross-mount rules, readers/writers map | | [docs/db.md](docs/db.md) | DB architecture overview: three-DB model, cross-mount rules, readers/writers map |
| [docs/v2-db-central.md](docs/v2-db-central.md) | Central DB (`data/v2.db`) — every table + migration system | | [docs/db-central.md](docs/db-central.md) | Central DB (`data/v2.db`) — every table + migration system |
| [docs/v2-db-session.md](docs/v2-db-session.md) | Per-session `inbound.db` + `outbound.db` schemas + seq parity | | [docs/db-session.md](docs/db-session.md) | Per-session `inbound.db` + `outbound.db` schemas + seq parity |
| [docs/v2-agent-runner-details.md](docs/v2-agent-runner-details.md) | Agent-runner internals + MCP tool interface | | [docs/agent-runner-details.md](docs/agent-runner-details.md) | Agent-runner internals + MCP tool interface |
| [docs/v2-isolation-model.md](docs/v2-isolation-model.md) | Three-level channel isolation model | | [docs/isolation-model.md](docs/isolation-model.md) | Three-level channel isolation model |
| [docs/v2-setup-wiring.md](docs/v2-setup-wiring.md) | What's wired, what's open in the setup flow | | [docs/setup-wiring.md](docs/setup-wiring.md) | What's wired, what's open in the setup flow |
| [docs/v2-checklist.md](docs/v2-checklist.md) | Rolling status checklist across all subsystems | | [docs/checklist.md](docs/checklist.md) | Rolling status checklist across all subsystems |
| [docs/v2-architecture-diagram.md](docs/v2-architecture-diagram.md) | Diagram version of the architecture | | [docs/architecture-diagram.md](docs/architecture-diagram.md) | Diagram version of the architecture |
| [docs/v2-build-and-runtime.md](docs/v2-build-and-runtime.md) | Runtime split (Node host + Bun container), lockfiles, image build surface, CI, key invariants | | [docs/build-and-runtime.md](docs/build-and-runtime.md) | Runtime split (Node host + Bun container), lockfiles, image build surface, CI, key invariants |
## Container Build Cache ## Container Build Cache
@@ -159,7 +169,7 @@ The container buildkit caches the build context aggressively. `--no-cache` alone
## Container Runtime (Bun) ## Container Runtime (Bun)
The agent container runs on **Bun**; the host runs on **Node** (pnpm). They communicate only via session DBs — no shared modules. Details and rationale: [docs/v2-build-and-runtime.md](docs/v2-build-and-runtime.md). The agent container runs on **Bun**; the host runs on **Node** (pnpm). They communicate only via session DBs — no shared modules. Details and rationale: [docs/build-and-runtime.md](docs/build-and-runtime.md).
**Gotchas — trigger + action:** **Gotchas — trigger + action:**
@@ -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 ## 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. 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. 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 ## 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 ### Claude Provider
Wraps `@anthropic-ai/claude-agent-sdk`'s `query()`. Wraps `@anthropic-ai/claude-agent-sdk`'s `query()`.
@@ -445,7 +447,7 @@ pending → processing → completed
### MCP Tools ### 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). **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 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 5. If `wakeAgent === true`: enrich the prompt with script output, then invoke the provider
Same as v1 behavior.
### Transcript Archiving ### 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. 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 ### Provider Factory
```typescript ```typescript
type ProviderName = 'claude' | 'codex' | 'opencode'; type ProviderName = 'claude' | string;
function createProvider(name: ProviderName, config: ProviderConfig): AgentProvider { function createProvider(name: ProviderName, config: ProviderConfig): AgentProvider {
switch (name) { // Trunk registers 'claude'; additional providers self-register when installed via skills.
case 'claude': return new ClaudeProvider(config); const factory = providerRegistry.get(name);
case 'codex': return new CodexProvider(config); if (!factory) throw new Error(`Unknown provider: ${name}`);
case 'opencode': return new OpenCodeProvider(config); return factory(config);
default: throw new Error(`Unknown provider: ${name}`);
}
} }
``` ```
@@ -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`. `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) - 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 - 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/*`) - Additional directories discovery (`/workspace/extra/*`)
- Logging via stderr (`[agent-runner] ...`) - 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 ## Related Documents
- **[v2-architecture-draft.md](v2-architecture-draft.md)** — High-level architecture (session DB schema, central DB, channel adapters, message flow) - **[architecture.md](architecture.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 - **[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 ## Channel Adapter Interface
### NanoClaw Channel Interface (v2) ### NanoClaw Channel Interface
```typescript ```typescript
interface ChannelSetup { interface ChannelSetup {
@@ -59,7 +59,7 @@ interface OutboundMessage {
### Chat SDK Bridge ### 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 ```typescript
function createChatSdkBridge( function createChatSdkBridge(
@@ -170,7 +170,7 @@ function createChatSdkBridge(
### Native NanoClaw Channel (no Chat SDK) ### 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 ```typescript
function createWhatsAppChannel(): ChannelAdapter { function createWhatsAppChannel(): ChannelAdapter {
@@ -293,14 +293,14 @@ function createWhatsAppChannel(): ChannelAdapter {
"type": "card", "type": "card",
"title": "Deployment Approval", "title": "Deployment Approval",
"children": [ "children": [
{ "type": "text", "content": "Deploy v2.1.0 to production?" }, { "type": "text", "content": "Deploy 2.1.0 to production?" },
{ "type": "actions", "children": [ { "type": "actions", "children": [
{ "type": "button", "id": "approve", "label": "Approve", "style": "primary" }, { "type": "button", "id": "approve", "label": "Approve", "style": "primary" },
{ "type": "button", "id": "reject", "label": "Reject", "style": "danger" } { "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> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" /> <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> <script src="https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js"></script>
<style> <style>
:root { :root {
@@ -128,7 +128,7 @@
</head> </head>
<body> <body>
<header> <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> <div class="sub">Session-DB messaging model · Chat SDK bridge · OneCLI credential gateway · per-session containers</div>
<nav> <nav>
<a href="#overview">1 · Overview</a> <a href="#overview">1 · Overview</a>
@@ -396,7 +396,7 @@ flowchart LR
</div> </div>
</section> </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> </main>
<script> <script>
@@ -1,4 +1,4 @@
# NanoClaw v2 Architecture Diagram # NanoClaw Architecture Diagram
## System Overview ## System Overview
@@ -1,4 +1,4 @@
# NanoClaw v2 Architecture (Draft) # NanoClaw Architecture (Draft)
## Core Idea ## 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. 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 ### 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. This is documented as a pattern, not a built-in feature.
## What Stays the Same ## Core Properties
- Container isolation via filesystem mounts - Container isolation via filesystem mounts
- Credential proxy (OneCLI) - Credential proxy (OneCLI)
- Per-agent-group workspace (folder, CLAUDE.md, skills) - Per-agent-group workspace (folder, CLAUDE.md, skills)
- Polling-based (not event-driven) - 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) - Per-agent-group agent-runner recompilation on container startup (agent can modify its own source, request rebuild/restart, changes persist across teardowns)
- Host ↔ container IO through mounted session DBs (`messages_in` / `messages_out`) — no stdin piping, no IPC files
## What Changes - Agent commands are `messages_out` rows with `kind: 'system'`
- Agent-to-agent supported via target-agent routing on `messages_out`
| Component | v1 | v2 | - Scheduling uses `process_after` / `deliver_after` + `recurrence` on the same message tables
|-----------|----|----| - Media via signed URLs, downloaded in the container
| Host ↔ container IO | stdin + IPC files | Mounted session DB (messages_in / messages_out) | - Channel adapters use the Chat SDK bridge + a standard interface (trunk ships only the bridge/registry; platform adapters install via `/add-<channel>` skills)
| Container input | Prompt string piped to stdin | Agent-runner polls messages_in | - Routing: channel adapter extracts IDs, host maps to entities
| Container output | stdout markers | Agent-runner writes to messages_out | - Concurrency: Chat SDK per-channel + container limits
| Agent commands | IPC JSON files | messages_out with `kind: 'system'` | - Session scoping: per-session DB, multiple sessions per agent group
| 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 |
## Design Decisions ## Design Decisions
@@ -463,7 +456,7 @@ Typing indicators: host sets typing when a container is active for a session, cl
### Message Batching ### 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 ### 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 - One line in the barrel file (`channels/index.ts`) to import the self-registering module
- Zero changes to routing, formatting, delivery, or container code - 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: 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/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. | | `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 ### 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 ```typescript
log.info('IPC message sent', { chatJid, sourceGroup }); log.info('IPC message sent', { chatJid, sourceGroup });
@@ -578,7 +571,7 @@ log.error('Error processing', { file, err });
### DB File Structure ### 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/ src/db/
@@ -586,7 +579,7 @@ src/db/
schema.ts ← CREATE TABLE statements (current state, for reference) schema.ts ← CREATE TABLE statements (current state, for reference)
migrations/ migrations/
index.ts ← runner: checks version, applies pending 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 002-pending-questions.ts ← example: adds pending_questions table
... ← skills append new numbered files ... ← skills append new numbered files
agent-groups.ts ← CRUD for agent_groups agent-groups.ts ← CRUD for agent_groups
@@ -598,7 +591,7 @@ src/db/
**Principles:** **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. - **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. - **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). - **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: **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. - **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. - 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 ## 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. 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. All IO goes through the session DB. No stdin, no stdout markers, no IPC files.
| v1 | v2 | - Initial input and follow-ups: poll `messages_in`
|----|----| - Output: write `messages_out` rows
| Initial input from stdin (JSON envelope) | Poll `messages_in` | - MCP tools: write DB rows (no IPC files)
| Follow-up messages from IPC files | Same poll — new rows appear | - Shutdown: host kills the container on idle timeout, or the agent-runner exits when there's no pending work
| 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 |
### Poll Loop ### 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. 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 - **`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 - **`webhook`** — format as `[WEBHOOK: source/event]` + JSON payload
- **`system`** — host action results (e.g., "register_group succeeded"). Format as system context, not chat. - **`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 ### 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 | | 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_to_agent` | Write `messages_out` with `channel_type: 'agent'`, `platform_id: '{target}'` |
| `send_card` | Write `messages_out` with card structure | | `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 ### 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. 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 ### 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. **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 - Session resume via provider-specific mechanisms
- System prompt loading from CLAUDE.md files - System prompt loading from CLAUDE.md files
- PreCompact hook for transcript archiving (Claude provider) - 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 ## Related Documents
- **[v2-api-details.md](v2-api-details.md)** — Channel adapter interface (NanoClaw + Chat SDK bridge), message content examples, host delivery logic - **[api-details.md](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) - **[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 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) - [~] iMessage via Chat SDK (adapter + skill written, not tested)
- [x] Backward compatibility with native channels (old adapters still work) - [x] Backward compatibility with native channels (old adapters still work)
- [x] Channel barrel wired (src/index.ts imports barrel, skills uncomment) - [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] 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] /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) - [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 ## Migration
- [ ] v1 -> v2 migration skill
- [ ] Database migration (v1 SQLite -> v2 central DB + session DBs)
- [ ] Channel credential preservation
- [ ] Custom skill/code porting - [ ] 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. - [ ] 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/`). 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` ### 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 ```sql
CREATE TABLE messaging_group_agents ( 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`. - **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` ### 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); 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`. 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 ### 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 ```sql
CREATE TABLE chat_sdk_kv ( CREATE TABLE chat_sdk_kv (
@@ -306,7 +306,7 @@ Migrations live in `src/db/migrations/`, one file per migration. Runner: `runMig
| # | File | Introduces | | # | 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` | | 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) | | 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 | | 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` | | 008 | `008-dropped-messages.ts` | `unregistered_senders` |
| 009 | `009-drop-pending-credentials.ts` | Drop the defunct `pending_credentials` table | | 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. 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); 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()`. **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)`. **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` ### 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 ```sql
CREATE TABLE destinations ( 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`. **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. **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. - **[db-central.md](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-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 ## 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 | | 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. - **`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. - **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. - **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. 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 ## 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. 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. 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. 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. 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 # 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 ## 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 ## 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 - Host sweep reads `processing_ack` table + heartbeat file mtime for stale detection
- Container clears stale `processing_ack` entries on startup (crash recovery) - 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` - 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!" ✓ - E2E verified: host → Docker container → Claude responds → "E2E works!" ✓
### OneCLI Integration ### OneCLI Integration
@@ -23,14 +23,14 @@ Last updated: 2026-04-09, branch `v2`, commit `1dc5750`
### Channel Barrel ### Channel Barrel
- `src/index.ts` imports `./channels/index.js` (the barrel) - `src/index.ts` imports `./channels/index.js` (the barrel)
- Channel skills uncomment lines in the barrel to enable channels - Trunk ships the barrel + Chat SDK bridge only; `/add-<channel>` skills drop adapter files in and register them via the barrel slot
- Discord is uncommented by default (it was previously a direct import in index.ts) - No channel adapters ship in trunk
### Setup Registration (partially) ### Setup Registration (partially)
- `setup/register.ts` rewritten to create v2 entities (`agent_groups`, `messaging_groups`, `messaging_group_agents`) in `data/v2.db` - `setup/register.ts` creates entities (`agent_groups`, `messaging_groups`, `messaging_group_agents`) in `data/v2.db`
- Accepts `--platform-id` (v2) and `--jid` (v1 compat) flags - Accepts `--platform-id` flag
- Added `getMessagingGroupAgentByPair()` to prevent duplicate wiring - `getMessagingGroupAgentByPair()` prevents duplicate wiring
- `setup/verify.ts` updated to check v2 central DB (counts agent groups with wiring) - `setup/verify.ts` checks the central DB (counts agent groups with wiring)
### Router Logging ### Router Logging
- `src/router.ts` logs `MESSAGE DROPPED` at WARN level when no agents wired, with actionable guidance - `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 ## 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). 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~~ ### 2. ~~Setup SKILL.md Missing Group Registration Step~~
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~~
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. 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. 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 ## Architecture Reference
### v2 Entity Model ### Entity Model
``` ```
agent_groups (id, name, folder, agent_provider, container_config) agent_groups (id, name, folder, agent_provider, container_config)
↕ many-to-many ↕ many-to-many
@@ -93,13 +89,13 @@ Channel adapter → routeInbound() → resolve messaging_group → resolve agent
### Key Files ### Key Files
| File | Purpose | | File | Purpose |
|------|---------| |------|---------|
| `src/index.ts` | v2 entry point, imports channel barrel | | `src/index.ts` | Entry point, imports channel barrel |
| `src/channels/index.ts` | Channel barrel — uncomment to enable | | `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/router.ts` | Inbound routing, auto-creates messaging groups |
| `src/session-manager.ts` | Creates inbound.db + outbound.db per session | | `src/session-manager.ts` | Creates inbound.db + outbound.db per session |
| `src/delivery.ts` | Polls outbound.db, delivers, handles system actions | | `src/delivery.ts` | Polls outbound.db, delivers, handles system actions |
| `src/host-sweep.ts` | Syncs processing_ack, stale detection, recurrence | | `src/host-sweep.ts` | Syncs processing_ack, stale detection, recurrence |
| `src/container-runner.ts` | Spawns containers, OneCLI ensureAgent + applyContainerConfig | | `src/container-runner.ts` | Spawns containers, OneCLI ensureAgent + applyContainerConfig |
| `setup/register.ts` | Creates v2 entities (agent_group, messaging_group, wiring) | | `setup/register.ts` | Creates entities (agent_group, messaging_group, wiring) |
| `setup/verify.ts` | Checks v2 central DB for registered groups | | `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) | | `container/agent-runner/src/db/connection.ts` | Two-DB connection layer (inbound read-only, outbound read-write) |