diff --git a/packages/coding-agent/src/features/subagent/lane-events.ts b/packages/coding-agent/src/features/subagent/lane-events.ts index 825439b..abc807f 100644 --- a/packages/coding-agent/src/features/subagent/lane-events.ts +++ b/packages/coding-agent/src/features/subagent/lane-events.ts @@ -18,6 +18,14 @@ export type BackgroundLaneEvent = | "background_progress" | "background_restarted"; +/** Events that start a parent turn when the parent is idle. */ +const WAKING_EVENTS: ReadonlySet = new Set([ + "background_done", + "background_failed", + "background_interrupted", + "background_needs_input", +]); + /** `details` on an `` message; read by its transcript renderer. */ export interface AgentNotificationDetails { agentId: string; @@ -92,7 +100,14 @@ export function notifyLaneEvent( display: event !== "background_progress", details, }, - { deliverAs: "steer" }, + // A lane usually settles while the parent sits idle, waiting on it. A + // steer alone only appends the message then, so the model never reads the + // result until the user types again. Terminal and needs-input events wake + // the parent; progress and restart notices ride along with the next turn + // instead of waking it every 15s per lane. They leave triggerTurn unset + // rather than false: an explicit false would defer them to the end of a + // running turn instead of steering into it. + WAKING_EVENTS.has(event) ? { deliverAs: "steer", triggerTurn: true } : { deliverAs: "steer" }, ); } diff --git a/packages/coding-agent/test/step-subagent-events.test.ts b/packages/coding-agent/test/step-subagent-events.test.ts index a1d5448..5e775fe 100644 --- a/packages/coding-agent/test/step-subagent-events.test.ts +++ b/packages/coding-agent/test/step-subagent-events.test.ts @@ -18,6 +18,7 @@ interface SentMessage { content: string; details?: { agentId?: string; event?: string; status?: string }; deliverAs?: string; + triggerTurn?: boolean; } function textResult(value: string): StepSubagentRunResult { @@ -65,8 +66,8 @@ function createApi(): { api: ExtensionAPI; tools: Map; s setActiveTools: () => {}, getFlag: () => false, appendEntry: () => {}, - sendMessage: (message: SentMessage, options?: { deliverAs?: string }) => { - sent.push({ ...message, deliverAs: options?.deliverAs }); + sendMessage: (message: SentMessage, options?: { deliverAs?: string; triggerTurn?: boolean }) => { + sent.push({ ...message, deliverAs: options?.deliverAs, triggerTurn: options?.triggerTurn }); }, sendUserMessage: () => {}, } as unknown as ExtensionAPI; @@ -164,6 +165,8 @@ test("background lane completion steers an escaped agent-notification", async () const done = sent[0]; expect(done.customType).toBe("agent-notification"); expect(done.deliverAs).toBe("steer"); + // The parent is usually idle while it waits on a lane; the result must wake it. + expect(done.triggerTurn).toBe(true); expect(done.details?.event).toBe("background_done"); expect(done.content).toContain('alias="audit" role="admin"'); expect(done.content).not.toContain('alias="audit" role="admin"'); @@ -474,3 +477,29 @@ test("agent_send validates its target and action inputs", async () => { expect(tools.has("agent_wait")).toBe(false); expect(tools.has("agent_list")).toBe(false); }); + +test("progress notifications ride along without waking an idle parent", async () => { + const { notifyLaneEvent } = await import("../src/features/subagent/lane-events.ts"); + const sent: Array<{ event?: string; options?: { deliverAs?: string; triggerTurn?: boolean } }> = []; + const pi = { + sendMessage: ( + message: { details?: { event?: string } }, + options?: { deliverAs?: string; triggerTurn?: boolean }, + ) => sent.push({ event: message.details?.event, options }), + } as unknown as ExtensionAPI; + const lane = { id: "lane", subscribe: "progress", status: "running", details: { results: [] } } as never; + + notifyLaneEvent(pi, lane, "background_progress", "step 1/1"); + notifyLaneEvent(pi, lane, "background_restarted"); + notifyLaneEvent(pi, lane, "background_needs_input", "which file?"); + notifyLaneEvent(pi, lane, "background_failed", "boom"); + + // Non-waking events must leave triggerTurn unset, not false: AgentSession + // defers an explicit false to the end of a running turn instead of steering. + expect(sent).toEqual([ + { event: "background_progress", options: { deliverAs: "steer" } }, + { event: "background_restarted", options: { deliverAs: "steer" } }, + { event: "background_needs_input", options: { deliverAs: "steer", triggerTurn: true } }, + { event: "background_failed", options: { deliverAs: "steer", triggerTurn: true } }, + ]); +});