Skip to main content

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
intervalMs
number
default:"30000"
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.
stop(): void
Example:
lifecycleManager.stop();
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>
sessionId
string
required
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

StatusDescriptionTerminal?
spawningSession is being createdNo
workingAgent is actively workingNo
pr_openPR has been createdNo
ci_failedCI checks are failingNo
review_pendingWaiting for code reviewNo
changes_requestedReviewer requested changesNo
approvedPR approved but not yet mergeableNo
mergeablePR is ready to mergeNo
mergedPR has been mergedYes
needs_inputAgent is waiting for user inputNo
stuckAgent appears to be stuckNo
erroredSession encountered an errorYes
killedSession was terminatedYes
doneSession completed successfullyYes
terminatedSession was forcibly terminatedYes

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:
retries
number
Maximum number of retry attempts before escalating.
escalateAfter
number | string
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 TypeStatus TriggerPriority
session.spawnedspawninginfo
session.workingworkinginfo
session.exitedkilledurgent
session.stuckstuckurgent
session.needs_inputneeds_inputurgent
session.errorederroredurgent
Event TypeStatus TriggerPriority
pr.createdpr_openinfo
pr.mergedmergedaction
pr.closedN/Awarning
Event TypeStatus TriggerPriority
ci.failingci_failedwarning
ci.passingN/Ainfo
Event TypeStatus TriggerPriority
review.pendingreview_pendinginfo
review.approvedapprovedaction
review.changes_requestedchanges_requestedwarning
Event TypeStatus TriggerPriority
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:
urgent
EventPriority
Critical issues requiring immediate attention (stuck, needs_input, exited).
action
EventPriority
Actionable events (approved, ready to merge, merged).
warning
EventPriority
Issues that may need attention (CI failed, changes requested).
info
EventPriority
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

Build docs developers (and LLMs) love