mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 07:20:43 +00:00
Respect retryable Codex app-server errors
Codex app-server sends retryable stream error notifications while a turn is still recovering. OpenClaw now ignores retryable app-server errors and preserves nested terminal error messages instead of replacing them with a generic fallback.
This commit is contained in:
@@ -113,6 +113,9 @@ Docs: https://docs.openclaw.ai
|
||||
- Media delivery: avoid sending generated image attachments twice when the
|
||||
assistant reply already includes explicit `MEDIA:` lines for the same turn,
|
||||
and reject unsafe remote `MEDIA:` URLs before delivery. Thanks @pashpashpash.
|
||||
- Codex harness: ignore retryable app-server error notifications after Codex
|
||||
recovers, and preserve the real nested error message for terminal app-server
|
||||
failures instead of replacing it with a generic failure. Thanks @pashpashpash.
|
||||
- Agents/subagents: keep queued subagent announces session-only when the
|
||||
requester has no external channel target, avoiding ambiguous multi-channel
|
||||
delivery failures. Fixes #59201. Thanks @larrylhollan.
|
||||
|
||||
@@ -129,6 +129,17 @@ function agentMessageDelta(delta: string, itemId = "msg-1"): ProjectorNotificati
|
||||
return forCurrentTurn("item/agentMessage/delta", { itemId, delta });
|
||||
}
|
||||
|
||||
function appServerError(params: { message: string; willRetry: boolean }): ProjectorNotification {
|
||||
return forCurrentTurn("error", {
|
||||
error: {
|
||||
message: params.message,
|
||||
codexErrorInfo: null,
|
||||
additionalDetails: null,
|
||||
},
|
||||
willRetry: params.willRetry,
|
||||
});
|
||||
}
|
||||
|
||||
function turnCompleted(items: unknown[] = []): ProjectorNotification {
|
||||
return {
|
||||
method: "turn/completed",
|
||||
@@ -235,6 +246,40 @@ describe("CodexAppServerEventProjector", () => {
|
||||
expect(result.lastAssistant?.content).toEqual([{ type: "text", text: "OK from raw" }]);
|
||||
});
|
||||
|
||||
it("does not fail a completed reply after a retryable app-server error notification", async () => {
|
||||
const projector = await createProjector();
|
||||
|
||||
await projector.handleNotification(agentMessageDelta("still working"));
|
||||
await projector.handleNotification(
|
||||
appServerError({ message: "stream disconnected", willRetry: true }),
|
||||
);
|
||||
await projector.handleNotification(
|
||||
turnCompleted([{ type: "agentMessage", id: "msg-1", text: "final answer" }]),
|
||||
);
|
||||
|
||||
const result = projector.buildResult(buildEmptyToolTelemetry());
|
||||
|
||||
expect(result.assistantTexts).toEqual(["final answer"]);
|
||||
expect(result.promptError).toBeNull();
|
||||
expect(result.promptErrorSource).toBeNull();
|
||||
expect(result.lastAssistant?.stopReason).toBe("stop");
|
||||
expect(result.lastAssistant?.errorMessage).toBeUndefined();
|
||||
});
|
||||
|
||||
it("uses nested app-server error messages for terminal errors", async () => {
|
||||
const projector = await createProjector();
|
||||
|
||||
await projector.handleNotification(
|
||||
appServerError({ message: "stream failed permanently", willRetry: false }),
|
||||
);
|
||||
|
||||
const result = projector.buildResult(buildEmptyToolTelemetry());
|
||||
|
||||
expect(result.promptError).toBe("stream failed permanently");
|
||||
expect(result.promptErrorSource).toBe("prompt");
|
||||
expect(result.lastAssistant).toBeUndefined();
|
||||
});
|
||||
|
||||
it("normalizes snake_case current token usage fields", async () => {
|
||||
const projector = await createProjector();
|
||||
|
||||
|
||||
@@ -153,7 +153,10 @@ export class CodexAppServerEventProjector {
|
||||
this.handleRawResponseItemCompleted(params);
|
||||
break;
|
||||
case "error":
|
||||
this.promptError = readString(params, "message") ?? "codex app-server error";
|
||||
if (readBooleanAlias(params, ["willRetry", "will_retry"]) === true) {
|
||||
break;
|
||||
}
|
||||
this.promptError = readCodexErrorNotificationMessage(params) ?? "codex app-server error";
|
||||
this.promptErrorSource = "prompt";
|
||||
break;
|
||||
default:
|
||||
@@ -844,6 +847,29 @@ function readNumber(record: JsonObject, key: string): number | undefined {
|
||||
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
||||
}
|
||||
|
||||
function readBoolean(record: JsonObject, key: string): boolean | undefined {
|
||||
const value = record[key];
|
||||
return typeof value === "boolean" ? value : undefined;
|
||||
}
|
||||
|
||||
function readBooleanAlias(record: JsonObject, keys: readonly string[]): boolean | undefined {
|
||||
for (const key of keys) {
|
||||
const value = readBoolean(record, key);
|
||||
if (value !== undefined) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function readCodexErrorNotificationMessage(record: JsonObject): string | undefined {
|
||||
const error = record.error;
|
||||
if (isJsonObject(error)) {
|
||||
return readString(error, "message") ?? readString(error, "error");
|
||||
}
|
||||
return readString(record, "message");
|
||||
}
|
||||
|
||||
function readHookOutputEntries(
|
||||
value: JsonValue | undefined,
|
||||
): Array<{ kind?: string; text: string }> {
|
||||
|
||||
Reference in New Issue
Block a user