Skip to main content

What is a Session?

A session represents a single conversation scope with an AI agent. Sessions are the primary isolation boundary in Craft Agents:
  • Each session maps 1:1 with an SDK session (Claude Agent SDK, Pi SDK, etc.)
  • Sessions maintain independent conversation history, token usage, and metadata
  • Multiple sessions can run simultaneously in the same workspace
  • Sessions persist their complete state in JSONL format for efficient streaming
Key Insight: Sessions are conversation boundaries, not workspaces. You can have multiple conversations (sessions) in the same project (workspace) without them interfering with each other.

Session Type Definition

interface Session {
  id: string;                    // Unique identifier (stable, known immediately)
  sdkSessionId?: string;         // SDK session ID (captured after first message)
  workspaceId: string;           // Which workspace this session belongs to
  name?: string;                 // Optional user-defined name
  createdAt: number;             // Unix timestamp
  lastUsedAt: number;            // For sorting recent sessions
  
  // Workflow management
  isArchived?: boolean;          // Whether this session is archived
  isFlagged?: boolean;           // Whether this session is flagged
  status?: SessionStatus;        // 'todo' | 'in_progress' | 'needs_review' | 'done' | 'cancelled'
  
  // Read tracking
  lastReadMessageId?: string;    // ID of the last message the user has read
}

Session Status

Sessions can have workflow statuses for task tracking:
type SessionStatus = 'todo' | 'in_progress' | 'needs_review' | 'done' | 'cancelled';
The status system is extensible - workspaces can define custom statuses via statuses/config.json.

Session Storage

Sessions are stored at ~/.craft-agent/workspaces/{workspace-id}/sessions/{session-id}/:
sessions/session-abc123/
├── session.jsonl           # Messages + header (JSONL format)
├── attachments/            # File attachments
│   ├── image-1.png
│   └── document.pdf
├── plans/                  # Plan files (safe mode)
│   └── plan-1.md
├── data/                   # Transform output (JSON)
│   └── analysis.json
├── long_responses/         # Summarized tool results
│   └── large-output.txt
└── downloads/              # Binary downloads from APIs
    └── report.pdf

JSONL Format

Sessions use JSONL (JSON Lines) for efficient streaming writes:
{"id":"session-abc123","workspaceId":"ws-2024-01","createdAt":1705334400000,"lastUsedAt":1705420800000,"tokenUsage":{"inputTokens":1250,"outputTokens":830}}
{"id":"msg-1","type":"user","content":"Help me refactor this function","timestamp":1705334401000}
{"id":"msg-2","type":"assistant","content":"I'll help you refactor that function. Let me read the file first.","timestamp":1705334402000}
{"id":"msg-3","type":"tool","toolName":"Read","input":{"filePath":"./src/utils.ts"},"result":"...","timestamp":1705334403000}
Benefits:
  • Streaming writes - Append messages without rewriting the entire file
  • Incremental reads - Load messages on demand (line 1 = header, rest = messages)
  • Efficient parsing - No need to load entire conversation into memory
  • Crash resilient - Partial writes don’t corrupt previous messages

Session Persistence Implementation

See the PersistenceQueue for debounced session writes

Session Lifecycle

1. Creation

import { createSession } from '@craft-agent/shared/sessions';

const session = await createSession(
  workspace.rootPath,
  workspace.id
);

console.log(`Created session: ${session.id}`);
// Output: session-2024-01-15-abc123

2. Loading

import { loadSession } from '@craft-agent/shared/sessions';

// Load full session with messages
const session = loadSession(workspace.rootPath, sessionId);

console.log(`Messages: ${session.messages.length}`);
console.log(`Tokens: ${session.tokenUsage.inputTokens}`);

3. Metadata-Only Loading

For listing sessions, use SessionMetadata to avoid loading all messages:
import { listSessions } from '@craft-agent/shared/sessions';

// Fast listing without loading message content
const sessions = listSessions(workspace.rootPath);

sessions.forEach(session => {
  console.log(`${session.name || session.id}`);
  console.log(`  Messages: ${session.messageCount}`);
  console.log(`  Preview: ${session.preview}`);
});

4. Persistence

Sessions are persisted using a debounced write queue:
import { sessionPersistenceQueue } from '@craft-agent/shared/sessions';

// Queue a session write (debounced 500ms)
await sessionPersistenceQueue.queueWrite(
  workspace.rootPath,
  session
);

// Flush immediately if needed
await sessionPersistenceQueue.flush();
The persistence queue batches writes to avoid excessive disk I/O during rapid message streams.

5. Archiving

import { updateSession } from '@craft-agent/shared/sessions';

// Archive a session
await updateSession(workspace.rootPath, session.id, {
  isArchived: true
});

6. Deletion

import { deleteSession } from '@craft-agent/shared/sessions';

// Permanently delete session and all data
await deleteSession(workspace.rootPath, session.id);
Deletion is permanent and removes all messages, attachments, and metadata. There is no undo.

Session Configuration

Sessions have runtime configuration for agent behavior:
interface SessionConfig {
  id: string;
  workspaceId: string;
  
  // Working directory (user-changeable, independent of workspace root)
  workingDirectory?: string;
  
  // SDK storage location (for transcript files)
  sdkCwd?: string;
  
  // Permission mode for this session
  permissionMode?: PermissionMode;
  
  // Enabled sources (subset of workspace sources)
  enabledSourceSlugs?: string[];
  
  // Recovery messages for session resume
  getRecoveryMessages?: () => RecoveryMessage[];
}

Working Directory

Sessions have their own working directory, independent of the workspace root:
import { updateSession } from '@craft-agent/shared/sessions';

// Change working directory mid-session
await updateSession(workspace.rootPath, session.id, {
  workingDirectory: '/Users/alice/Projects/my-app/backend'
});
The working directory affects where bash commands run and how relative paths are resolved. It does NOT change the workspace scope.

Session-Scoped Permissions

Each session has its own permission mode:
import { getPermissionMode, setPermissionMode } from '@craft-agent/shared/agent';

// Get current mode for session
const mode = getPermissionMode(workspace.id, session.id);
console.log(mode); // 'safe' | 'ask' | 'allow-all'

// Set mode for session
setPermissionMode(workspace.id, session.id, 'safe');

Permission Mode Behavior

Explore

Mode: safeRead-only exploration. Blocks:
  • All bash commands
  • Write, Edit tools
  • MCP mutations
  • API mutations

Ask to Edit

Mode: askPrompts for bash commands:
  • Bash → permission dialog
  • Write/Edit → allowed
  • MCP/API → allowed

Auto

Mode: allow-allAuto-approves everything:
  • All tools allowed
  • No permission dialogs
  • Fastest iteration

Permission Configuration

Learn about custom permission rules and safe mode

Session Recovery

When resuming a session after a crash or restart, Craft Agents uses recovery context:
import type { RecoveryMessage } from '@craft-agent/shared/agent';

interface RecoveryMessage {
  type: 'user' | 'assistant';
  content: string;
}

// Provide recovery context to agent
const agent = createAgent({
  session: {
    id: session.id,
    getRecoveryMessages: () => [
      { type: 'user', content: 'Refactor the login function' },
      { type: 'assistant', content: 'I\'ll read the file first...' }
    ]
  }
});

Recovery Strategy

  1. Detect empty response - SDK returns empty on missing thread
  2. Extract recent messages - Get last 5-10 messages from session
  3. Format as context - Prepend to next user message
  4. Resume naturally - Agent continues from where it left off
Recovery messages are truncated to ~1000 characters each to avoid bloating context.

Session Branching

Some backends (Claude, Pi) support creating branch sessions from existing conversations:
import { createBranchSession } from '@craft-agent/shared/sessions';

const branchSession = await createBranchSession(
  workspace.rootPath,
  parentSession.id,
  parentSession.sdkSessionId,
  'Explore alternative approach'
);

console.log(`Branched from ${parentSession.id}${branchSession.id}`);
Use cases:
  • Try alternative implementations without losing progress
  • Explore “what if” scenarios
  • A/B test different approaches
Branch sessions share history up to the branch point, then diverge. Parent and branch sessions remain independent.

Token Usage Tracking

import type { SessionTokenUsage } from '@craft-agent/shared/sessions';

interface SessionTokenUsage {
  inputTokens: number;           // Total input tokens
  outputTokens: number;          // Total output tokens
  cacheCreationTokens?: number;  // Prompt caching (write)
  cacheReadTokens?: number;      // Prompt caching (read)
  totalCost?: number;            // USD cost estimate
}

// Access token usage
const session = loadSession(workspace.rootPath, sessionId);
console.log(`Input: ${session.tokenUsage.inputTokens}`);
console.log(`Output: ${session.tokenUsage.outputTokens}`);
console.log(`Cost: $${session.tokenUsage.totalCost?.toFixed(4)}`);

Usage Tracking

See UsageTracker for real-time token monitoring

Session Metadata

For efficient listing, use SessionMetadata without loading messages:
interface SessionMetadata {
  id: string;
  workspaceId: string;
  name?: string;
  createdAt: number;
  lastUsedAt: number;
  messageCount: number;
  preview?: string;        // Preview of first user message
  sdkSessionId?: string;
  isArchived?: boolean;
  isFlagged?: boolean;
  status?: SessionStatus;
  hidden?: boolean;        // Hidden from session list
}

Listing Sessions

import { listSessions } from '@craft-agent/shared/sessions';

// List all sessions (metadata only)
const allSessions = listSessions(workspace.rootPath);

// Filter active sessions
const activeSessions = allSessions.filter(s => !s.isArchived);

// Sort by last used
activeSessions.sort((a, b) => b.lastUsedAt - a.lastUsedAt);

// Most recent session
const latest = activeSessions[0];
console.log(`Latest: ${latest.name || latest.id}`);
console.log(`  ${latest.messageCount} messages`);
console.log(`  Preview: ${latest.preview}`);

Best Practices

Give sessions descriptive names for easy identification:
await updateSession(workspace.rootPath, session.id, {
  name: 'Refactor authentication system'
});
Good names:
  • “Fix login redirect bug”
  • “Add dark mode support”
  • “Database migration planning”
Bad names:
  • “Session 1”
  • “Test”
  • “New conversation”
Track session progress with statuses:
await updateSession(workspace.rootPath, session.id, {
  status: 'in_progress'
});
// Later...
await updateSession(workspace.rootPath, session.id, {
  status: 'needs_review'
});
Keep your session list clean:
await updateSession(workspace.rootPath, session.id, {
  isArchived: true,
  status: 'done'
});
Check token usage periodically:
const session = loadSession(workspace.rootPath, sessionId);
if (session.tokenUsage.inputTokens > 100000) {
  console.warn('Session approaching context limit');
}

Workspaces

Project-scoped configuration and storage

Agents

Agent backends and session management

Permissions

Session-scoped permission modes

Persistence Queue

Debounced session write implementation

Build docs developers (and LLMs) love