mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-18 12:14:46 +00:00
fix(codex): fail fast after quiescent app-server turns
Fix Codex app-server turns that go quiet after the last non-assistant current-turn item completes without turn/completed.\n\nMaintainer proof: focused watchdog regression passed on rebased head, diff whitespace check passed, and local OpenClaw CLI + WebSocket app-server transport proof observed turn/interrupt after the short idle watchdog.\n\nCo-authored-by: funmerlin <funmerlin@users.noreply.github.com>
This commit is contained in:
@@ -14,6 +14,7 @@ Docs: https://docs.openclaw.ai
|
||||
- LINE: acknowledge signed webhook events before agent processing so slow model replies do not cause LINE `request_timeout` delivery failures. Fixes #65375. Thanks @myericho.
|
||||
- Codex/Lossless: keep Codex explicit compaction on native app-server threads while allowing Lossless through the context-engine slot; `openclaw doctor --fix` now migrates legacy `compaction.provider: "lossless-claw"` config to `plugins.slots.contextEngine`.
|
||||
- Cron/doctor: report scheduled jobs with explicit `payload.model` overrides, including provider namespace counts and default-model mismatches, so stale cron model pins are visible during auth or billing investigations. Fixes #82151. Thanks @mgonto.
|
||||
- Codex app-server: keep the short turn-completion idle watchdog armed after the last non-assistant current-turn item completes, so a quiet Codex app-server releases the OpenClaw session lane before the outer attempt timeout. Fixes #82171. (#82172) Thanks @funmerlin.
|
||||
- Gateway/approvals: treat `turnSourceTo` as optional in `canBridgeNoDeviceChatApprovalFromBackend`, matching the existing optional handling of `turnSourceAccountId` and `turnSourceThreadId`. Channels without a recipient concept (webchat, control-ui) leave `turnSourceTo` null on both the approval snapshot and the replay params, so the prior required-string check rejected every backend replay with `APPROVAL_CLIENT_MISMATCH`. Cross-channel replay is still gated by the required `turnSourceChannel` and `sessionKey` checks. Fixes #82132. (#82136) Thanks @ottodeng.
|
||||
- Cron: load runtime plugins before isolated cron model and delivery resolution so external channels can be selected for scheduled runs. (#82111) Thanks @medns.
|
||||
- Twitch: keep gateway accounts running until shutdown instead of treating successful monitor startup as a clean channel exit, preventing immediate auto-restart loops. Fixes #60071. (#81853) Thanks @edenfunf.
|
||||
|
||||
@@ -2458,6 +2458,93 @@ describe("runCodexAppServerAttempt", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("times out promptly when the last completed non-assistant current-turn item is not followed by turn completion", async () => {
|
||||
let notify: (notification: CodexServerNotification) => Promise<void> = async () => undefined;
|
||||
const request = vi.fn(async (method: string) => {
|
||||
if (method === "thread/start") {
|
||||
return threadStartResult("thread-1");
|
||||
}
|
||||
if (method === "turn/start") {
|
||||
return turnStartResult("turn-1", "inProgress");
|
||||
}
|
||||
return {};
|
||||
});
|
||||
setCodexAppServerClientFactoryForTest(
|
||||
async () =>
|
||||
({
|
||||
request,
|
||||
addNotificationHandler: (handler: typeof notify) => {
|
||||
notify = handler;
|
||||
return () => undefined;
|
||||
},
|
||||
addRequestHandler: () => () => undefined,
|
||||
}) as never,
|
||||
);
|
||||
const params = createParams(
|
||||
path.join(tempDir, "session.jsonl"),
|
||||
path.join(tempDir, "workspace"),
|
||||
);
|
||||
params.timeoutMs = 200;
|
||||
|
||||
const run = runCodexAppServerAttempt(params, {
|
||||
turnCompletionIdleTimeoutMs: 5,
|
||||
turnTerminalIdleTimeoutMs: 60_000,
|
||||
});
|
||||
await vi.waitFor(
|
||||
() =>
|
||||
expect(request).toHaveBeenCalledWith("turn/start", expect.anything(), expect.anything()),
|
||||
{ interval: 1 },
|
||||
);
|
||||
await notify({
|
||||
method: "item/started",
|
||||
params: {
|
||||
threadId: "thread-1",
|
||||
turnId: "turn-1",
|
||||
item: {
|
||||
type: "dynamicToolCall",
|
||||
id: "tool-1",
|
||||
tool: "sessions_list",
|
||||
arguments: {},
|
||||
status: "inProgress",
|
||||
},
|
||||
},
|
||||
});
|
||||
await notify({
|
||||
method: "item/completed",
|
||||
params: {
|
||||
threadId: "thread-1",
|
||||
turnId: "turn-1",
|
||||
item: {
|
||||
type: "dynamicToolCall",
|
||||
id: "tool-1",
|
||||
tool: "sessions_list",
|
||||
arguments: {},
|
||||
status: "completed",
|
||||
success: true,
|
||||
contentItems: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await expect(run).resolves.toMatchObject({
|
||||
aborted: true,
|
||||
timedOut: true,
|
||||
promptError: "codex app-server turn idle timed out waiting for turn/completed",
|
||||
});
|
||||
await vi.waitFor(
|
||||
() =>
|
||||
expect(request).toHaveBeenCalledWith(
|
||||
"turn/interrupt",
|
||||
{
|
||||
threadId: "thread-1",
|
||||
turnId: "turn-1",
|
||||
},
|
||||
{ timeoutMs: 5_000 },
|
||||
),
|
||||
{ interval: 1 },
|
||||
);
|
||||
});
|
||||
|
||||
it("applies before_prompt_build to Codex developer instructions and turn input", async () => {
|
||||
const beforePromptBuild = vi.fn(async () => ({
|
||||
systemPrompt: "custom codex system",
|
||||
|
||||
@@ -1258,6 +1258,16 @@ export async function runCodexAppServerAttempt(
|
||||
turnAssistantCompletionIdleWatchArmed &&
|
||||
notification.method === "item/completed" &&
|
||||
activeTurnItemIds.size === 0;
|
||||
const trackedDynamicToolCompletion = isTrackedOpenClawDynamicToolCompletionNotification(
|
||||
notification,
|
||||
activeOpenClawDynamicToolCallIds,
|
||||
);
|
||||
const shouldRearmCompletionIdleWatchAfterLastCurrentTurnItem =
|
||||
isCurrentTurnNotification &&
|
||||
notification.method === "item/completed" &&
|
||||
activeTurnItemIds.size === 0 &&
|
||||
!trackedDynamicToolCompletion &&
|
||||
!isCompletedAssistantNotification(notification);
|
||||
if (isCurrentTurnNotification && notification.method === "error") {
|
||||
if (isRetryableErrorNotification(notification.params)) {
|
||||
disarmTurnCompletionIdleWatch();
|
||||
@@ -1271,6 +1281,11 @@ export async function runCodexAppServerAttempt(
|
||||
armTurnAssistantCompletionIdleWatch(describeNotificationActivity(notification));
|
||||
} else if (unblockedAssistantCompletionRelease) {
|
||||
armTurnAssistantCompletionIdleWatch(describeNotificationActivity(notification));
|
||||
} else if (shouldRearmCompletionIdleWatchAfterLastCurrentTurnItem) {
|
||||
// If a non-assistant current-turn item is the last active item and the
|
||||
// bridge then goes quiet, reset the short completion-idle guard from that
|
||||
// final completion so the remaining silent-turn gap fails fast.
|
||||
armTurnCompletionIdleWatch();
|
||||
} else if (
|
||||
isCurrentTurnNotification &&
|
||||
shouldDisarmAssistantCompletionIdleWatch(notification)
|
||||
@@ -1282,10 +1297,8 @@ export async function runCodexAppServerAttempt(
|
||||
!turnCompletionIdleWatchPinnedByTerminalError &&
|
||||
notification.method !== "turn/completed" &&
|
||||
isCurrentTurnNotification &&
|
||||
!isTrackedOpenClawDynamicToolCompletionNotification(
|
||||
notification,
|
||||
activeOpenClawDynamicToolCallIds,
|
||||
)
|
||||
!trackedDynamicToolCompletion &&
|
||||
!shouldRearmCompletionIdleWatchAfterLastCurrentTurnItem
|
||||
) {
|
||||
// The short completion-idle watchdog guards blind gaps after Codex
|
||||
// accepts a turn or after OpenClaw hands a turn-scoped request result
|
||||
|
||||
Reference in New Issue
Block a user