feat(hooks): add agent:turn:end internal hook event#60734
Conversation
Greptile SummaryAdds an Confidence Score: 5/5Safe to merge; all remaining findings are P2 style suggestions with no correctness impact. The change is additive, fire-and-forget so it cannot regress the turn lifecycle, and both call sites are correctly updated. Only P2 suggestions remain. No files require special attention. Prompt To Fix All With AIThis is a comment left during a code review.
Path: src/hooks/internal-hooks.ts
Line: 474-479
Comment:
**`hasNumberContextField` defined away from sibling helpers**
`hasStringContextField` and `hasBooleanContextField` are grouped together around line 350. Defining `hasNumberContextField` ~124 lines later, right before its single use site, breaks the pattern and makes it harder to discover when adding future event types. Move this block up to sit alongside the other two helpers.
How can I resolve this? If you propose a fix, please make it concise.
---
This is a comment left during a code review.
Path: src/hooks/internal-hooks.ts
Line: 463-466
Comment:
**`errorCode` not surfaced in hook context**
`AgentTurnEndHookContext` only carries `success: boolean`, so hook consumers can detect that a turn failed but cannot distinguish error codes (e.g. `ACP_TURN_FAILED` vs `ACP_SESSION_INIT_FAILED`). If any subscriber needs to branch on failure kind or log structured error info, consider adding an optional `errorCode` field while the shape is still easy to extend.
```suggestion
export type AgentTurnEndHookContext = {
success: boolean;
durationMs: number;
errorCode?: string;
};
```
How can I resolve this? If you propose a fix, please make it concise.Reviews (1): Last reviewed commit: "feat(hooks): add agent:turn:end internal..." | Re-trigger Greptile |
| function hasNumberContextField<T extends Record<string, unknown>>( | ||
| context: Partial<T>, | ||
| key: keyof T, | ||
| ): boolean { | ||
| return typeof context[key] === "number"; | ||
| } |
There was a problem hiding this comment.
hasNumberContextField defined away from sibling helpers
hasStringContextField and hasBooleanContextField are grouped together around line 350. Defining hasNumberContextField ~124 lines later, right before its single use site, breaks the pattern and makes it harder to discover when adding future event types. Move this block up to sit alongside the other two helpers.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/hooks/internal-hooks.ts
Line: 474-479
Comment:
**`hasNumberContextField` defined away from sibling helpers**
`hasStringContextField` and `hasBooleanContextField` are grouped together around line 350. Defining `hasNumberContextField` ~124 lines later, right before its single use site, breaks the pattern and makes it harder to discover when adding future event types. Move this block up to sit alongside the other two helpers.
How can I resolve this? If you propose a fix, please make it concise.Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
| export type AgentTurnEndHookContext = { | ||
| success: boolean; | ||
| durationMs: number; | ||
| }; |
There was a problem hiding this comment.
errorCode not surfaced in hook context
AgentTurnEndHookContext only carries success: boolean, so hook consumers can detect that a turn failed but cannot distinguish error codes (e.g. ACP_TURN_FAILED vs ACP_SESSION_INIT_FAILED). If any subscriber needs to branch on failure kind or log structured error info, consider adding an optional errorCode field while the shape is still easy to extend.
| export type AgentTurnEndHookContext = { | |
| success: boolean; | |
| durationMs: number; | |
| }; | |
| export type AgentTurnEndHookContext = { | |
| success: boolean; | |
| durationMs: number; | |
| errorCode?: string; | |
| }; |
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/hooks/internal-hooks.ts
Line: 463-466
Comment:
**`errorCode` not surfaced in hook context**
`AgentTurnEndHookContext` only carries `success: boolean`, so hook consumers can detect that a turn failed but cannot distinguish error codes (e.g. `ACP_TURN_FAILED` vs `ACP_SESSION_INIT_FAILED`). If any subscriber needs to branch on failure kind or log structured error info, consider adding an optional `errorCode` field while the shape is still easy to extend.
```suggestion
export type AgentTurnEndHookContext = {
success: boolean;
durationMs: number;
errorCode?: string;
};
```
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8fafab7f47
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| fireAndForgetHook( | ||
| triggerInternalHook( | ||
| createInternalHookEvent("agent", "turn:end", params.sessionKey, { |
There was a problem hiding this comment.
Defer hook dispatch off turn thread
recordTurnCompletion is intended to be fire-and-forget, but triggerInternalHook(...) is invoked immediately while building the argument to fireAndForgetHook. Because triggerInternalHook awaits handler(event), any hook implemented as a synchronous function will run inline before the promise is detached, adding user-visible latency to ACP turn completion (especially for CPU/file-heavy handlers). This makes agent:turn:end capable of blocking the turn lifecycle despite the fire-and-forget wrapper.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Thanks for the review. To clarify — the pattern here matches how message-related hooks are fired in deliver.ts and dispatch-from-config.ts (both use fireAndForgetHook(triggerInternalHook(...))), not all hooks in the codebase. bootstrap-hooks.ts and compaction-hooks.ts correctly use await triggerInternalHook where blocking is intentional.
The key point still holds: triggerInternalHook is async and returns a Promise immediately — the for...await loop inside only runs in subsequent microtasks. fireAndForgetHook detaches that promise, so no handler code can block the caller. No change needed.
- Move hasNumberContextField alongside sibling helpers (hasStringContextField, hasBooleanContextField) - Add optional errorCode field to AgentTurnEndHookContext so subscribers can distinguish failure kinds
Adds an
agent:turn:endinternal hook event that fires after each completed agent turn (both successful and failed).Changes:
src/hooks/internal-hooks.ts: AddAgentTurnEndHookContext,AgentTurnEndHookEventtypes,hasNumberContextFieldhelper, andisAgentTurnEndEventtype guardsrc/acp/control-plane/manager.core.ts: ExtendrecordTurnCompletionwithsessionKeyparam and fire hook viafireAndForgetHook— existing stats logic is preserved identicallyThe hook fires on both success and error paths. It is fire-and-forget so it cannot block the turn lifecycle.
Supersedes #39596 (closed due to messy rebase conflicts).