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