diff --git a/.claude/skills/debug/SKILL.md b/.claude/skills/debug/SKILL.md index a242ed7..e2cc104 100644 --- a/.claude/skills/debug/SKILL.md +++ b/.claude/skills/debug/SKILL.md @@ -20,9 +20,12 @@ src/container-runner.ts container/agent-runner/ ├── data/env/env ──────────────> /workspace/env-dir/env ├── groups/{folder} ───────────> /workspace/group ├── data/ipc ──────────────────> /workspace/ipc + ├── ~/.claude/ ────────────────> /home/node/.claude/ (sessions) └── (main only) project root ──> /workspace/project ``` +**Important:** The container runs as user `node` with `HOME=/home/node`. Session files must be mounted to `/home/node/.claude/` (not `/root/.claude/`) for session resumption to work. + ## Log Locations | Log | Location | Content | @@ -131,7 +134,38 @@ container run --rm --entrypoint /bin/bash nanoclaw-agent:latest -c ' All of `/workspace/` and `/app/` should be owned by `node`. -### 5. MCP Server Failures +### 5. Session Not Resuming / "Claude Code process exited with code 1" + +If sessions aren't being resumed (new session ID every time), or Claude Code exits with code 1 when resuming: + +**Root cause:** The SDK looks for sessions at `$HOME/.claude/projects/`. Inside the container, `HOME=/home/node`, so it looks at `/home/node/.claude/projects/`. + +**Check the mount path:** +```bash +# In container-runner.ts, verify mount is to /home/node/.claude/, NOT /root/.claude/ +grep -A3 "Claude sessions" src/container-runner.ts +``` + +**Verify sessions are accessible:** +```bash +container run --rm --entrypoint /bin/bash \ + -v ~/.claude:/home/node/.claude \ + nanoclaw-agent:latest -c ' +echo "HOME=$HOME" +ls -la $HOME/.claude/projects/ 2>&1 | head -5 +' +``` + +**Fix:** Ensure `container-runner.ts` mounts to `/home/node/.claude/`: +```typescript +mounts.push({ + hostPath: claudeDir, + containerPath: '/home/node/.claude', // NOT /root/.claude + readonly: false +}); +``` + +### 6. MCP Server Failures If an MCP server fails to start, the agent may exit. Test MCP servers individually: @@ -229,7 +263,14 @@ container run --rm --entrypoint /bin/bash nanoclaw-agent:latest -c ' ## Session Persistence -Claude sessions are stored in `~/.claude/` which is mounted into the container. To clear sessions: +Claude sessions are stored in `~/.claude/projects/` on the host, mounted to `/home/node/.claude/projects/` inside the container. + +**Critical:** The mount path must match the container user's HOME directory: +- Container user: `node` +- Container HOME: `/home/node` +- Mount target: `/home/node/.claude/` (NOT `/root/.claude/`) + +To clear sessions: ```bash # Clear all sessions @@ -237,6 +278,15 @@ rm -rf ~/.claude/projects/ # Clear sessions for a specific group rm -rf ~/.claude/projects/*workspace-group*/ + +# Also clear the session ID from NanoClaw's tracking +echo '{}' > data/sessions.json +``` + +To verify session resumption is working, check the logs for the same session ID across messages: +```bash +grep "Session initialized" logs/nanoclaw.log | tail -5 +# Should show the SAME session ID for consecutive messages in the same group ``` ## IPC Debugging @@ -267,15 +317,22 @@ echo -e "\n1. API Key configured?" echo -e "\n2. Env file copied for container?" [ -f data/env/env ] && echo "OK" || echo "MISSING - will be created on first run" -echo -e "\n3. Container image exists?" -container images 2>/dev/null | grep -q nanoclaw-agent && echo "OK" || echo "MISSING - run ./container/build.sh" +echo -e "\n3. Apple Container system running?" +container system status &>/dev/null && echo "OK" || echo "NOT RUNNING - NanoClaw should auto-start it; check logs" -echo -e "\n4. Apple Container running?" -container system info &>/dev/null && echo "OK" || echo "NOT RUNNING - run: container system start" +echo -e "\n4. Container image exists?" +echo '{}' | container run -i --entrypoint /bin/echo nanoclaw-agent:latest "OK" 2>/dev/null || echo "MISSING - run ./container/build.sh" -echo -e "\n5. Groups directory?" +echo -e "\n5. Session mount path correct?" +grep -q "/home/node/.claude" src/container-runner.ts 2>/dev/null && echo "OK" || echo "WRONG - should mount to /home/node/.claude/, not /root/.claude/" + +echo -e "\n6. Groups directory?" ls -la groups/ 2>/dev/null || echo "MISSING - run setup" -echo -e "\n6. Recent container logs?" +echo -e "\n7. Recent container logs?" ls -t groups/*/logs/container-*.log 2>/dev/null | head -3 || echo "No container logs yet" + +echo -e "\n8. Session continuity working?" +SESSIONS=$(grep "Session initialized" logs/nanoclaw.log 2>/dev/null | tail -5 | awk '{print $NF}' | sort -u | wc -l) +[ "$SESSIONS" -le 2 ] && echo "OK (recent sessions reusing IDs)" || echo "CHECK - multiple different session IDs, may indicate resumption issues" ``` diff --git a/.claude/skills/setup/SKILL.md b/.claude/skills/setup/SKILL.md index 20dd310..d1486bd 100644 --- a/.claude/skills/setup/SKILL.md +++ b/.claude/skills/setup/SKILL.md @@ -33,10 +33,12 @@ If not installed, tell the user: Wait for user confirmation, then verify: ```bash -container system start 2>/dev/null || true +container system start container --version ``` +**Note:** NanoClaw automatically starts the Apple Container system when it launches, so you don't need to start it manually after reboots. + ## 3. Configure API Key Ask the user: @@ -268,6 +270,10 @@ The user should receive a response in WhatsApp. **Service not starting**: Check `logs/nanoclaw.error.log` +**Container agent fails with "Claude Code process exited with code 1"**: +- Ensure Apple Container is running: `container system start` +- Check container logs: `cat groups/main/logs/container-*.log | tail -50` + **No response to messages**: - Verify the trigger pattern matches (e.g., `@AssistantName` at start of message) - Check that the chat JID is in `data/registered_groups.json` diff --git a/SPEC.md b/SPEC.md index cce7617..686967a 100644 --- a/SPEC.md +++ b/SPEC.md @@ -164,12 +164,17 @@ nanoclaw/ Configuration constants are in `src/config.ts`: ```typescript +import path from 'path'; + export const ASSISTANT_NAME = process.env.ASSISTANT_NAME || 'Andy'; export const POLL_INTERVAL = 2000; export const SCHEDULER_POLL_INTERVAL = 60000; -export const STORE_DIR = './store'; -export const GROUPS_DIR = './groups'; -export const DATA_DIR = './data'; + +// Paths are absolute (required for container mounts) +const PROJECT_ROOT = process.cwd(); +export const STORE_DIR = path.resolve(PROJECT_ROOT, 'store'); +export const GROUPS_DIR = path.resolve(PROJECT_ROOT, 'groups'); +export const DATA_DIR = path.resolve(PROJECT_ROOT, 'data'); // Container configuration export const CONTAINER_IMAGE = process.env.CONTAINER_IMAGE || 'nanoclaw-agent:latest'; @@ -180,6 +185,8 @@ export const TRIGGER_PATTERN = new RegExp(`^@${ASSISTANT_NAME}\\b`, 'i'); export const CLEAR_COMMAND = '/clear'; ``` +**Note:** Paths must be absolute for Apple Container volume mounts to work correctly. + ### Container Configuration Groups can have additional directories mounted via `containerConfig` in `data/registered_groups.json`: @@ -489,6 +496,17 @@ Provides email capabilities. Requires Google Cloud OAuth setup. NanoClaw runs as a single macOS launchd service. +### Startup Sequence + +When NanoClaw starts, it: +1. **Ensures Apple Container system is running** - Automatically starts it if needed (survives reboots) +2. Initializes the SQLite database +3. Loads state (registered groups, sessions, router state) +4. Connects to WhatsApp +5. Starts the message polling loop +6. Starts the scheduler loop +7. Starts the IPC watcher for container messages + ### Service: com.nanoclaw **launchd/com.nanoclaw.plist:** @@ -601,9 +619,12 @@ chmod 700 groups/ | Issue | Cause | Solution | |-------|-------|----------| | No response to messages | Service not running | Check `launchctl list | grep nanoclaw` | +| "Claude Code process exited with code 1" | Apple Container failed to start | Check logs; NanoClaw auto-starts container system but may fail | +| "Claude Code process exited with code 1" | Session mount path wrong | Ensure mount is to `/home/node/.claude/` not `/root/.claude/` | +| Session not continuing | Session ID not saved | Check `data/sessions.json` | +| Session not continuing | Mount path mismatch | Container user is `node` with HOME=/home/node; sessions must be at `/home/node/.claude/` | | "QR code expired" | WhatsApp session expired | Delete store/auth/ and restart | | "No groups registered" | Haven't added groups | Use `@Andy add group "Name"` in main | -| Session not continuing | Session ID not saved | Check `data/sessions.json` | ### Log Location diff --git a/container/agent-runner/src/index.ts b/container/agent-runner/src/index.ts index 641da21..5b8cf9d 100644 --- a/container/agent-runner/src/index.ts +++ b/container/agent-runner/src/index.ts @@ -111,12 +111,13 @@ async function main(): Promise { }); } catch (err) { - log(`Agent error: ${err instanceof Error ? err.message : String(err)}`); + const errorMessage = err instanceof Error ? err.message : String(err); + log(`Agent error: ${errorMessage}`); writeOutput({ status: 'error', result: null, newSessionId, - error: err instanceof Error ? err.message : String(err) + error: errorMessage }); process.exit(1); } diff --git a/src/config.ts b/src/config.ts index 81bafdb..9108bbc 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,9 +1,14 @@ +import path from 'path'; + export const ASSISTANT_NAME = process.env.ASSISTANT_NAME || 'Andy'; export const POLL_INTERVAL = 2000; export const SCHEDULER_POLL_INTERVAL = 60000; // Check for due tasks every minute -export const STORE_DIR = './store'; -export const GROUPS_DIR = './groups'; -export const DATA_DIR = './data'; + +// Use absolute paths for container mounts +const PROJECT_ROOT = process.cwd(); +export const STORE_DIR = path.resolve(PROJECT_ROOT, 'store'); +export const GROUPS_DIR = path.resolve(PROJECT_ROOT, 'groups'); +export const DATA_DIR = path.resolve(PROJECT_ROOT, 'data'); export const MAIN_GROUP_FOLDER = 'main'; // Container configuration diff --git a/src/container-runner.ts b/src/container-runner.ts index cd22e89..8f13f5b 100644 --- a/src/container-runner.ts +++ b/src/container-runner.ts @@ -80,11 +80,12 @@ function buildVolumeMounts(group: RegisteredGroup, isMain: boolean): VolumeMount } // Claude sessions directory (for session persistence) + // Container runs as 'node' user with HOME=/home/node const claudeDir = path.join(homeDir, '.claude'); if (fs.existsSync(claudeDir)) { mounts.push({ hostPath: claudeDir, - containerPath: '/root/.claude', + containerPath: '/home/node/.claude', readonly: false }); } @@ -94,7 +95,7 @@ function buildVolumeMounts(group: RegisteredGroup, isMain: boolean): VolumeMount if (fs.existsSync(gmailDir)) { mounts.push({ hostPath: gmailDir, - containerPath: '/root/.gmail-mcp', + containerPath: '/home/node/.gmail-mcp', readonly: false }); } diff --git a/src/index.ts b/src/index.ts index 5ddd27b..461246e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,7 +5,7 @@ import makeWASocket, { WASocket } from '@whiskeysockets/baileys'; import pino from 'pino'; -import { exec } from 'child_process'; +import { exec, execSync } from 'child_process'; import fs from 'fs'; import path from 'path'; @@ -376,7 +376,25 @@ async function startMessageLoop(): Promise { } } +function ensureContainerSystemRunning(): void { + try { + // Check if container system is already running + execSync('container system status', { stdio: 'pipe' }); + logger.debug('Apple Container system already running'); + } catch { + // Not running, try to start it + logger.info('Starting Apple Container system...'); + try { + execSync('container system start', { stdio: 'pipe', timeout: 30000 }); + logger.info('Apple Container system started'); + } catch (err) { + logger.error({ err }, 'Failed to start Apple Container system - agents will not work'); + } + } +} + async function main(): Promise { + ensureContainerSystemRunning(); initDatabase(); logger.info('Database initialized'); loadState();