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:
@@ -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"
|
||||
```
|
||||
|
||||
@@ -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
29
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
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
|
||||
20
src/index.ts
20
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<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();
|
||||
|
||||
Reference in New Issue
Block a user