diff --git a/.claude/skills/add-discord/SKILL.md b/.claude/skills/add-discord/SKILL.md
index 6d3ccc8..f22c0c7 100644
--- a/.claude/skills/add-discord/SKILL.md
+++ b/.claude/skills/add-discord/SKILL.md
@@ -44,7 +44,7 @@ import './discord.js';
### 4. Install the adapter package (pinned)
```bash
-pnpm install @chat-adapter/discord@4.26.0
+pnpm install @chat-adapter/discord@4.27.0
```
### 5. Build
diff --git a/.claude/skills/add-gchat/SKILL.md b/.claude/skills/add-gchat/SKILL.md
index c4d8dfd..b3b7d1b 100644
--- a/.claude/skills/add-gchat/SKILL.md
+++ b/.claude/skills/add-gchat/SKILL.md
@@ -44,7 +44,7 @@ import './gchat.js';
### 4. Install the adapter package (pinned)
```bash
-pnpm install @chat-adapter/gchat@4.26.0
+pnpm install @chat-adapter/gchat@4.27.0
```
### 5. Build
diff --git a/.claude/skills/add-github/SKILL.md b/.claude/skills/add-github/SKILL.md
index 78366f3..2441f13 100644
--- a/.claude/skills/add-github/SKILL.md
+++ b/.claude/skills/add-github/SKILL.md
@@ -48,7 +48,7 @@ import './github.js';
### 4. Install the adapter package (pinned)
```bash
-pnpm install @chat-adapter/github@4.26.0
+pnpm install @chat-adapter/github@4.27.0
```
### 5. Build
diff --git a/.claude/skills/add-linear/SKILL.md b/.claude/skills/add-linear/SKILL.md
index dc657af..237aaa0 100644
--- a/.claude/skills/add-linear/SKILL.md
+++ b/.claude/skills/add-linear/SKILL.md
@@ -87,7 +87,7 @@ Linear OAuth apps can't be @-mentioned, so the bridge's `onNewMention` handler n
### 5. Install the adapter package (pinned)
```bash
-pnpm install @chat-adapter/linear@4.26.0
+pnpm install @chat-adapter/linear@4.27.0
```
### 6. Build
diff --git a/.claude/skills/add-slack/SKILL.md b/.claude/skills/add-slack/SKILL.md
index d09db61..0b67b50 100644
--- a/.claude/skills/add-slack/SKILL.md
+++ b/.claude/skills/add-slack/SKILL.md
@@ -44,7 +44,7 @@ import './slack.js';
### 4. Install the adapter package (pinned)
```bash
-pnpm install @chat-adapter/slack@4.26.0
+pnpm install @chat-adapter/slack@4.27.0
```
### 5. Build
diff --git a/.claude/skills/add-teams/SKILL.md b/.claude/skills/add-teams/SKILL.md
index 10bce29..f6eeaf9 100644
--- a/.claude/skills/add-teams/SKILL.md
+++ b/.claude/skills/add-teams/SKILL.md
@@ -44,7 +44,7 @@ import './teams.js';
### 4. Install the adapter package (pinned)
```bash
-pnpm install @chat-adapter/teams@4.26.0
+pnpm install @chat-adapter/teams@4.27.0
```
### 5. Build
diff --git a/.claude/skills/add-telegram/SKILL.md b/.claude/skills/add-telegram/SKILL.md
index f605b41..03247c5 100644
--- a/.claude/skills/add-telegram/SKILL.md
+++ b/.claude/skills/add-telegram/SKILL.md
@@ -58,7 +58,7 @@ In `setup/index.ts`, add this entry to the `STEPS` map (right after the `registe
### 5. Install the adapter package (pinned)
```bash
-pnpm install @chat-adapter/telegram@4.26.0
+pnpm install @chat-adapter/telegram@4.27.0
```
### 6. Build
diff --git a/.claude/skills/add-whatsapp-cloud/SKILL.md b/.claude/skills/add-whatsapp-cloud/SKILL.md
index d08f375..7e8bd1c 100644
--- a/.claude/skills/add-whatsapp-cloud/SKILL.md
+++ b/.claude/skills/add-whatsapp-cloud/SKILL.md
@@ -44,7 +44,7 @@ import './whatsapp-cloud.js';
### 4. Install the adapter package (pinned)
```bash
-pnpm install @chat-adapter/whatsapp@4.26.0
+pnpm install @chat-adapter/whatsapp@4.27.0
```
### 5. Build
diff --git a/.claude/skills/add-whatsapp/SKILL.md b/.claude/skills/add-whatsapp/SKILL.md
index 3f10ce1..232725f 100644
--- a/.claude/skills/add-whatsapp/SKILL.md
+++ b/.claude/skills/add-whatsapp/SKILL.md
@@ -57,7 +57,7 @@ groups: () => import('./groups.js'),
### 5. Install the adapter packages (pinned)
```bash
-pnpm install @whiskeysockets/baileys@6.17.16 qrcode@1.5.4 @types/qrcode@1.5.6 pino@9.6.0
+pnpm install @whiskeysockets/baileys@7.0.0-rc.9 qrcode@1.5.4 @types/qrcode@1.5.6 pino@9.6.0
```
### 6. Build
diff --git a/container/agent-runner/src/db/connection.ts b/container/agent-runner/src/db/connection.ts
index 3ca44a8..871e43a 100644
--- a/container/agent-runner/src/db/connection.ts
+++ b/container/agent-runner/src/db/connection.ts
@@ -27,21 +27,29 @@ const DEFAULT_HEARTBEAT_PATH = '/workspace/.heartbeat';
let _inbound: Database | null = null;
let _outbound: Database | null = null;
let _heartbeatPath: string = DEFAULT_HEARTBEAT_PATH;
+let _testMode = false;
/**
- * Avoid all cached db reads; open inbound.db read-only with mmap and page cache disabled.
- *
+ * Avoid all cached db reads; open inbound.db read-only with mmap and page cache disabled.
+ *
* Use this (not getInboundDb) for readers that need to see host-written rows
* promptly — e.g. messages_in polling. Caller must .close() the returned
* connection (try/finally).
*
* Needed for mounts where host writes don't reliably invalidate
* SQLite's caches: virtiofs (Colima, Lima, Podman Machine, Apple
- * Container), NFS.
- *
+ * Container), NFS.
+ *
* Cost is microseconds per query, so safe for universal use.
*/
export function openInboundDb(): Database {
+ // In test mode return a thin wrapper over the in-memory singleton.
+ // Callers do try/finally { db.close() } — the wrapper no-ops close()
+ // so the singleton survives for the rest of the test.
+ if (_testMode && _inbound) {
+ const db = _inbound;
+ return { prepare: (sql: string) => db.prepare(sql), exec: (sql: string) => db.exec(sql), close: () => {} } as unknown as Database;
+ }
const db = new Database(DEFAULT_INBOUND_PATH, { readonly: true });
db.exec('PRAGMA busy_timeout = 5000');
db.exec('PRAGMA mmap_size = 0');
@@ -170,6 +178,7 @@ export function clearStaleProcessingAcks(): void {
/** For tests — creates in-memory DBs with the session schemas. */
export function initTestSessionDb(): { inbound: Database; outbound: Database } {
+ _testMode = true;
_inbound = new Database(':memory:');
_inbound.exec('PRAGMA foreign_keys = ON');
_inbound.exec(`
@@ -246,6 +255,7 @@ export function initTestSessionDb(): { inbound: Database; outbound: Database } {
export function closeSessionDb(): void {
_inbound?.close();
_inbound = null;
+ _testMode = false;
_outbound?.close();
_outbound = null;
}
diff --git a/container/agent-runner/src/providers/claude.ts b/container/agent-runner/src/providers/claude.ts
index c9478b8..6c30cc2 100644
--- a/container/agent-runner/src/providers/claude.ts
+++ b/container/agent-runner/src/providers/claude.ts
@@ -34,7 +34,11 @@ const SDK_DISALLOWED_TOOLS = [
'ExitWorktree',
];
-// Tool allowlist for NanoClaw agent containers
+// Tool allowlist for NanoClaw agent containers. MCP-tool entries are derived
+// at the call site from the registered `mcpServers` map so that any server
+// added via `add_mcp_server` (or wired in container.json directly) is
+// reachable to the agent — without this, the SDK's allowedTools filter
+// silently drops every MCP namespace not listed here.
const TOOL_ALLOWLIST = [
'Bash',
'Read',
@@ -54,9 +58,15 @@ const TOOL_ALLOWLIST = [
'ToolSearch',
'Skill',
'NotebookEdit',
- 'mcp__nanoclaw__*',
];
+// MCP server names are sanitized by the SDK when forming tool prefixes:
+// any character outside [A-Za-z0-9_-] becomes '_'. Mirror that here so our
+// allowlist patterns match what the SDK actually exposes.
+function mcpAllowPattern(serverName: string): string {
+ return `mcp__${serverName.replace(/[^a-zA-Z0-9_-]/g, '_')}__*`;
+}
+
interface SDKUserMessage {
type: 'user';
message: { role: 'user'; content: string };
@@ -277,7 +287,10 @@ export class ClaudeProvider implements AgentProvider {
resume: input.continuation,
pathToClaudeCodeExecutable: '/pnpm/claude',
systemPrompt: instructions ? { type: 'preset' as const, preset: 'claude_code' as const, append: instructions } : undefined,
- allowedTools: TOOL_ALLOWLIST,
+ allowedTools: [
+ ...TOOL_ALLOWLIST,
+ ...Object.keys(this.mcpServers).map(mcpAllowPattern),
+ ],
disallowedTools: SDK_DISALLOWED_TOOLS,
env: this.env,
permissionMode: 'bypassPermissions',
diff --git a/migrate-v2.sh b/migrate-v2.sh
index f06a548..2325edd 100644
--- a/migrate-v2.sh
+++ b/migrate-v2.sh
@@ -408,20 +408,12 @@ else
fi
done
- # 2d. WhatsApp LID resolution. After whatsapp is installed (so Baileys
- # is on disk) and auth files have been copied (so we can connect with
- # the migrated identity), boot Baileys briefly to learn LID↔phone
- # mappings during initial sync, then write paired LID-keyed
- # messaging_groups. Best-effort: any failure degrades to runtime
- # approval flow, which the WA adapter's isMention=true on DMs handles.
- for ch in "${SELECTED_CHANNELS[@]}"; do
- if [ "$ch" = "whatsapp" ]; then
- run_step "2d-whatsapp-lids" \
- "Resolve WhatsApp LIDs for migrated DMs" \
- "setup/migrate-v2/whatsapp-resolve-lids.ts"
- break
- fi
- done
+ # 2d. (Removed) WhatsApp LID resolution was previously needed because the
+ # v6 adapter couldn't reliably translate LID→phone JIDs, so the migration
+ # pre-created dual messaging_groups rows. With Baileys v7, the adapter
+ # resolves LIDs via extractAddressingContext + signalRepository.lidMapping
+ # on every inbound message, so dual rows are unnecessary and were causing
+ # split sessions.
fi
echo
diff --git a/nanoclaw.sh b/nanoclaw.sh
index 82d445a..bcf4e49 100755
--- a/nanoclaw.sh
+++ b/nanoclaw.sh
@@ -137,6 +137,83 @@ write_header
# NANOCLAW_BOOTSTRAPPED=1 and skips re-printing the wordmark.
cat "$PROJECT_ROOT/assets/setup-splash.txt"
+# ─── pre-flight: minimum hardware specs ────────────────────────────────
+# NanoClaw runs an agent container per session. Below this threshold the
+# host + container + agent will struggle (OOM under load). Soft warn — the
+# user can override.
+
+# RAM floor is set below 4 GB because "4 GB" VMs typically report 3700–3900 MB
+# after kernel reserves (e.g. Hetzner CX21 ≈ 3814, AWS t3.medium ≈ 3800).
+MIN_MEM_MB=3700
+
+detect_mem_mb() {
+ case "$(uname -s)" in
+ Linux)
+ awk '/^MemTotal:/ {printf "%d", $2 / 1024}' /proc/meminfo 2>/dev/null
+ ;;
+ Darwin)
+ local bytes
+ bytes=$(sysctl -n hw.memsize 2>/dev/null || echo 0)
+ echo $(( bytes / 1024 / 1024 ))
+ ;;
+ esac
+}
+
+MEM_MB=$(detect_mem_mb)
+: "${MEM_MB:=0}"
+
+LOW_MEM=false
+[ "$MEM_MB" -gt 0 ] && [ "$MEM_MB" -lt "$MIN_MEM_MB" ] && LOW_MEM=true
+
+if [ "$LOW_MEM" = true ]; then
+ printf ' %s\n' "$(red 'Warning: this machine likely cannot run NanoClaw.')"
+ printf ' %s\n' "$(dim 'NanoClaw recommends a 4 GB+ RAM machine. Below this, the host + agent')"
+ printf ' %s\n' "$(dim 'container will run out of memory under most workloads. A stronger')"
+ printf ' %s\n' "$(dim 'machine is strongly recommended.')"
+ printf ' %s\n' "$(dim " · Detected RAM: ${MEM_MB} MB")"
+ printf '\n'
+ read -r -p " $(bold 'Try anyway?') [y/N] " SPECS_ANS /dev/null \
+ || grep -qi 'Google' /sys/class/dmi/id/sys_vendor 2>/dev/null; }; then
+ printf ' %s\n' "$(red 'Warning: Google Cloud VM detected.')"
+ printf ' %s\n' "$(dim 'Google blocks sudo commands, so NanoClaw is unlikely to run successfully on this VM.')"
+ printf ' %s\n\n' "$(dim 'If you want to run NanoClaw successfully, switch to a different provider (Hetzner, Hostinger, exe.dev and others..).')"
+ read -r -p " $(bold 'Try anyway?') [y/N] " GCE_ANS
-
140k tokens, 70% of context window
+