fix(coding-agent): drop stale goal continuation tick while streaming
Reviewer flagged a race left open by the streaming guard added in #2455: getUserInput() arms onInputCallback and schedules an 800 ms goal continuation timer; when /goal set takes the streaming branch (or any extension/hook starts a turn inside that window), the timer fired unchecked. The downstream onInputCallback resolved the main waiter with a goal-continuation submission, submitInteractiveInput called session.promptCustomMessage without a streamingBehavior, and the same AgentBusyError the PR set out to fix resurfaced. Make the continuation timer streaming-aware: at fire time, bail out when session.isStreaming || isCompacting || hasPostPromptWork is true. Reuses the auto-submit busy check loop mode already relies on (renamed #isLoopAutoSubmitBlocked -> #isAutoSubmitBlocked since both flows have the same notion of 'agent is busy, do not submit'). The next agent_end in #handleGoalSessionEvent reschedules normally. Fixes #2454
This commit is contained in:
@@ -225,6 +225,30 @@ describe("InteractiveMode goal mode integration", () => {
|
||||
await waiter.inputPromise;
|
||||
});
|
||||
|
||||
it("drops a goal continuation tick while the agent is streaming", async () => {
|
||||
// Repro for the race the streaming guard on /goal set X exposed: the
|
||||
// 800ms continuation timer armed by getUserInput() can outlive the idle
|
||||
// window when streaming starts between schedule and fire (e.g. /goal set
|
||||
// taking the streaming branch, or any extension that triggers a turn).
|
||||
// Without the streaming-aware guard the timer fires onInputCallback
|
||||
// with a `goal-continuation` and submitInteractiveInput resurfaces
|
||||
// AgentBusyError via promptCustomMessage.
|
||||
await harness.mode.handleGoalModeCommand("Ship the release");
|
||||
const waiter = await armInputWaiter(harness.mode);
|
||||
|
||||
let streaming = true;
|
||||
Object.defineProperty(harness.session, "isStreaming", { configurable: true, get: () => streaming });
|
||||
|
||||
// Let the 800ms timer fire while streaming is true.
|
||||
await Bun.sleep(900);
|
||||
|
||||
expect(waiter.getResolvedText()).toBeUndefined();
|
||||
|
||||
streaming = false;
|
||||
harness.mode.onInputCallback?.(harness.mode.startPendingSubmission({ text: "cleanup" }));
|
||||
await waiter.inputPromise;
|
||||
});
|
||||
|
||||
it("refuses /goal while plan mode is active", async () => {
|
||||
const showWarning = vi.spyOn(harness.mode, "showWarning");
|
||||
harness.mode.planModeEnabled = true;
|
||||
|
||||
Reference in New Issue
Block a user