/**
* The hand-back review of a resident agent's turn, awaited so the verdict
* travels WITH the completion report instead of trailing it.
*
* A resident's spawning call has already returned when its turn ends, so there
* is no tool_result for the auto-mode gate to attach its holistic review to.
* Emitting the actions and sending the report immediately (the previous
* shape) let the "flagged" notice land one or more tool boundaries after the
* report — the model could act on an agent's output before hearing that auto
* mode had concerns about how it was produced. Claude Code gates the hand-back
* (findings §17.2). This module holds the wait: the payload carries an
* `onReview` callback the gate answers exactly once (a rendered flag, or
* undefined for clean / not in auto mode), and a timeout bounds the wait so a
* slow or absent reviewer degrades to "report plus a note", never to a report
* that waits forever.
*/
import { SUBAGENT_ACTIONS_CHANNEL, type ChildAction, type SubagentActionsPayload } from "../auto-mode/actions.ts";
/** How long a completion report waits for the gate's verdict before going out with a note instead. */
export const HAND_BACK_REVIEW_TIMEOUT_MS = 45_000;
/** The note that replaces a verdict that did not arrive in time. */
export const HAND_BACK_REVIEW_TIMEOUT_NOTE =
"\nAuto mode's review of this agent's actions did not finish before the report was delivered. Treat the output as unverified until you have checked it.\n";
/** The events slice this module needs. */
export interface ActionsEmitter {
emit(channel: string, data: unknown): void;
}
/**
* Emit a resident turn's actions for review and resolve with the gate's
* rendered flag (undefined when clean, no actions, or the gate is not in auto
* mode). Never rejects. The first answer wins; late or repeated answers are
* ignored.
*/
export function awaitHandBackReview(
events: ActionsEmitter,
run: { taskId: string; name: string },
actions: ChildAction[] | undefined,
timeoutMs = HAND_BACK_REVIEW_TIMEOUT_MS,
): Promise {
if (!actions?.length) return Promise.resolve(undefined);
return new Promise((resolve) => {
let settled = false;
const finish = (flag: string | undefined) => {
if (settled) return;
settled = true;
clearTimeout(timer);
resolve(flag);
};
const timer = setTimeout(() => finish(HAND_BACK_REVIEW_TIMEOUT_NOTE), timeoutMs);
timer.unref?.();
events.emit(SUBAGENT_ACTIONS_CHANNEL, {
toolCallId: run.taskId,
actions,
background: true,
agentName: run.name,
onReview: finish,
} satisfies SubagentActionsPayload);
});
}
/** Prepend a review flag to a notification body, when there is one. */
export function withReview(body: string, flag: string | undefined): string {
return flag ? `${flag}\n\n${body}` : body;
}