Skip to main content
Loom Core docs

Agent Lifecycle

This document describes the canonical model loom-core uses to represent vendor CLI processes (Claude Code, Codex, Gemini CLI) as agents + sessions, how the lifecycle is wired for each vendor, and how gaps in vendor hook surfaces are compensated for.

The Model

Three concepts, owned by three different subsystems:

ConceptOwned byLifetimeSource of truth
Presencepkg/agentcontext / svc_presence.goEphemeral; TTL expires after missed heartbeats (3× TTL default)In-memory registry + Qdrant projection
Sessionpkg/agentcontext / svc_sessions_*.goDurable; explicit active → ended → summarized state transitionsQdrant CollSessions
Agent viewinternal/hud/fleetview / JoinDerived per HUD refresh from live presence + session recordsComputed, never stored

Presence is the liveness beacon. Session is the durable context. An "agent" in the HUD is the derived join of the two, and HasSession is always computed at read time, never persisted — see the design notes on MR !211.

Orphan state

A presence row with HasPresence=true && HasSession=false past a 120s grace window is flagged IsOrphan=true by fleetview.Join. The fleet monitor auto-deregisters orphans past 10 minutes. See MR !212.

Per-Vendor Lifecycle

Claude Code

Full lifecycle via native hook events:

Hook eventGenerated command (summary)Purpose
SessionStartloom agent session-start --auto-recall + keepalive daemonRegister presence + start session
PostToolUseloom agent heartbeat --ensure-sessionKeep presence alive, bootstrap session
Stoploom agent session-end --summarize --summary-asyncEnd session, deregister presence
SubagentStartCache parent-session-id for subagent groupingThread hierarchy

Wired in pkg/generator/configs_hooks.go via the platform profile claude — emits to ~/.claude/settings.json.

Gemini CLI

Full lifecycle via Gemini hook events. Event names differ from Claude's but the loom commands are identical:

Hook eventGenerated command (summary)Purpose
SessionStartloom agent session-start --auto-recall + keepaliveRegister presence + start session
AfterToolloom agent heartbeat --ensure-sessionKeep presence alive, bootstrap session
SessionEndloom agent session-end --summarize --summary-asyncEnd session, deregister presence

Gemini has no SubagentStart equivalent — closest are BeforeAgent / AfterAgent (per-turn, not per-subagent), so subagent hierarchy is claude-only for now. Emits to ~/.gemini/settings.json.

Codex CLI

No native lifecycle hook surface beyond notify, which fires on turn completion only — not on process start, not on process exit, not on tool use. See Codex config reference.

Loom wires Codex via a single notify = [...] entry in ~/.codex/config.toml that invokes a background loom agent keepalive-wrap process with --ensure-session, rate-limited to one launch per 15s. The keepalive daemon runs detached (via nohup) with a 12h max-lifetime and sends periodic heartbeats; when it exits (timeout, SIGTERM), it deregisters the presence.

EventGenerated behaviorGap vs Claude/Gemini
Process startCovered by notify on first turn (first meaningful signal we can hook) — slight delay vs SessionStart~1 turn late
Tool useNot hooked — notify only fires at turn boundaryNo per-tool heartbeat
Process exitNot hookednotify does not fire on exitSession-end never called

The "session-end never called" gap is why the fleet monitor's orphan reaper exists: Codex agents that terminate without a clean shutdown are detected via HasPresence && !HasSession past 120s, and their presence rows are force-deregistered after 10 minutes. See internal/hud/monitor/fleet.goreapOrphans.

Future: codex_hooks feature flag

OpenAI's Codex has an experimental codex_hooks feature (gated behind features.codex_hooks in the config) that adds session_start, stop, user_prompt_submit, pre_tool_use, post_tool_use, and permission_request events. Events exist in codex-rs/hooks/src/events/ but are not yet exposed via the public enum. When this goes GA, Codex should join the "native hooks" tier and the notify shim can be deprecated.

Cross-Vendor Contract

The TestVendorLifecycleContract test in pkg/generator/configs_hooks_test.go pins the contract for each vendor so a future generator refactor cannot silently drop a hook. The test is the executable version of this document.

Observability

  • Presence count by status — exposed on FleetSnapshot.{Active,Idle,Offline,Orphan}Agents.
  • Orphan attentioninternal/hud/coordination.Build attaches "orphan without session" to the agent's AttentionReasons, so the HUD attention list surfaces orphans automatically.
  • Reap audit trailfleet.go:reapOrphans logs "fleet: reaped orphan presence" with agent_id and reap_after_seconds on every successful deregister.
Agent Lifecycle | Loom Core docs