Overview
The Lifecycle Manager is the core orchestration engine that polls sessions, detects state transitions, emits events, and triggers automated reactions. It implements a state machine that tracks sessions through their entire lifecycle from spawning to completion.
Key responsibilities:
Periodically poll all active sessions
Detect state transitions (spawning → working → pr_open → etc.)
Emit events on transitions
Execute automated reactions (auto-fix CI failures, review comments, etc.)
Escalate to human notification when auto-handling fails
The Lifecycle Manager runs as a background polling loop, typically checking sessions every 30 seconds. It’s the primary automation layer between agents and humans.
Architecture
import { createLifecycleManager } from '@composio/ao-core' ;
const lifecycleManager = createLifecycleManager ({
config: orchestratorConfig ,
registry: pluginRegistry ,
sessionManager: sessionManager ,
});
// Start polling loop (default: 30s interval)
lifecycleManager . start ();
// Force check a specific session
await lifecycleManager . check ( 'my-app-1' );
// Stop polling
lifecycleManager . stop ();
Methods
start
Start the lifecycle polling loop.
start ( intervalMs ?: number ): void
Polling interval in milliseconds. Default is 30 seconds.
Example:
// Start with default 30s interval
lifecycleManager . start ();
// Start with custom 60s interval
lifecycleManager . start ( 60_000 );
The polling loop includes a re-entrancy guard - if a previous poll is still running, subsequent polls are skipped until it completes.
stop
Stop the lifecycle polling loop.
Example:
Always call stop() before shutting down the orchestrator to clean up the polling interval.
check
Force an immediate state check for a specific session, bypassing the polling schedule.
check ( sessionId : SessionId ): Promise < void >
The session ID to check (e.g., “my-app-1”).
Example:
// Manually trigger a state check
await lifecycleManager . check ( 'my-app-1' );
Use cases:
After manually updating session metadata
When you need immediate reaction to state changes
For testing and debugging
getStates
Get a snapshot of all tracked session states.
getStates (): Map < SessionId , SessionStatus >
return
Map<SessionId, SessionStatus>
Map of session IDs to their current status.
Example:
const states = lifecycleManager . getStates ();
for ( const [ sessionId , status ] of states ) {
console . log ( ` ${ sessionId } : ${ status } ` );
}
State Machine
The lifecycle manager implements a state machine with the following transitions:
Session Statuses
Status Description Terminal? spawningSession is being created No workingAgent is actively working No pr_openPR has been created No ci_failedCI checks are failing No review_pendingWaiting for code review No changes_requestedReviewer requested changes No approvedPR approved but not yet mergeable No mergeablePR is ready to merge No mergedPR has been merged Yes needs_inputAgent is waiting for user input No stuckAgent appears to be stuck No erroredSession encountered an error Yes killedSession was terminated Yes doneSession completed successfully Yes terminatedSession was forcibly terminated Yes
Status Detection
The lifecycle manager determines session status by polling multiple sources:
1. Runtime Liveness
const runtime = registry . get < Runtime >( 'runtime' , project . runtime );
const alive = await runtime . isAlive ( session . runtimeHandle );
if ( ! alive ) return 'killed' ;
2. Agent Activity
Prefers JSONL-based detection (reads agent’s session files directly):
const activityState = await agent . getActivityState ( session , config . readyThresholdMs );
if ( activityState . state === 'waiting_input' ) return 'needs_input' ;
if ( activityState . state === 'exited' ) return 'killed' ;
Falls back to terminal output parsing if JSONL is unavailable.
3. PR Detection
const detectedPR = await scm . detectPR ( session , project );
if ( detectedPR ) {
session . pr = detectedPR ;
// Persist to metadata
}
4. CI Status
const ciStatus = await scm . getCISummary ( session . pr );
if ( ciStatus === 'failing' ) return 'ci_failed' ;
5. Review State
const reviewDecision = await scm . getReviewDecision ( session . pr );
if ( reviewDecision === 'changes_requested' ) return 'changes_requested' ;
if ( reviewDecision === 'approved' ) {
const mergeReady = await scm . getMergeability ( session . pr );
return mergeReady . mergeable ? 'mergeable' : 'approved' ;
}
Reaction System
Reactions are automated responses to state transitions. They can send messages to agents, notify humans, or trigger actions like auto-merge.
Reaction Configuration
# agent-orchestrator.yaml
reactions :
ci-failed :
auto : true
action : send-to-agent
message : "CI is failing. Run `gh pr checks`, fix issues, and push."
retries : 2
escalateAfter : 2
approved-and-green :
auto : false
action : notify
priority : action
message : "PR is ready to merge"
Reaction Actions
send-to-agent Sends a message to the agent runtime: await sessionManager . send ( sessionId , reactionConfig . message );
Retries on failure (configurable via retries)
Escalates to human after max retries or timeout
Non-blocking (session continues running)
notify Sends a push notification to configured notifiers: const event = createEvent ( 'reaction.triggered' , {
sessionId ,
projectId ,
message: `Reaction ' ${ reactionKey } ' triggered notification` ,
});
await notifyHuman ( event , reactionConfig . priority ?? 'info' );
auto-merge Triggers automatic PR merge: // Triggers SCM plugin to merge the PR
// Currently posts notification; actual merge logic in SCM plugin
Escalation
Reactions can escalate to human notification after:
Maximum number of retry attempts before escalating.
Escalate after N attempts (number) or duration (string like “30m”).
Example:
reactions :
ci-failed :
retries : 2 # Try twice
escalateAfter : 2 # Then escalate
changes-requested :
escalateAfter : "30m" # Escalate after 30 minutes
Reaction Tracking
The lifecycle manager tracks attempts per session:
interface ReactionTracker {
attempts : number ;
firstTriggered : Date ;
}
// Tracked as "sessionId:reactionKey"
const trackerKey = ` ${ sessionId } :ci-failed` ;
Reset when session transitions to a new state
Persists across polling cycles
Cleaned up when session is killed/merged
Events
The lifecycle manager emits events on state transitions. Events are routed to notifiers based on priority.
Event Types
Event Type Status Trigger Priority session.spawnedspawninginfo session.workingworkinginfo session.exitedkilledurgent session.stuckstuckurgent session.needs_inputneeds_inputurgent session.errorederroredurgent
Event Type Status Trigger Priority pr.createdpr_openinfo pr.mergedmergedaction pr.closedN/A warning
Event Type Status Trigger Priority ci.failingci_failedwarning ci.passingN/A info
Event Type Status Trigger Priority review.pendingreview_pendinginfo review.approvedapprovedaction review.changes_requestedchanges_requestedwarning
Event Type Status Trigger Priority merge.readymergeableaction merge.completedmergedaction
Event Structure
interface OrchestratorEvent {
id : string ; // UUID
type : EventType ; // e.g., "ci.failing"
priority : EventPriority ; // urgent | action | warning | info
sessionId : SessionId ;
projectId : string ;
timestamp : Date ;
message : string ; // Human-readable description
data : Record < string , unknown >; // Event-specific data
}
Example:
{
id : "a3b4c5d6-e7f8-1234-5678-9abcdef01234" ,
type : "ci.failing" ,
priority : "warning" ,
sessionId : "my-app-1" ,
projectId : "my-app" ,
timestamp : new Date ( "2026-03-04T10:30:00Z" ),
message : "my-app-1: pr_open → ci_failed" ,
data : { oldStatus : "pr_open" , newStatus : "ci_failed" }
}
Notification Routing
Events are routed to notifiers based on priority:
# agent-orchestrator.yaml
notificationRouting :
urgent : [ "desktop" , "composio" ]
action : [ "desktop" , "composio" ]
warning : [ "composio" ]
info : [ "composio" ]
Priority levels:
Critical issues requiring immediate attention (stuck, needs_input, exited).
Actionable events (approved, ready to merge, merged).
Issues that may need attention (CI failed, changes requested).
Informational updates (session spawned, working).
Error Handling
Graceful Degradation
The lifecycle manager continues operating even when individual checks fail:
try {
const activityState = await agent . getActivityState ( session );
} catch {
// Preserve current stuck/needs_input state rather than coercing to "working"
if ( session . status === 'stuck' || session . status === 'needs_input' ) {
return session . status ;
}
}
Re-entrancy Guard
Prevents overlapping poll cycles:
let polling = false ;
async function pollAll () {
if ( polling ) return ; // Skip if previous poll still running
polling = true ;
try {
// ... poll all sessions
} finally {
polling = false ;
}
}
State Cleanup
Prunes stale entries when sessions are deleted:
// Remove from state map
for ( const trackedId of states . keys ()) {
if ( ! currentSessionIds . has ( trackedId )) {
states . delete ( trackedId );
}
}
// Remove from reaction trackers
for ( const trackerKey of reactionTrackers . keys ()) {
const sessionId = trackerKey . split ( ':' )[ 0 ];
if ( ! currentSessionIds . has ( sessionId )) {
reactionTrackers . delete ( trackerKey );
}
}
Complete Example
import { createLifecycleManager } from '@composio/ao-core' ;
// Create dependencies
const config = loadConfig ();
const registry = createPluginRegistry ();
await registry . loadBuiltins ( config );
const sessionManager = createSessionManager ({ config , registry });
// Create lifecycle manager
const lifecycleManager = createLifecycleManager ({
config ,
registry ,
sessionManager ,
});
// Start with 15s interval for more responsive automation
lifecycleManager . start ( 15_000 );
// Manually check a session after updating metadata
await sessionManager . send ( 'my-app-1' , 'Please fix the type errors' );
await lifecycleManager . check ( 'my-app-1' );
// Get current state
const states = lifecycleManager . getStates ();
console . log ( 'Active sessions:' , Array . from ( states . entries ()));
// Cleanup on shutdown
process . on ( 'SIGINT' , () => {
lifecycleManager . stop ();
process . exit ( 0 );
});
See Also