← Back to PRs

#20268: feat(hooks): emit subagent:complete internal hook event

by AytuncYildizli open 2026-02-18 18:27 View on GitHub →
docs agents size: L
## Summary Adds a `subagent:complete` internal hook event that fires when sub-agent sessions finish. This gives external hooks observability into sub-agent lifecycle — currently the announce flow has no hook integration, so external systems can't track, log, or retry deliveries. Refs: https://github.com/openclaw/openclaw/discussions/20205 ## Changes ### `src/hooks/internal-hooks.ts` - Added `"subagent"` to the `InternalHookEventType` union type ### `src/agents/subagent-registry.ts` - Added `hookEmittedRuns` Set for exactly-once delivery guard - Extracted `emitSubagentCompleteHook(entry)` helper that checks dedup + emits the hook - Both resolution paths (lifecycle listener + `agent.wait`) call the helper - Guard is cleared in `releaseSubagentRun()` and `resetSubagentRegistryForTests()` ### `src/agents/subagent-registry.hook-event.test.ts` (new) 7 tests covering: - Lifecycle path fires hook with correct payload - agent.wait path fires hook with correct payload - Dedup guard ensures exactly-once delivery when both paths resolve - Error outcomes include the error field - Error outcomes via lifecycle error events - Full payload shape validation (childSessionKey, runId, label, task, outcome, startedAt, endedAt, runtimeMs) - No hook fires for unregistered runIds ## Hook payload shape ```ts { type: "subagent", action: "complete", sessionKey: "<requester session key>", context: { childSessionKey: string, runId: string, label?: string, task: string, outcome: { status: "ok" | "error", error?: string }, startedAt: number, endedAt: number, runtimeMs: number } } ``` ## Example hook usage ```ts // hooks/subagent-logger/handler.ts import type { InternalHookEvent } from "../../hooks/internal-hooks.js"; export default async function handler(event: InternalHookEvent) { if (event.action === "complete") { const { runId, label, outcome, runtimeMs } = event.context; console.log(`[subagent:complete] ${label ?? runId} → ${outcome.status} (${runtimeMs}ms)`); } } ``` ## What's NOT included The initial branch had an `agent:complete` event in `agent-events.ts` that would fire for ALL agent runs (heartbeats, main sessions, etc). This was removed to keep the PR focused on the discussion proposal — subagent:complete only. The broader `agent:complete` can be a follow-up. --- > 🤖 This PR was prepared with AI assistance. <!-- greptile_comment --> <h3>Greptile Summary</h3> This PR adds a `subagent:complete` internal hook event that fires when sub-agent sessions finish, giving external hooks observability into sub-agent lifecycle. The implementation is clean: a new `emitSubagentCompleteHook` helper with a `hookEmittedRuns` dedup Set ensures exactly-once delivery across both the lifecycle listener and `agent.wait` resolution paths. The type system change in `internal-hooks.ts` is minimal and correct. - The dedup guard in `emitSubagentCompleteHook` is synchronous, which correctly prevents races in Node's single-threaded event loop - `hookEmittedRuns` entries are not cleaned up in `sweepSubagentRuns()` or `finalizeSubagentCleanup()` — the two paths that actually remove entries from `subagentRuns` — causing the Set to grow unboundedly in long-running processes - Test coverage is thorough (7 tests covering both resolution paths, dedup, error outcomes, payload shape, and unregistered runId filtering), though the tests don't mock `loadConfig` or the registry store, making them less isolated than sibling test files <h3>Confidence Score: 3/5</h3> - The core hook emission logic is correct but the dedup Set leaks memory in long-running processes because cleanup paths don't clear it. - Score of 3 reflects that the feature works correctly for its intended purpose and has good test coverage, but the `hookEmittedRuns` Set is never cleaned up through the normal subagent lifecycle (sweep and finalize-delete paths), which will cause slow memory growth in long-lived gateway processes. The fix is straightforward — add `hookEmittedRuns.delete(runId)` alongside existing `subagentRuns.delete(runId)` calls. - Pay close attention to `src/agents/subagent-registry.ts` — the `hookEmittedRuns` cleanup gap in `sweepSubagentRuns` and `finalizeSubagentCleanup`. <sub>Last reviewed commit: 018eb88</sub> <!-- greptile_other_comments_section --> <sub>(4/5) You can add custom instructions or style guidelines for the agent [here](https://app.greptile.com/review/github)!</sub> <!-- /greptile_comment -->

Most Similar PRs