Skip to main content
NanoClaw Pro uses Claude’s session system to maintain conversation context across interactions. Each group has an isolated session that persists between agent invocations.

Overview

Sessions enable the agent to remember:
  • Previous conversation turns
  • Files it has read or created
  • Tool usage history
  • Context from earlier in the conversation

How Sessions Work

1. Session Storage

Sessions are stored per-group in isolated directories:
data/
└── sessions/
    ├── whatsapp_family-chat/
    │   └── .claude/
    │       ├── settings.json          # Session config
    │       ├── session_abc123.jsonl   # Conversation transcript
    │       └── memory.json            # Auto-memory (user preferences)
    ├── whatsapp_work-team/
    │   └── .claude/
    │       └── session_xyz789.jsonl
    └── whatsapp_main/
        └── .claude/
            └── session_main001.jsonl
Key points:
  • Each group has its own .claude/ directory
  • Sessions are isolated - groups can’t see each other’s history
  • JSONL format allows incremental updates
Location: Created by src/container-runner.ts:115-145

2. Session Mounting

The group’s .claude/ directory is mounted into the container:
// From src/container-runner.ts:115-162
const groupSessionsDir = path.join(
  DATA_DIR,
  'sessions',
  group.folder,
  '.claude',
);
fs.mkdirSync(groupSessionsDir, { recursive: true });

// Create settings.json if missing
const settingsFile = path.join(groupSessionsDir, 'settings.json');
if (!fs.existsSync(settingsFile)) {
  fs.writeFileSync(
    settingsFile,
    JSON.stringify(
      {
        env: {
          CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS: '1',
          CLAUDE_CODE_ADDITIONAL_DIRECTORIES_CLAUDE_MD: '1',
          CLAUDE_CODE_DISABLE_AUTO_MEMORY: '0',
        },
      },
      null,
      2,
    ) + '\n',
  );
}

mounts.push({
  hostPath: groupSessionsDir,
  containerPath: '/home/node/.claude',
  readonly: false,
});
Mount details:
  • Host: data/sessions/{group}/.claude/
  • Container: /home/node/.claude/
  • Read-write (agent needs to update transcript)
Location: src/container-runner.ts:115-162
The container runs as node user (uid 1000) with HOME=/home/node, so the mount path must be /home/node/.claude/, not /root/.claude/.

3. Session ID Tracking

Session IDs are stored in SQLite and passed to the agent:
// From src/index.ts:260-339
async function runAgent(
  group: RegisteredGroup,
  prompt: string,
  chatJid: string,
  onOutput?: (output: ContainerOutput) => Promise<void>,
): Promise<'success' | 'error'> {
  const sessionId = sessions[group.folder];
  
  const output = await runContainerAgent(
    group,
    {
      prompt,
      sessionId,        // <-- Resume session if exists
      groupFolder: group.folder,
      chatJid,
      isMain,
    },
    ...
  );
  
  // Save new/updated session ID
  if (output.newSessionId) {
    sessions[group.folder] = output.newSessionId;
    setSession(group.folder, output.newSessionId);
  }
  
  return output.status === 'error' ? 'error' : 'success';
}
Flow:
  1. Load sessionId from in-memory cache (initialized from SQLite)
  2. Pass to container via stdin
  3. Agent resumes session (or creates new one if null)
  4. Agent returns updated sessionId in output
  5. Save to cache and SQLite
Location: src/index.ts:260-339

4. Claude Agent SDK Integration

Inside the container, the agent-runner uses Claude Agent SDK’s resume option:
// From container/agent-runner/src/index.ts (conceptual)
import { Agent } from '@anthropic-ai/claude-agent-sdk';

const input: ContainerInput = JSON.parse(await readStdin());

const agent = new Agent({
  settingSources: ['project'],  // Loads CLAUDE.md files
  ...(input.sessionId && { resume: input.sessionId }),
});

const result = await agent.query(input.prompt);

// Return session ID for persistence
const output: ContainerOutput = {
  status: 'success',
  result: result.response,
  newSessionId: agent.sessionId,
};

console.log(OUTPUT_START_MARKER);
console.log(JSON.stringify(output));
console.log(OUTPUT_END_MARKER);
Resume behavior:
  • If sessionId provided: Load existing transcript, continue conversation
  • If sessionId is null: Create new session, start fresh
Location: container/agent-runner/src/index.ts

Session Lifecycle

First Interaction (New Session)

Subsequent Interactions (Resume Session)

Session Isolation

Each group has completely isolated sessions:
// Group A cannot see Group B's session

// Group A (family-chat)
data/sessions/whatsapp_family-chat/.claude/
  ├── session_abc123.jsonl    # Family conversation
  └── memory.json             # Family preferences

// Group B (work-team)  
data/sessions/whatsapp_work-team/.claude/
  ├── session_xyz789.jsonl    # Work conversation
  └── memory.json             # Work preferences
Why isolation matters:
  • Prevents information leakage between groups
  • Different groups have different contexts
  • Security boundary between trusted (main) and untrusted groups
Enforced by: Container mounts - each container only gets its group’s session dir Location: src/container-runner.ts:115-162

Session Settings

Each group’s session is configured via settings.json:
{
  "env": {
    "CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS": "1",
    "CLAUDE_CODE_ADDITIONAL_DIRECTORIES_CLAUDE_MD": "1",
    "CLAUDE_CODE_DISABLE_AUTO_MEMORY": "0"
  }
}

CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS

Enables agent swarms (subagent orchestration):
// Agent can spawn subagents for parallel work
await agent.query("Research topic X while I draft the report");
Docs

CLAUDE_CODE_ADDITIONAL_DIRECTORIES_CLAUDE_MD

Loads CLAUDE.md files from mounted directories:
/workspace/group/CLAUDE.md        # Group memory
/workspace/global/CLAUDE.md       # Global memory (if mounted)
/workspace/project/groups/CLAUDE.md  # Main group only
Docs

CLAUDE_CODE_DISABLE_AUTO_MEMORY

Enables Claude’s auto-memory feature:
  • Automatically extracts user preferences from conversation
  • Stores in memory.json
  • Applies to future interactions
Docs Location: src/container-runner.ts:122-144

Session Transcript Format

Sessions are stored as JSONL (JSON Lines):
{"role":"user","content":"What's the weather?"}
{"role":"assistant","content":"I'll check the weather for you.","tool_use":[{"name":"web_search","input":{"query":"weather"}}]}
{"role":"tool","tool_use_id":"toolu_123","content":"Sunny, 72°F"}
{"role":"assistant","content":"The weather is sunny and 72°F."}
Each line is a complete JSON object representing:
  • User messages
  • Assistant responses
  • Tool calls and results
  • System context
Benefits:
  • Incremental appends (no need to rewrite entire file)
  • Easy to parse and inspect
  • Efficient for long conversations

Session Persistence

In-Memory Cache

Session IDs are cached in memory for fast access:
// From src/index.ts:59-60
let sessions: Record<string, string> = {};

function loadState(): void {
  sessions = getAllSessions();
  // ...
}
Location: src/index.ts:59-77

SQLite Storage

Sessions are persisted in the sessions table:
CREATE TABLE sessions (
  group_folder TEXT PRIMARY KEY,
  session_id TEXT NOT NULL
);
Operations:
// From src/db.ts:514-538
export function getSession(groupFolder: string): string | undefined {
  const row = db
    .prepare('SELECT session_id FROM sessions WHERE group_folder = ?')
    .get(groupFolder) as { session_id: string } | undefined;
  return row?.session_id;
}

export function setSession(groupFolder: string, sessionId: string): void {
  db.prepare(
    'INSERT OR REPLACE INTO sessions (group_folder, session_id) VALUES (?, ?)',
  ).run(groupFolder, sessionId);
}

export function getAllSessions(): Record<string, string> {
  const rows = db
    .prepare('SELECT group_folder, session_id FROM sessions')
    .all() as Array<{ group_folder: string; session_id: string }>;
  const result: Record<string, string> = {};
  for (const row of rows) {
    result[row.group_folder] = row.session_id;
  }
  return result;
}
Location: src/db.ts:514-538

Session Updates During Streaming

When streaming output, session IDs are updated immediately:
// From src/index.ts:295-302
const wrappedOnOutput = onOutput
  ? async (output: ContainerOutput) => {
      if (output.newSessionId) {
        sessions[group.folder] = output.newSessionId;
        setSession(group.folder, output.newSessionId);
      }
      await onOutput(output);
    }
  : undefined;
Why update during streaming?
  • Session may change mid-conversation (agent creates subagents)
  • Prevents session ID loss if container crashes after first output
  • Enables piped messages to use updated session
Location: src/index.ts:295-302

Container Reuse and Sessions

When messages are piped to an active container, the session continues in-memory:
// Container already running for group
queue.sendMessage(chatJid, formatted);
// Agent processes piped message, updates session transcript
Benefits:
  • No session reload overhead
  • Conversation context stays hot in memory
  • Faster response times
Trade-off:
  • Session updates only written to disk when agent SDK flushes
  • Container crash loses unflushed updates

Session Debugging

To inspect a session:
# View session transcript
cat data/sessions/whatsapp_family-chat/.claude/session_abc123.jsonl | jq .

# View auto-memory
cat data/sessions/whatsapp_family-chat/.claude/memory.json | jq .

# Check session ID in database
sqlite3 store/messages.db "SELECT * FROM sessions WHERE group_folder = 'whatsapp_family-chat';"

Session Reset

To reset a group’s session (forget all context):
# Delete session files
rm -rf data/sessions/whatsapp_family-chat/.claude/session_*.jsonl

# Clear session ID from database
sqlite3 store/messages.db "DELETE FROM sessions WHERE group_folder = 'whatsapp_family-chat';"

# Restart NanoClaw
launchctl kickstart -k gui/$(id -u)/com.nanoclaw
Next message will create a fresh session.

Session Limitations

Context Window

Claude has a finite context window (200K tokens). Very long conversations may:
  • Truncate early messages
  • Require session reset
  • Benefit from periodic summarization

Disk Usage

Sessions grow over time. A 1000-message conversation may use:
  • 5-10 MB for transcript
  • 100-500 KB for memory
Monitor with:
du -sh data/sessions/*

Cross-Group Context

Sessions are isolated per-group. The agent cannot reference:
  • Conversations from other groups
  • Files created in other group folders
Use global memory (groups/CLAUDE.md) for shared context.

Best Practices

1. Periodic Memory Updates

Encourage the agent to write important facts to CLAUDE.md:
User: Remember that I prefer concise responses
Agent: I've noted your preference in CLAUDE.md.
This persists preferences even if sessions are reset.

2. Session Hygiene

Reset sessions when:
  • Switching topics completely
  • Conversation becomes too long (>500 messages)
  • Agent seems confused about context

3. Group Naming

Use descriptive group folder names:
  • whatsapp_family-chat
  • telegram_dev-team
  • group1
  • chat
Helps when inspecting sessions manually.

Next Steps

Message Flow

How sessions integrate with message processing

Security

Session isolation and security

Build docs developers (and LLMs) love