Overview
The Session Manager orchestrates the Runtime, Agent, and Workspace plugins to create, manage, and destroy agent sessions. It handles the full session lifecycle from creation to cleanup.
Core responsibilities:
Spawn new sessions (create workspace → runtime → launch agent)
Restore crashed/killed sessions
List all sessions with live state enrichment
Kill sessions and clean up resources
Send messages to running sessions
Automatic cleanup of completed work
The Session Manager is stateless - all session data is stored in flat metadata files. It rebuilds session state on every operation by reading these files and polling plugins.
Architecture
import { createSessionManager } from '@composio/ao-core' ;
const sessionManager = createSessionManager ({
config: orchestratorConfig ,
registry: pluginRegistry ,
});
// Spawn a new session
const session = await sessionManager . spawn ({
projectId: 'my-app' ,
issueId: 'INT-1234' ,
prompt: 'Fix the login bug' ,
});
// List all sessions
const sessions = await sessionManager . list ();
// Send a message
await sessionManager . send ( 'my-app-1' , 'Please add unit tests' );
// Kill a session
await sessionManager . kill ( 'my-app-1' );
Methods
spawn
Create a new agent session.
spawn ( config : SessionSpawnConfig ): Promise < Session >
Project ID from agent-orchestrator.yaml (e.g., “my-app”).
Issue identifier (e.g., “INT-1234”, “#42”). Can be tracker issue or free-text.
Git branch name. If not specified, inferred from issue or session ID.
Initial prompt for the agent. Combined with issue context if available.
Override agent plugin for this session (e.g., “codex”, “claude-code”).
Returns:
The newly created session object with all metadata.
Example:
// Spawn with issue ID (fetches issue from tracker)
const session = await sessionManager . spawn ({
projectId: 'my-app' ,
issueId: 'INT-1234' ,
prompt: 'Add TypeScript types' ,
});
// Spawn with explicit branch
const session2 = await sessionManager . spawn ({
projectId: 'my-app' ,
branch: 'feature/new-dashboard' ,
prompt: 'Build the dashboard UI' ,
});
// Spawn with agent override
const session3 = await sessionManager . spawn ({
projectId: 'my-app' ,
agent: 'codex' , // Use Codex instead of default
prompt: 'Refactor the API' ,
});
Spawn Process
The spawn process follows these steps:
Validate project - Check that projectId exists in config
Validate issue - If issueId provided, fetch from tracker (fails fast on auth/network errors)
Reserve session ID - Atomically create unique ID (e.g., “my-app-1”)
Create workspace - Use Workspace plugin to create isolated environment
Generate prompt - Combine user prompt + issue context + project rules
Create runtime - Launch agent process in runtime environment
Write metadata - Persist session state to disk
Post-launch setup - Configure agent (optional plugin hook)
Send initial prompt - For agents with promptDelivery: "post-launch"
If any step fails after workspace creation, the Session Manager automatically cleans up all created resources (workspace, runtime, metadata) to prevent resource leaks.
Branch Name Resolution
Branch names are determined by priority:
if ( config . branch ) {
// 1. Explicit branch always wins
branch = config . branch ;
} else if ( config . issueId && tracker ) {
// 2. Let tracker generate branch name
branch = tracker . branchName ( config . issueId , project );
} else if ( config . issueId ) {
// 3. Generate from issue ID
branch = `feat/ ${ sanitize ( config . issueId ) } ` ;
} else {
// 4. Fallback to session ID
branch = `session/ ${ sessionId } ` ;
}
spawnOrchestrator
Create an orchestrator session that manages other sessions.
spawnOrchestrator ( config : OrchestratorSpawnConfig ): Promise < Session >
System prompt with orchestrator instructions. Automatically written to file to avoid shell truncation.
Returns:
The orchestrator session with role: "orchestrator" metadata.
Example:
import { readFileSync } from 'node:fs' ;
const systemPrompt = readFileSync ( './orchestrator-prompt.md' , 'utf-8' );
const orchestrator = await sessionManager . spawnOrchestrator ({
projectId: 'my-app' ,
systemPrompt ,
});
// Session ID: "my-app-orchestrator"
// Permissions: always "skip" (no permission prompts)
Orchestrator sessions run in the project directory (not a worktree) and have permissions set to “skip” so they can autonomously run ao CLI commands.
list
List all sessions, optionally filtered by project.
list ( projectId ?: string ): Promise < Session [] >
Filter to sessions from this project only. If omitted, returns all sessions.
Returns:
Array of session objects enriched with live runtime state and agent activity.
Example:
// List all sessions
const allSessions = await sessionManager . list ();
// List sessions for specific project
const appSessions = await sessionManager . list ( 'my-app' );
// Display session info
for ( const session of allSessions ) {
console . log ( ` ${ session . id } : ${ session . status } ( ${ session . activity ?? 'unknown' } )` );
if ( session . pr ) console . log ( ` PR: ${ session . pr . url } ` );
}
Session Enrichment
The list() method enriches sessions with live state:
// 1. Read metadata from disk
const raw = readMetadataRaw ( sessionsDir , sessionId );
const session = metadataToSession ( sessionId , raw );
// 2. Check runtime liveness (with 2s timeout per session)
if ( session . runtimeHandle ) {
const alive = await runtime . isAlive ( session . runtimeHandle );
if ( ! alive ) session . status = 'killed' ;
}
// 3. Detect agent activity (reads JSONL files)
const activity = await agent . getActivityState ( session );
if ( activity ) {
session . activity = activity . state ;
session . lastActivityAt = activity . timestamp ?? session . lastActivityAt ;
}
// 4. Enrich with agent session info (summary, cost)
const info = await agent . getSessionInfo ( session );
if ( info ) session . agentInfo = info ;
Enrichment includes subprocess calls (tmux/ps checks) which can be slow under load. Each session has a 2-second timeout. If enrichment times out, the session keeps its metadata values.
get
Get a single session by ID.
get ( sessionId : SessionId ): Promise < Session | null >
Session ID to retrieve (e.g., “my-app-1”).
Returns:
Session object if found, null otherwise.
Example:
const session = await sessionManager . get ( 'my-app-1' );
if ( ! session ) {
console . error ( 'Session not found' );
return ;
}
console . log ( `Status: ${ session . status } ` );
console . log ( `Branch: ${ session . branch } ` );
console . log ( `Workspace: ${ session . workspacePath } ` );
restore
Restore a crashed or killed session.
restore ( sessionId : SessionId ): Promise < Session >
Returns:
The restored session with updated restoredAt timestamp.
Throws:
SessionNotRestorableError - Session is not in a terminal state or is merged
WorkspaceMissingError - Workspace doesn’t exist and can’t be recreated
Example:
try {
const session = await sessionManager . restore ( 'my-app-1' );
console . log ( 'Session restored:' , session . id );
} catch ( err ) {
if ( err instanceof SessionNotRestorableError ) {
console . error ( 'Cannot restore:' , err . reason );
} else if ( err instanceof WorkspaceMissingError ) {
console . error ( 'Workspace missing:' , err . path );
}
}
Restore Process
Find metadata - Check active sessions, fall back to archive
Validate restorability - Must be terminal state (killed/errored) but not merged
Check workspace - Verify workspace exists or attempt recreation
Destroy old runtime - Kill any orphaned runtime processes
Get launch command - Try agent’s restore command, fall back to fresh launch
Create runtime - Launch agent with restored environment
Update metadata - Set status to “spawning”, record restoredAt
Restorable vs Non-Restorable Sessions
Restorable:
killed - Agent process exited
errored - Session encountered an error
done - Session completed
terminated - Forcibly terminated
cleanup - Cleaned up but can be restored
Non-restorable:
merged - PR already merged (cannot reopen)
working, pr_open, etc. - Session is still active
kill
Terminate a session and clean up all resources.
kill ( sessionId : SessionId ): Promise < void >
Example:
await sessionManager . kill ( 'my-app-1' );
console . log ( 'Session killed' );
Kill Process
Find session - Locate metadata across all projects
Destroy runtime - Stop agent process (tmux kill, docker stop, etc.)
Destroy workspace - Delete worktree or clone (unless it’s the project path)
Archive metadata - Move to archive/ subdirectory with timestamp
Workspaces that match the project path are NOT deleted (orchestrator sessions run in the main repo).
cleanup
Automatically clean up completed sessions.
cleanup ( projectId ?: string , options ?: { dryRun? : boolean }): Promise < CleanupResult >
Limit cleanup to this project.
Preview what would be cleaned up without actually killing sessions.
Returns:
interface CleanupResult {
killed : string []; // Sessions that were (or would be) killed
skipped : string []; // Sessions that were skipped
errors : Array <{ // Sessions that failed to clean up
sessionId : string ;
error : string ;
}>;
}
Example:
// Dry run to see what would be cleaned
const preview = await sessionManager . cleanup ( undefined , { dryRun: true });
console . log ( 'Would kill:' , preview . killed );
console . log ( 'Would skip:' , preview . skipped );
// Actually clean up
const result = await sessionManager . cleanup ();
console . log ( 'Killed:' , result . killed );
if ( result . errors . length > 0 ) {
console . error ( 'Errors:' , result . errors );
}
Cleanup Criteria
Sessions are killed if ANY of these conditions are met:
PR is merged or closed - Work is complete
Issue is completed - Work is done
Runtime is dead - Agent process no longer running
Never cleaned up:
Orchestrator sessions (checked by role: "orchestrator" or ID ending in “-orchestrator”)
Sessions where checks fail (can’t verify state)
send
Send a message to a running session.
send ( sessionId : SessionId , message : string ): Promise < void >
Session ID to send message to.
Message text to send to the agent.
Example:
await sessionManager . send ( 'my-app-1' ,
'CI is failing. Run `gh pr checks` to see failures and fix them.'
);
How it works:
// 1. Find session metadata
const raw = readMetadataRaw ( sessionsDir , sessionId );
// 2. Build runtime handle (stored or fabricated)
const handle = raw . runtimeHandle
? JSON . parse ( raw . runtimeHandle )
: { id: sessionId , runtimeName: config . defaults . runtime };
// 3. Send via runtime plugin
const runtime = registry . get < Runtime >( 'runtime' , handle . runtimeName );
await runtime . sendMessage ( handle , message );
For tmux runtime, this uses tmux send-keys to type the message. For other runtimes, it may use stdin, API calls, or other mechanisms.
Session Object
The Session interface represents a running agent session:
interface Session {
id : SessionId ; // Unique ID (e.g., "my-app-1")
projectId : string ; // Config key (e.g., "my-app")
status : SessionStatus ; // Current lifecycle status
activity : ActivityState | null ; // Agent activity (active, ready, idle, etc.)
branch : string | null ; // Git branch name
issueId : string | null ; // Issue identifier
pr : PRInfo | null ; // PR metadata if PR exists
workspacePath : string | null ; // Path to workspace on disk
runtimeHandle : RuntimeHandle | null ; // Runtime-specific handle
agentInfo : AgentSessionInfo | null ; // Agent summary, cost, etc.
createdAt : Date ; // When session was created
lastActivityAt : Date ; // Last detected activity
restoredAt ?: Date ; // When session was restored (if ever)
metadata : Record < string , string >; // Raw metadata key-value pairs
}
Activity States
Agent is processing (thinking, writing code).
Agent finished its turn, alive and waiting for input.
Agent has been inactive for a while (stale).
Agent is asking a question / permission prompt.
Agent hit an error or is stuck.
Agent process is no longer running.
Sessions are stored as flat key=value files:
File path:
~/.agent-orchestrator/{hash}-{projectId}/sessions/{sessionId}
Example file (my-app-1):
project =my-app
worktree =/Users/foo/.agent-orchestrator/a3b4c5d6e7f8-my-app/worktrees/my-app-1
branch =feat/INT-1234
status =working
tmuxName =a3b4c5d6e7f8-my-app-1
pr =https://github.com/org/repo/pull/42
issue =INT-1234
summary =Fix login authentication bug
agent =claude-code
createdAt =2026-03-04T10:30:00.000Z
runtimeHandle ={ "id" : "a3b4c5d6e7f8-my-app-1" , "runtimeName" : "tmux" }
Metadata files use user-facing session names (“my-app-1”), not tmux session names. The tmuxName field maps to the globally unique tmux session.
Error Handling
Resource Cleanup on Failure
The Session Manager automatically cleans up resources when spawn fails:
try {
const handle = await runtime . create ( ... );
} catch ( err ) {
// Clean up workspace
if ( workspace && workspacePath !== project . path ) {
await workspace . destroy ( workspacePath );
}
// Clean up reserved session ID
deleteMetadata ( sessionsDir , sessionId , false );
throw err ;
}
Issue Validation
Issue validation fails fast to prevent creating resources for invalid issues:
if ( config . issueId && tracker ) {
try {
await tracker . getIssue ( config . issueId , project );
} catch ( err ) {
if ( isIssueNotFoundError ( err )) {
// Ad-hoc issue string - proceed without tracker
} else {
// Auth/network error - fail fast
throw new Error ( `Failed to fetch issue: ${ err } ` );
}
}
}
Graceful Degradation
Non-critical failures don’t stop the session:
// Post-launch prompt delivery failure is non-fatal
if ( agent . promptDelivery === 'post-launch' && prompt ) {
try {
await runtime . sendMessage ( handle , prompt );
} catch {
// Agent is running but didn't get the prompt
// User can retry with `ao send`
}
}
Complete Example
import { createSessionManager } from '@composio/ao-core' ;
import { loadConfig } from '@composio/ao-core' ;
import { createPluginRegistry } from '@composio/ao-core' ;
// Setup
const config = loadConfig ();
const registry = createPluginRegistry ();
await registry . loadBuiltins ( config );
const sessionManager = createSessionManager ({
config ,
registry ,
});
// Spawn a new session
const session = await sessionManager . spawn ({
projectId: 'my-app' ,
issueId: 'INT-1234' ,
prompt: 'Add TypeScript types to the API' ,
});
console . log ( 'Created session:' , session . id );
// List all sessions
const sessions = await sessionManager . list ();
for ( const s of sessions ) {
console . log ( ` ${ s . id } : ${ s . status } ( ${ s . activity } )` );
}
// Send a message
await sessionManager . send ( session . id ,
'Please add unit tests for the new types'
);
// Wait for work to complete...
// Clean up finished sessions
const cleanup = await sessionManager . cleanup ();
console . log ( 'Cleaned up:' , cleanup . killed );
// Restore if needed
if ( cleanup . killed . includes ( session . id )) {
const restored = await sessionManager . restore ( session . id );
console . log ( 'Restored:' , restored . id );
}
See Also