summaryrefslogtreecommitdiff
path: root/pi/agent/extensions/loop-scheduler/index.ts
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-05-25 10:09:11 +0300
committerPaul Buetow <paul@buetow.org>2026-05-25 10:09:11 +0300
commit106f3f414802e34f2f58bdc4d9c2c92c845bab15 (patch)
tree0df579d06e2d26dba3dc37b1ee6ccf96833045bd /pi/agent/extensions/loop-scheduler/index.ts
parentb1bcf57124b810b629ecb33ff651b619ff8d7178 (diff)
fix(loop-scheduler): await waitForIdle in agent_end before draining
Inside an agent_end listener, agent.state.isStreaming is still true — finishRun() only clears it in the finally block of runWithLifecycle, after all agent_end listeners settle. So when we dispatched a pending job from agent_end and called pi.sendUserMessage(..., { deliverAs: 'followUp' }), the message was routed into agent.followUpQueue. The agent loop had already passed its getFollowUpMessages() check, so it exited without draining the queue. The message sat there as a stuck 'Follow-up: ...' in pi's UI, agentBusy stayed true forever, and every subsequent pending loop was blocked because no further agent_end fired. Await ctx.waitForIdle() in the agent_end handler before resetting agentBusy and calling drainPendingJobs. By then finishRun() has cleared isStreaming, so sendUserMessage starts a fresh run instead of enqueueing into a dead followUp queue, and pending loops drain serially as designed. Amp-Thread-ID: https://ampcode.com/threads/T-019e5de9-a0c3-7559-9cf0-f81ce751e763 Co-authored-by: Amp <amp@ampcode.com>
Diffstat (limited to 'pi/agent/extensions/loop-scheduler/index.ts')
-rw-r--r--pi/agent/extensions/loop-scheduler/index.ts13
1 files changed, 12 insertions, 1 deletions
diff --git a/pi/agent/extensions/loop-scheduler/index.ts b/pi/agent/extensions/loop-scheduler/index.ts
index cb5398b..67247b8 100644
--- a/pi/agent/extensions/loop-scheduler/index.ts
+++ b/pi/agent/extensions/loop-scheduler/index.ts
@@ -1196,11 +1196,22 @@ export default function loopSchedulerExtension(pi: ExtensionAPI): void {
pi.on("agent_end", async (_event, ctx) => {
rememberContext(ctx);
- agentBusy = false;
currentAssistantText = "";
currentIdleGeneration += 1;
queueIdleWatchJobs();
updateUi(ctx);
+ // CRITICAL: inside agent_end the agent's isStreaming flag is STILL true
+ // (finishRun() runs in the finally block after all listeners settle, see
+ // pi-coding-agent/packages/agent/src/agent.ts). If we dispatch here and
+ // call pi.sendUserMessage(..., { deliverAs: "followUp" }) right now, the
+ // message gets routed into agent.followUpQueue. But the agent loop has
+ // already passed its getFollowUpMessages() check — it will exit without
+ // draining the queue and our message sits there forever, visible as a
+ // stuck "Follow-up: ..." in pi's UI. We'd also leak agentBusy=true and
+ // block every subsequent pending job because no further agent_end fires.
+ // Wait for the run to actually finish before draining.
+ await ctx.waitForIdle();
+ agentBusy = false;
drainPendingJobs();
});