feat: interrupt canceled codex app-server turns
This commit is contained in:
@@ -72,6 +72,11 @@ function normalizePositiveInteger(value, fallback) {
|
||||
return Number.isFinite(numeric) && numeric > 0 ? Math.floor(numeric) : fallback;
|
||||
}
|
||||
|
||||
function normalizeNonNegativeInteger(value, fallback) {
|
||||
const numeric = Number(value);
|
||||
return Number.isFinite(numeric) && numeric >= 0 ? Math.floor(numeric) : fallback;
|
||||
}
|
||||
|
||||
function boolFromConfigOrEnv(configValue, envValue, fallback) {
|
||||
if (configValue === true || configValue === false) {
|
||||
return configValue;
|
||||
@@ -2809,6 +2814,9 @@ export async function executeCodexAppServerTask(runnerConfig, task) {
|
||||
const progressCollector = createProgressCollector();
|
||||
const progressEmits = [];
|
||||
let lastProgressSnapshotJson = "";
|
||||
let interruptRequested = false;
|
||||
let interruptReason = "";
|
||||
let interruptPollTimer;
|
||||
const pending = new Map();
|
||||
const retryTimers = new Set();
|
||||
let resolveTurnCompleted;
|
||||
@@ -2836,6 +2844,10 @@ export async function executeCodexAppServerTask(runnerConfig, task) {
|
||||
|
||||
const cleanup = () => {
|
||||
clearTimeout(timeout);
|
||||
if (interruptPollTimer) {
|
||||
clearInterval(interruptPollTimer);
|
||||
interruptPollTimer = undefined;
|
||||
}
|
||||
for (const timer of retryTimers) {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
@@ -2984,12 +2996,67 @@ export async function executeCodexAppServerTask(runnerConfig, task) {
|
||||
const status = message.params?.turn?.status ?? message.params?.status ?? "completed";
|
||||
if (status === "completed") {
|
||||
resolveTurnCompleted(message.params ?? {});
|
||||
} else if (status === "interrupted" && interruptRequested) {
|
||||
resolveTurnCompleted({ ...(message.params ?? {}), interrupted: true });
|
||||
} else {
|
||||
rejectTurnCompleted(new Error(`CODEX_APP_SERVER_TURN_${String(status).toUpperCase()}`));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const startActiveTurnInterruptPolling = ({ threadId, turnId }) => {
|
||||
if (
|
||||
!threadId ||
|
||||
!turnId ||
|
||||
typeof runnerConfig.shouldInterruptActiveTurn !== "function"
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pollIntervalMs = normalizeNonNegativeInteger(
|
||||
runnerConfig.interruptPollIntervalMs,
|
||||
750,
|
||||
);
|
||||
const checkAndInterrupt = async () => {
|
||||
if (interruptRequested || turnSettled || closed) {
|
||||
return;
|
||||
}
|
||||
let decision;
|
||||
try {
|
||||
decision = await runnerConfig.shouldInterruptActiveTurn({
|
||||
taskId: task?.taskId,
|
||||
task,
|
||||
threadId,
|
||||
turnId,
|
||||
});
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
const shouldInterrupt =
|
||||
decision === true ||
|
||||
decision?.interrupt === true ||
|
||||
decision?.canceled === true ||
|
||||
decision?.status === "canceled";
|
||||
if (!shouldInterrupt || interruptRequested || turnSettled || closed) {
|
||||
return;
|
||||
}
|
||||
interruptRequested = true;
|
||||
interruptReason = trimToDefined(decision?.reason) || "USER_CANCELED_TASK";
|
||||
try {
|
||||
await request("turn/interrupt", { threadId, turnId });
|
||||
} catch (error) {
|
||||
rejectTurnCompleted(error);
|
||||
}
|
||||
};
|
||||
|
||||
void checkAndInterrupt();
|
||||
if (pollIntervalMs > 0) {
|
||||
interruptPollTimer = setInterval(() => {
|
||||
void checkAndInterrupt();
|
||||
}, pollIntervalMs);
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
rpcTransport = await openCodexAppServerTransport(runnerConfig, cwd, {
|
||||
onLine: handleTransportLine,
|
||||
@@ -3041,17 +3108,34 @@ export async function executeCodexAppServerTask(runnerConfig, task) {
|
||||
model: runnerConfig.model,
|
||||
});
|
||||
activeTurnStarted = true;
|
||||
const activeTurnId = trimToDefined(turnResult?.turn?.id) || targetTurnRef;
|
||||
startActiveTurnInterruptPolling({ threadId, turnId: activeTurnId });
|
||||
await turnCompleted;
|
||||
if (progressEmits.length > 0) {
|
||||
await Promise.allSettled(progressEmits);
|
||||
}
|
||||
|
||||
const normalizedReply = (replyBody || completedMessageText).trim();
|
||||
if (interruptRequested) {
|
||||
return {
|
||||
status: "interrupted",
|
||||
replyBody: "已按用户要求中断当前 Codex turn。",
|
||||
threadId,
|
||||
turnId: activeTurnId,
|
||||
turnControl: "interrupt",
|
||||
interruptReason,
|
||||
cwd,
|
||||
transport: runnerConfig.transport,
|
||||
executionProgress: progressCollector.snapshot(),
|
||||
interThreadBroker,
|
||||
canFallbackToCli: false,
|
||||
};
|
||||
}
|
||||
return {
|
||||
status: "completed",
|
||||
replyBody: normalizedReply,
|
||||
threadId,
|
||||
turnId: trimToDefined(turnResult?.turn?.id) || targetTurnRef,
|
||||
turnId: activeTurnId,
|
||||
turnControl,
|
||||
cwd,
|
||||
transport: runnerConfig.transport,
|
||||
|
||||
Reference in New Issue
Block a user