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 ├── data/env/env ──────────────> /workspace/env-dir/env
├── groups/{folder} ───────────> /workspace/group ├── groups/{folder} ───────────> /workspace/group
├── data/ipc ──────────────────> /workspace/ipc ├── data/ipc ──────────────────> /workspace/ipc
├── ~/.claude/ ────────────────> /home/node/.claude/ (sessions)
└── (main only) project root ──> /workspace/project └── (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 Locations
| Log | Location | Content | | 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`. 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: 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 ## 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 ```bash
# Clear all sessions # Clear all sessions
@@ -237,6 +278,15 @@ rm -rf ~/.claude/projects/
# Clear sessions for a specific group # Clear sessions for a specific group
rm -rf ~/.claude/projects/*workspace-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 ## IPC Debugging
@@ -267,15 +317,22 @@ echo -e "\n1. API Key configured?"
echo -e "\n2. Env file copied for container?" echo -e "\n2. Env file copied for container?"
[ -f data/env/env ] && echo "OK" || echo "MISSING - will be created on first run" [ -f data/env/env ] && echo "OK" || echo "MISSING - will be created on first run"
echo -e "\n3. Container image exists?" echo -e "\n3. Apple Container system running?"
container images 2>/dev/null | grep -q nanoclaw-agent && echo "OK" || echo "MISSING - run ./container/build.sh" container system status &>/dev/null && echo "OK" || echo "NOT RUNNING - NanoClaw should auto-start it; check logs"
echo -e "\n4. Apple Container running?" echo -e "\n4. Container image exists?"
container system info &>/dev/null && echo "OK" || echo "NOT RUNNING - run: container system start" 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" 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" 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: Wait for user confirmation, then verify:
```bash ```bash
container system start 2>/dev/null || true container system start
container --version 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 ## 3. Configure API Key
Ask the user: Ask the user:
@@ -268,6 +270,10 @@ The user should receive a response in WhatsApp.
**Service not starting**: Check `logs/nanoclaw.error.log` **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**: **No response to messages**:
- Verify the trigger pattern matches (e.g., `@AssistantName` at start of message) - Verify the trigger pattern matches (e.g., `@AssistantName` at start of message)
- Check that the chat JID is in `data/registered_groups.json` - 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`: Configuration constants are in `src/config.ts`:
```typescript ```typescript
import path from 'path';
export const ASSISTANT_NAME = process.env.ASSISTANT_NAME || 'Andy'; export const ASSISTANT_NAME = process.env.ASSISTANT_NAME || 'Andy';
export const POLL_INTERVAL = 2000; export const POLL_INTERVAL = 2000;
export const SCHEDULER_POLL_INTERVAL = 60000; export const SCHEDULER_POLL_INTERVAL = 60000;
export const STORE_DIR = './store';
export const GROUPS_DIR = './groups'; // Paths are absolute (required for container mounts)
export const DATA_DIR = './data'; 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 // Container configuration
export const CONTAINER_IMAGE = process.env.CONTAINER_IMAGE || 'nanoclaw-agent:latest'; 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'; export const CLEAR_COMMAND = '/clear';
``` ```
**Note:** Paths must be absolute for Apple Container volume mounts to work correctly.
### Container Configuration ### Container Configuration
Groups can have additional directories mounted via `containerConfig` in `data/registered_groups.json`: 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. 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 ### Service: com.nanoclaw
**launchd/com.nanoclaw.plist:** **launchd/com.nanoclaw.plist:**
@@ -601,9 +619,12 @@ chmod 700 groups/
| Issue | Cause | Solution | | Issue | Cause | Solution |
|-------|-------|----------| |-------|-------|----------|
| No response to messages | Service not running | Check `launchctl list | grep nanoclaw` | | 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 | | "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 | | "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 ### Log Location

View File

@@ -111,12 +111,13 @@ async function main(): Promise<void> {
}); });
} catch (err) { } 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({ writeOutput({
status: 'error', status: 'error',
result: null, result: null,
newSessionId, newSessionId,
error: err instanceof Error ? err.message : String(err) error: errorMessage
}); });
process.exit(1); process.exit(1);
} }

View File

@@ -1,9 +1,14 @@
import path from 'path';
export const ASSISTANT_NAME = process.env.ASSISTANT_NAME || 'Andy'; export const ASSISTANT_NAME = process.env.ASSISTANT_NAME || 'Andy';
export const POLL_INTERVAL = 2000; export const POLL_INTERVAL = 2000;
export const SCHEDULER_POLL_INTERVAL = 60000; // Check for due tasks every minute export const SCHEDULER_POLL_INTERVAL = 60000; // Check for due tasks every minute
export const STORE_DIR = './store';
export const GROUPS_DIR = './groups'; // Use absolute paths for container mounts
export const DATA_DIR = './data'; 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'; export const MAIN_GROUP_FOLDER = 'main';
// Container configuration // Container configuration

View File

@@ -80,11 +80,12 @@ function buildVolumeMounts(group: RegisteredGroup, isMain: boolean): VolumeMount
} }
// Claude sessions directory (for session persistence) // Claude sessions directory (for session persistence)
// Container runs as 'node' user with HOME=/home/node
const claudeDir = path.join(homeDir, '.claude'); const claudeDir = path.join(homeDir, '.claude');
if (fs.existsSync(claudeDir)) { if (fs.existsSync(claudeDir)) {
mounts.push({ mounts.push({
hostPath: claudeDir, hostPath: claudeDir,
containerPath: '/root/.claude', containerPath: '/home/node/.claude',
readonly: false readonly: false
}); });
} }
@@ -94,7 +95,7 @@ function buildVolumeMounts(group: RegisteredGroup, isMain: boolean): VolumeMount
if (fs.existsSync(gmailDir)) { if (fs.existsSync(gmailDir)) {
mounts.push({ mounts.push({
hostPath: gmailDir, hostPath: gmailDir,
containerPath: '/root/.gmail-mcp', containerPath: '/home/node/.gmail-mcp',
readonly: false readonly: false
}); });
} }

View File

@@ -5,7 +5,7 @@ import makeWASocket, {
WASocket WASocket
} from '@whiskeysockets/baileys'; } from '@whiskeysockets/baileys';
import pino from 'pino'; import pino from 'pino';
import { exec } from 'child_process'; import { exec, execSync } from 'child_process';
import fs from 'fs'; import fs from 'fs';
import path from 'path'; 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> { async function main(): Promise<void> {
ensureContainerSystemRunning();
initDatabase(); initDatabase();
logger.info('Database initialized'); logger.info('Database initialized');
loadState(); loadState();