#13415: fix(hooks): bridge agent_end events to internal/workspace hooks
agents
size: S
trusted-contributor
experienced-contributor
Cluster:
Hook and Gateway Improvements
## Summary
Fixes #10778
When plugin hooks fire `agent_end` via `hookRunner.runAgentEnd()`, the event is not bridged to the internal/workspace hook system (`triggerInternalHook`). This means workspace hooks registered on `agent:end` never fire.
**Root cause**: `attempt.ts` only calls `hookRunner.runAgentEnd()` (plugin hooks) but never dispatches to `triggerInternalHook()` (internal hooks). The `agent:bootstrap` event already has this bridging via `bootstrap-hooks.ts`, but `agent:end` was missing the equivalent.
**Fix**:
- New `agent-end-hooks.ts` module (following the exact pattern of `bootstrap-hooks.ts`) that creates an `InternalHookEvent` with type `"agent"`, action `"end"`, and context containing messages, success, error, durationMs, agentId, and workspaceDir
- Call `triggerAgentEndHook()` from `attempt.ts` as fire-and-forget, outside the `hookRunner?.hasHooks("agent_end")` guard so internal hooks fire regardless of plugin hooks
## Test plan
- [x] 3 new tests in `agent-end-hooks.test.ts` (TDD: all 3 fail before, pass after)
- [x] Fires `agent:end` internal hook with correct context
- [x] Includes error in context when provided
- [x] Does not throw when no handlers are registered
- [x] All 18 existing `internal-hooks.test.ts` tests still pass
- [x] `pnpm build && pnpm check` passes clean
<!-- greptile_comment -->
<h2>Greptile Overview</h2>
<h3>Greptile Summary</h3>
This change adds a new internal hook bridge for the `agent:end` lifecycle event. A new `src/agents/agent-end-hooks.ts` helper builds an `InternalHookEvent` (`type: "agent"`, `action: "end"`) with a context containing the final message snapshot, success/error, duration, agentId, and workspaceDir, and then calls `triggerInternalHook`. `runEmbeddedAttempt` (`src/agents/pi-embedded-runner/run/attempt.ts`) now invokes this bridge after plugin `agent_end` hooks run, ensuring workspace/internal handlers registered on `agent:end` fire even when no plugins are present. New Vitest coverage (`src/agents/agent-end-hooks.test.ts`) validates emitted context, error inclusion, and no-op behavior when no handlers are registered.
<h3>Confidence Score: 5/5</h3>
- This PR is safe to merge with minimal risk.
- Changes are small and consistent with existing internal hook bridging patterns. The new fire-and-forget internal hook invocation properly attaches a `.catch()` to avoid unhandled promise rejections, and the new tests validate context wiring and no-handler behavior.
- No files require special attention
<!-- greptile_other_comments_section -->
<!-- /greptile_comment -->
Most Similar PRs
#16618: feat: bridge message lifecycle hooks to workspace hook system
by DarlingtonDeveloper · 2026-02-14
83.1%
#19565: feat: add agent lifecycle hook events (session, message, error)
by tag-assistant · 2026-02-17
81.1%
#10679: fix(hooks): invoke gateway_start and gateway_stop in lifecycle
by yassinebkr · 2026-02-06
78.6%
#7301: fix(hooks): use resolveAgentIdFromSessionKey instead of split(":")[0]
by tsukhani · 2026-02-02
78.5%
#7771: Hooks: wire lifecycle events and tests
by rabsef-bicrym · 2026-02-03
76.7%
#20580: feat(hooks): bridge after_tool_call to internal hook handler system
by CryptoKrad · 2026-02-19
76.5%
#20268: feat(hooks): emit subagent:complete internal hook event
by AytuncYildizli · 2026-02-18
76.4%
#8084: fix(plugins): wire up message_sending hook in outbound delivery
by lailoo · 2026-02-03
76.1%
#13861: feat(hooks): add session:compaction hook event
by lailoo · 2026-02-11
76.0%
#19422: fix: pass session context to plugin tool hooks in toToolDefinitions
by namabile · 2026-02-17
75.9%