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
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 } ` );
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
Detect empty response - SDK returns empty on missing thread
Extract recent messages - Get last 5-10 messages from session
Format as context - Prepend to next user message
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
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”
Use Statuses for Workflow
Track session progress with statuses: await updateSession ( workspace . rootPath , session . id , {
status: 'in_progress'
});
// Later...
await updateSession ( workspace . rootPath , session . id , {
status: 'needs_review'
});
Archive Completed Sessions
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