Discard plan-review intent after interrupted or superseding input

This commit is contained in:
wassname2
2026-09-21 12:00:45 +08:00
parent 87a9b72680
commit 572d1f8fe0
3 changed files with 12 additions and 5 deletions
+4 -4
View File
@@ -620,14 +620,14 @@ export default function mainSupervisor(pi: ExtensionAPI) {
if (state.child && state.parent) pi.appendEntry(RUN, { plan: state.plan, parent: state.parent, session: identity(ctx) });
if (clearCheckIn) { clearTimeout(clearCheckIn.deadline); clearCheckIn.deadline = undefined; }
});
let requestedPlanReview: { generation: number; digest: string } | undefined;
let requestedPlanReview: { generation: number; digest: string; signal?: AbortSignal } | undefined;
pi.on("agent_settled", async (_e, ctx) => {
agentRunActive = false;
clearCheckIn?.startDeadline?.();
reconcileReports(ctx);
remindReports(ctx);
const requested = requestedPlanReview; requestedPlanReview = undefined;
if (!requested || requested.generation !== generation || state.child || state.mode !== "planning" || !ctx.hasUI) return;
if (!requested || requested.signal?.aborted || requested.generation !== generation || state.child || state.mode !== "planning" || !ctx.hasUI) return;
const snapshot = readPlan();
if (snapshot.text === undefined || digest(snapshot.text) !== requested.digest) return;
await ready(ctx, true);
@@ -667,7 +667,7 @@ export default function mainSupervisor(pi: ExtensionAPI) {
return { systemPrompt: `${event.systemPrompt}\n\n${role}${pending.length ? `\n${pendingReportReviews(pending.map(reportLabel))}` : ""}`, ...(message ? { message } : {}) };
});
pi.on("input", (event, ctx) => {
if (event.source !== "extension") { pauseCheckIn = false; return; }
if (event.source !== "extension") { requestedPlanReview = undefined; pauseCheckIn = false; return; }
const wake = /^\[Scheduled task ([a-zA-Z0-9_-]+) fired\]\nName: ([^\n]+)\nAction: prompt\n/.exec(event.text);
if (!wake || wake[2] !== `goals-${ctx.sessionManager.getSessionId()}` || !ownedCheckInIds(ctx).has(wake[1])) return;
const snapshot = readPlan();
@@ -709,7 +709,7 @@ export default function mainSupervisor(pi: ExtensionAPI) {
parameters: Type.Object({}),
async execute(_id, _params, signal, _update, ctx) {
if (signal?.aborted || state.child || state.mode !== "planning" || !ctx.hasUI) return result(planReviewResult.unavailable);
requestedPlanReview = { generation, digest: digest(planText()) };
requestedPlanReview = { generation, digest: digest(planText()), signal };
return result(planReviewResult.queued);
},
});
+1 -1
View File
@@ -141,7 +141,7 @@ export const planDocument = (objective: string) => `# ${objective.split("\n")[0]
export const requestPlanReviewDescription = "Intentionally present the settled goal draft for human acceptance through the existing Ready/Discuss/Edit/Cancel menu. Planning parent only. Not for provisional drafts, redrafting, interview or Log updates; resolve consequential open questions first unless the user explicitly requests a shortcut. Only human Ready authorizes execution.";
export const planReviewResult = {
unavailable: "Plan review requires a planning parent and an interactive UI. No execution authorized.",
queued: "Intentional review queued for the end of this turn. Finish without repeating the full plan; the interface will present it. No execution authorized before human Ready. Further edits, a failed/aborted turn, Discuss/Edit/Cancel or a mode change cancel this request; request again only when settled.",
queued: "Intentional review queued for the end of this turn. Finish without repeating the full plan; the interface will present it. No execution authorized before human Ready. Further edits, new user input, a failed/aborted turn, Discuss/Edit/Cancel or a mode change cancel this request; request again only when settled.",
};
export const discuss = "Type your changes in chat; the draft stays open.";
+7
View File
@@ -337,6 +337,13 @@ it("keeps provisional drafts and interview updates separate from intentional acc
f.hooks.get("agent_end")({ messages: [{ role: "assistant", content: [], stopReason }] }, f.ctx);
await f.hooks.get("agent_settled")({}, f.ctx);
}
const interrupted = new AbortController();
await f.tools.get("RequestPlanReview").execute("interrupted", {}, interrupted.signal, undefined, f.ctx);
interrupted.abort(); // can arrive after the tool returned, without a new assistant error
await f.hooks.get("agent_settled")({}, f.ctx);
await f.tools.get("RequestPlanReview").execute("superseded", {}, undefined, undefined, f.ctx);
f.hooks.get("input")({ source: "interactive", text: "Wait, another question" }, f.ctx);
await f.hooks.get("agent_settled")({}, f.ctx);
await f.tools.get("RequestPlanReview").execute("discussed", {}, undefined, undefined, f.ctx);
await f.command("discuss");
await f.hooks.get("agent_settled")({}, f.ctx);