fix: keep headless alive for provider auto-resume
This commit is contained in:
parent
db41f92812
commit
120d7deda8
3 changed files with 79 additions and 8 deletions
|
|
@ -117,16 +117,30 @@ export function isTerminalNotification(
|
|||
return TERMINAL_PREFIXES.some((prefix) => message.startsWith(prefix));
|
||||
}
|
||||
|
||||
export function isPauseNotification(event: Record<string, unknown>): boolean {
|
||||
if (event.type !== "extension_ui_request" || event.method !== "notify")
|
||||
return false;
|
||||
const message = String(event.message ?? "").toLowerCase();
|
||||
return (
|
||||
message.startsWith("auto-mode paused") ||
|
||||
message.startsWith("step-mode paused")
|
||||
);
|
||||
}
|
||||
|
||||
export function isAutoResumeScheduledNotification(
|
||||
event: Record<string, unknown>,
|
||||
): boolean {
|
||||
if (event.type !== "extension_ui_request" || event.method !== "notify")
|
||||
return false;
|
||||
return /auto-resuming in \d+s/i.test(String(event.message ?? ""));
|
||||
}
|
||||
|
||||
export function isBlockedNotification(event: Record<string, unknown>): boolean {
|
||||
if (event.type !== "extension_ui_request" || event.method !== "notify")
|
||||
return false;
|
||||
const message = String(event.message ?? "").toLowerCase();
|
||||
// Blocked notifications come through stopAuto as "Auto-mode stopped (Blocked: ...)"
|
||||
return (
|
||||
message.includes("blocked:") ||
|
||||
message.startsWith("auto-mode paused") ||
|
||||
message.startsWith("step-mode paused")
|
||||
);
|
||||
return message.includes("blocked:") || isPauseNotification(event);
|
||||
}
|
||||
|
||||
export function isMilestoneReadyNotification(
|
||||
|
|
|
|||
|
|
@ -30,12 +30,14 @@ import {
|
|||
EXIT_SUCCESS,
|
||||
FIRE_AND_FORGET_METHODS,
|
||||
IDLE_TIMEOUT_MS,
|
||||
MULTI_TURN_DEADLOCK_BACKSTOP_MS,
|
||||
isAutoResumeScheduledNotification,
|
||||
isBlockedNotification,
|
||||
isInteractiveHeadlessTool,
|
||||
isMilestoneReadyNotification,
|
||||
isPauseNotification,
|
||||
isQuickCommand,
|
||||
isTerminalNotification,
|
||||
MULTI_TURN_DEADLOCK_BACKSTOP_MS,
|
||||
mapStatusToExitCode,
|
||||
NEW_MILESTONE_IDLE_TIMEOUT_MS,
|
||||
shouldArmHeadlessIdleTimeout,
|
||||
|
|
@ -554,6 +556,7 @@ async function runHeadlessOnce(
|
|||
let completed = false;
|
||||
let exitCode = 0;
|
||||
let milestoneReady = false; // tracks "Milestone X ready." for auto-chaining
|
||||
let providerAutoResumePending = false;
|
||||
const recentEvents: TrackedEvent[] = [];
|
||||
let lastVisibleProgressAt = Date.now();
|
||||
const interactiveToolCallIds = new Set<string>();
|
||||
|
|
@ -1100,8 +1103,15 @@ async function runHeadlessOnce(
|
|||
|
||||
// Handle extension_ui_request
|
||||
if (eventObj.type === "extension_ui_request" && clientStarted) {
|
||||
const waitForProviderAutoResume =
|
||||
providerAutoResumePending && isPauseNotification(eventObj);
|
||||
|
||||
if (isAutoResumeScheduledNotification(eventObj)) {
|
||||
providerAutoResumePending = true;
|
||||
}
|
||||
|
||||
// Check for terminal notification before auto-responding
|
||||
if (isBlockedNotification(eventObj)) {
|
||||
if (isBlockedNotification(eventObj) && !waitForProviderAutoResume) {
|
||||
blocked = true;
|
||||
}
|
||||
|
||||
|
|
@ -1110,13 +1120,20 @@ async function runHeadlessOnce(
|
|||
milestoneReady = true;
|
||||
}
|
||||
|
||||
if (isTerminalNotification(eventObj)) {
|
||||
if (isTerminalNotification(eventObj) && !waitForProviderAutoResume) {
|
||||
completed = true;
|
||||
}
|
||||
|
||||
// Structured trace: handle unit start/end notify messages
|
||||
if (eventObj.method === "notify") {
|
||||
const message = String(eventObj.message ?? "");
|
||||
if (
|
||||
message.includes("Auto-mode resumed") ||
|
||||
message.includes("Step-mode resumed") ||
|
||||
(message.includes("[unit]") && message.includes("starting"))
|
||||
) {
|
||||
providerAutoResumePending = false;
|
||||
}
|
||||
if (traceActive) {
|
||||
if (message.includes("[unit]") && message.includes("starting")) {
|
||||
handleUnitStart(message);
|
||||
|
|
|
|||
|
|
@ -194,8 +194,10 @@ import {
|
|||
EXIT_CANCELLED,
|
||||
EXIT_ERROR,
|
||||
EXIT_SUCCESS,
|
||||
isAutoResumeScheduledNotification,
|
||||
isBlockedNotification,
|
||||
isInteractiveHeadlessTool,
|
||||
isPauseNotification,
|
||||
isTerminalNotification,
|
||||
mapStatusToExitCode,
|
||||
shouldArmHeadlessIdleTimeout,
|
||||
|
|
@ -271,6 +273,44 @@ test("isBlockedNotification: auto pause exits as blocked", () => {
|
|||
);
|
||||
});
|
||||
|
||||
test("isAutoResumeScheduledNotification detects provider auto-resume notices", () => {
|
||||
assert.equal(
|
||||
isAutoResumeScheduledNotification({
|
||||
type: "extension_ui_request",
|
||||
method: "notify",
|
||||
message: "Rate limited: rate limit exceeded. Auto-resuming in 60s...",
|
||||
}),
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
isAutoResumeScheduledNotification({
|
||||
type: "extension_ui_request",
|
||||
method: "notify",
|
||||
message: "Auto-mode paused (Escape). Type to interact.",
|
||||
}),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test("isPauseNotification detects pause banners separately from auto-resume notices", () => {
|
||||
assert.equal(
|
||||
isPauseNotification({
|
||||
type: "extension_ui_request",
|
||||
method: "notify",
|
||||
message: "Auto-mode paused (Escape). Type to interact.",
|
||||
}),
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
isPauseNotification({
|
||||
type: "extension_ui_request",
|
||||
method: "notify",
|
||||
message: "Rate limited: rate limit exceeded. Auto-resuming in 60s...",
|
||||
}),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test("shouldArmHeadlessIdleTimeout: arms after tool calls when no interactive tool is in flight", () => {
|
||||
assert.equal(shouldArmHeadlessIdleTimeout(1, 0), true);
|
||||
assert.equal(shouldArmHeadlessIdleTimeout(3, 0), true);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue