clear awaitingAnswer in the input handler, not just on agent_start. had a bug where a master agent's reply, sent while the subagent was still mid-turn, was being consumed as a steer => subagent was left open waiting indefinitely. fixed now
This commit is contained in:
@@ -2,7 +2,7 @@
|
|||||||
name: researcher
|
name: researcher
|
||||||
description: Web researcher — searches the web and synthesizes findings
|
description: Web researcher — searches the web and synthesizes findings
|
||||||
tools: web_search, web_fetch, safe_bash
|
tools: web_search, web_fetch, safe_bash
|
||||||
model: openrouter/qwen/qwen3.8-max
|
model: openrouter/qwen/qwen3.8-2.4t-a95b
|
||||||
thinking: medium
|
thinking: medium
|
||||||
system-prompt: append
|
system-prompt: append
|
||||||
auto-exit: true
|
auto-exit: true
|
||||||
|
|||||||
+2
-2
@@ -3,8 +3,8 @@ name: worker
|
|||||||
description: General-purpose worker — reads, writes, and edits code
|
description: General-purpose worker — reads, writes, and edits code
|
||||||
tools: read, write, edit, safe_bash, web_search, web_fetch
|
tools: read, write, edit, safe_bash, web_search, web_fetch
|
||||||
subagent_agents: scout, researcher
|
subagent_agents: scout, researcher
|
||||||
model: openrouter/z-ai/glm-5.2
|
model: openrouter/qwen/qwen3.8-2.4t-a95b
|
||||||
thinking: medium
|
thinking: high
|
||||||
system-prompt: append
|
system-prompt: append
|
||||||
auto-exit: true
|
auto-exit: true
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -178,9 +178,10 @@ export default function (pi: ExtensionAPI) {
|
|||||||
|
|
||||||
let userTookOver = false;
|
let userTookOver = false;
|
||||||
let agentStarted = false;
|
let agentStarted = false;
|
||||||
// Set when ask_question is called; suppresses auto-exit for that turn so the
|
// Set when ask_question is called; suppresses auto-exit so the session stays
|
||||||
// session stays open while it waits for the orchestrator's reply. Cleared at
|
// open while it waits for the orchestrator's reply. Cleared when the reply
|
||||||
// the start of the next turn (when the reply lands).
|
// lands — on `input` (covers a reply steered into the current run) and on
|
||||||
|
// `agent_start` (covers a reply that starts a fresh turn after parking).
|
||||||
let awaitingAnswer = false;
|
let awaitingAnswer = false;
|
||||||
|
|
||||||
// Show widget + status bar on session start
|
// Show widget + status bar on session start
|
||||||
@@ -195,6 +196,15 @@ export default function (pi: ExtensionAPI) {
|
|||||||
|
|
||||||
pi.on("input", () => {
|
pi.on("input", () => {
|
||||||
recorder.input();
|
recorder.input();
|
||||||
|
// A submitted message is the orchestrator's (or a human's) reply — the
|
||||||
|
// pending ask_question has been answered, however it was delivered. Clear
|
||||||
|
// here, not only on agent_start, because a reply steered in *mid-run* is
|
||||||
|
// absorbed into the current run (pi's `steer` behavior injects it before
|
||||||
|
// the next LLM call): no new agent_start fires, so without this the flag
|
||||||
|
// would stay set and agent_end would park the session as `waiting` even
|
||||||
|
// though the answer already arrived and was consumed. (The `input` event
|
||||||
|
// fires for mid-run steers because prompt() emits it before queueing.)
|
||||||
|
awaitingAnswer = false;
|
||||||
// Ignore the initial task message that starts an autonomous subagent.
|
// Ignore the initial task message that starts an autonomous subagent.
|
||||||
// Only inputs after the first agent run has started count as user takeover.
|
// Only inputs after the first agent run has started count as user takeover.
|
||||||
if (!shouldMarkUserTookOver(agentStarted)) return;
|
if (!shouldMarkUserTookOver(agentStarted)) return;
|
||||||
|
|||||||
@@ -1656,6 +1656,101 @@ describe("subagent-done.ts", () => {
|
|||||||
rmSync(dir, { recursive: true, force: true });
|
rmSync(dir, { recursive: true, force: true });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Regression tests for the mid-run reply race: a reply steered in while the
|
||||||
|
// asking run is still open fires `input` but NOT `agent_start`, so the flag
|
||||||
|
// must be cleared on `input` or the session parks forever.
|
||||||
|
function setupCapturingExtension(sessionFile: string) {
|
||||||
|
const handlers = new Map<string, Array<(...args: any[]) => void>>();
|
||||||
|
const tools: any[] = [];
|
||||||
|
const api = {
|
||||||
|
on(event: string, handler: (...args: any[]) => void) {
|
||||||
|
if (!handlers.has(event)) handlers.set(event, []);
|
||||||
|
handlers.get(event)!.push(handler);
|
||||||
|
},
|
||||||
|
registerTool(t: any) { tools.push(t); },
|
||||||
|
registerCommand() {}, registerMessageRenderer() {}, registerShortcut() {},
|
||||||
|
sendUserMessage() {}, sendMessage() {}, getAllTools() { return []; },
|
||||||
|
} as any;
|
||||||
|
const saved = {
|
||||||
|
session: process.env.PI_SUBAGENT_SESSION,
|
||||||
|
name: process.env.PI_SUBAGENT_NAME,
|
||||||
|
agent: process.env.PI_SUBAGENT_AGENT,
|
||||||
|
autoExit: process.env.PI_SUBAGENT_AUTO_EXIT,
|
||||||
|
};
|
||||||
|
process.env.PI_SUBAGENT_SESSION = sessionFile;
|
||||||
|
process.env.PI_SUBAGENT_NAME = "scout-2";
|
||||||
|
process.env.PI_SUBAGENT_AGENT = "scout";
|
||||||
|
process.env.PI_SUBAGENT_AUTO_EXIT = "1";
|
||||||
|
subagentDoneExtension(api);
|
||||||
|
const emit = (event: string, ...args: any[]) =>
|
||||||
|
(handlers.get(event) ?? []).forEach((h) => h(...args));
|
||||||
|
const restore = () => {
|
||||||
|
restoreEnvVar("PI_SUBAGENT_SESSION", saved.session);
|
||||||
|
restoreEnvVar("PI_SUBAGENT_NAME", saved.name);
|
||||||
|
restoreEnvVar("PI_SUBAGENT_AGENT", saved.agent);
|
||||||
|
restoreEnvVar("PI_SUBAGENT_AUTO_EXIT", saved.autoExit);
|
||||||
|
};
|
||||||
|
const ask = async () => {
|
||||||
|
const tool = tools.find((t) => t.name === "ask_question");
|
||||||
|
await tool.execute("c1", { question: "v1 or v2?" }, undefined, undefined, { shutdown() {} });
|
||||||
|
};
|
||||||
|
return { emit, ask, restore };
|
||||||
|
}
|
||||||
|
|
||||||
|
it("exits (does not park) when the reply arrives mid-run via input", async () => {
|
||||||
|
const dir = createTestDir();
|
||||||
|
const { emit, ask, restore } = setupCapturingExtension(join(dir, "s.jsonl"));
|
||||||
|
try {
|
||||||
|
emit("agent_start");
|
||||||
|
await ask(); // sets awaitingAnswer mid-run
|
||||||
|
// Reply arrives MID-RUN as a steer: input fires, no new agent_start.
|
||||||
|
emit("input");
|
||||||
|
let shutdown = false;
|
||||||
|
emit("agent_end", { messages: [] }, { shutdown() { shutdown = true; } });
|
||||||
|
assert.equal(shutdown, true, "reply consumed mid-run → agent_end should exit, not park");
|
||||||
|
} finally {
|
||||||
|
restore();
|
||||||
|
rmSync(dir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("parks as waiting at agent_end while the reply is still pending (no input yet)", async () => {
|
||||||
|
const dir = createTestDir();
|
||||||
|
const { emit, ask, restore } = setupCapturingExtension(join(dir, "s.jsonl"));
|
||||||
|
try {
|
||||||
|
emit("agent_start");
|
||||||
|
await ask();
|
||||||
|
// No input yet — the orchestrator has not replied.
|
||||||
|
let shutdown = false;
|
||||||
|
emit("agent_end", { messages: [] }, { shutdown() { shutdown = true; } });
|
||||||
|
assert.equal(shutdown, false, "pending question with no reply must park, not exit");
|
||||||
|
} finally {
|
||||||
|
restore();
|
||||||
|
rmSync(dir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("exits when the reply arrives as a new turn (agent_start also clears the flag)", async () => {
|
||||||
|
const dir = createTestDir();
|
||||||
|
const { emit, ask, restore } = setupCapturingExtension(join(dir, "s.jsonl"));
|
||||||
|
try {
|
||||||
|
emit("agent_start");
|
||||||
|
await ask();
|
||||||
|
let shutdown1 = false;
|
||||||
|
emit("agent_end", { messages: [] }, { shutdown() { shutdown1 = true; } });
|
||||||
|
assert.equal(shutdown1, false, "parks while waiting");
|
||||||
|
// Reply arrives as a fresh turn after the subagent had parked.
|
||||||
|
emit("input");
|
||||||
|
emit("agent_start");
|
||||||
|
let shutdown2 = false;
|
||||||
|
emit("agent_end", { messages: [] }, { shutdown() { shutdown2 = true; } });
|
||||||
|
assert.equal(shutdown2, true, "after the reply turn, agent_end should exit");
|
||||||
|
} finally {
|
||||||
|
restore();
|
||||||
|
rmSync(dir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user