feat(agents): agent platform — registry, activity hooks, harness allowlist#119
Conversation
31fdf0e to
da14c97
Compare
Greptile SummaryThis PR lands the agent platform foundation — a registry, an activity-dispatch table, and an
Confidence Score: 4/5Safe to merge with the understanding that Codex sessions killed while awaiting a permission prompt will be permanently stuck in waiting_input until the reaper gap is addressed. The reaper's sticky-state guard in backend/internal/adapters/agent/codex/activity.go and backend/internal/lifecycle/runtime.go — the former introduces waiting_input for Codex without a session-end hook, and the latter's hasRecentActivity/runtimeClearlyDead interaction prevents the reaper from ever cleaning up those sessions if the process dies unexpectedly. Important Files Changed
Sequence DiagramsequenceDiagram
participant Agent as Agent (Codex/Claude/opencode)
participant Hook as ao hooks CLI
participant Dispatch as activitydispatch
participant Daemon as Daemon HTTP
participant LCM as lifecycle.Manager
participant Reaper as Reaper
Agent->>Hook: runs hook command (stdin: payload)
Hook->>Hook: validate AO_SESSION_ID
Hook->>Dispatch: Derive(agent, event, payload)
Dispatch-->>Hook: (ActivityState, ok)
Hook->>Daemon: "POST /api/v1/sessions/{id}/activity"
Daemon->>LCM: ApplyActivitySignal(id, signal)
LCM-->>Daemon: ok
Note over Reaper,LCM: Background reaper cycle
Reaper->>LCM: ApplyRuntimeObservation(id, ProbeDead)
LCM->>LCM: runtimeClearlyDead?
Note right of LCM: waiting_input (sticky) → hasRecentActivity=true → runtimeClearlyDead=false → no cleanup
Reviews (2): Last reviewed commit: "feat(agents): agent platform — registry,..." | Re-trigger Greptile |
da14c97 to
ea78106
Compare
| // in the adapter, so runtime exit still falls back to the reaper. | ||
| // | ||
| // TODO(codex): ActivityExited is still runtime-observation-owned. If Codex adds | ||
| // a native session/process-end hook, map that hook to ActivityExited here. Until | ||
| // then, make sure the lifecycle reaper can still mark a dead Codex runtime as | ||
| // exited even when the last hook signal was sticky waiting_input. |
There was a problem hiding this comment.
Reaper cannot clean up sessions in
waiting_input state
The TODO here says "make sure the lifecycle reaper can still mark a dead Codex runtime as exited even when the last hook signal was sticky waiting_input" — but the current reaper logic actively prevents this. runtimeClearlyDead calls hasRecentActivity, and hasRecentActivity unconditionally returns true for any sticky state (waiting_input), regardless of when the last signal arrived or whether the process is dead. This means a Codex session that was waiting for a permission approval when the process crashed or was killed will be permanently stuck in waiting_input — the reaper will never fire ApplyRuntimeObservation for it.
The same window exists for Claude Code: a session whose last hook was a notification → waiting_input and then the process received SIGKILL (no SessionEnd emitted) will also be stuck forever. The SessionEnd hook mitigates the common paths for Claude Code, but Codex has no equivalent exit hook at all.
…wlist Introduces the shared platform that per-agent adapters plug into, wired for the three shipped harnesses (claude-code, codex, opencode): - adapters/agent/registry: single source of truth for shipped adapters (Constructors), consumed by the daemon to resolve a session's harness. - adapters/agent/activitydispatch + 'ao hooks' command: maps an agent's native hook callbacks onto AO activity states (active/idle/waiting/...). - claudecode/codex/opencode: emit SessionStart/UserPromptSubmit/Stop activity. - HTTP + OpenAPI: report session activity state. - db: single migration widening sessions.harness to all shipped harnesses, so adding an adapter needs no further migration. - domain: harness constants + --agent alias for 'ao spawn'. Adding a new agent is now one adapter package plus a line in Constructors(). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: Harshit Singh Bhandari <claudeagain@pkarnal.com>
ea78106 to
7e9f081
Compare
There was a problem hiding this comment.
yyovil has reached the 50-review limit for trial accounts. To continue receiving code reviews, upgrade your plan.
Agent platform (base of a stacked series)
This is the root PR of a stack. It lands the shared platform that every
per-agent adapter plugs into; each subsequent PR in the stack adds one agent
adapter on top of this.
Wired here for the three already-shipped harnesses —
claude-code,codex,opencode.What's in it
adapters/agent/registry— single source of truth for the shippedadapters (
Constructors()); the daemon resolves a session's harness throughit. Adding an agent becomes one line here.
adapters/agent/activitydispatch+ theao hookscommand — maps anagent's native hook callbacks onto AO activity states
(
active/idle/waiting_input/…).SessionStart/UserPromptSubmit/Stopactivity through the dispatcher.sessions.harnessto all shippedharnesses at once, replacing what was an 18-migration chain where each step
textually depended on the previous one's output (and silently no-op'd if
merged out of order). New adapters now need no further migration.
--agentalias forao spawn.Why a stack
The per-agent adapters are independent packages, but they all register through
the same handful of files (
Constructors(), the activity dispatcher, theharness allowlist). Landing the platform once, then stacking one adapter per PR,
keeps each adapter reviewable in isolation without a tangle of migration-order
and merge hazards.
Testing
go build/go vet/go test ./...green, except the pre-existingTestSessionStreamsRealZellijPaneintegration test, which fails only on theknown environmental zellij IPC-socket-path-too-long issue (long macOS
$TMPDIR)— unrelated to this change.
🤖 Generated with Claude Code
Stack