Fix session persistence and auto-start container system

- Fix session mount path: ~/.claude/ now mounts to /home/node/.claude/
  (container runs as 'node' user with HOME=/home/node, not root)
- Fix ~/.gmail-mcp/ mount path similarly
- Use absolute paths for GROUPS_DIR and DATA_DIR (required for container mounts)
- Auto-start Apple Container system on NanoClaw startup
- Update debug skill with session troubleshooting guide
- Update spec.md with startup sequence and troubleshooting

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Gavriel
2026-02-01 11:31:52 +02:00
parent 67e0295d82
commit 8ca4c95517
7 changed files with 130 additions and 21 deletions

View File

@@ -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"
```

View File

@@ -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`

29
SPEC.md
View File

@@ -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

View File

@@ -111,12 +111,13 @@ async function main(): Promise<void> {
});
} 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);
}

View File

@@ -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

View File

@@ -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
});
}

View File

@@ -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<void> {
}
}
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<void> {
ensureContainerSystemRunning();
initDatabase();
logger.info('Database initialized');
loadState();