mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-19 00:24:45 +00:00
fix(codex): normalize thread id/sessionId cross-fill before schema validation (#80137)
Merged via squash.
Prepared head SHA: b2c20dd5d6
Co-authored-by: kagura-agent <268167063+kagura-agent@users.noreply.github.com>
Co-authored-by: omarshahine <10343873+omarshahine@users.noreply.github.com>
Reviewed-by: @omarshahine
This commit is contained in:
@@ -153,6 +153,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Volcengine/Kimi: strip provider-unsupported tool schema length and item constraint keywords for direct and coding-plan models so hosted Kimi runs do not reject message tools with `minLength`. Fixes #38817.
|
||||
- DeepSeek: backfill V4 `reasoning_content` replay fields for unowned OpenAI-compatible proxy providers, preventing follow-up request failures outside the bundled DeepSeek and OpenRouter routes. Fixes #79608.
|
||||
- iMessage: emit a WARN log when an action is blocked because the imsg private API bridge is not attached, so operators see the silent-drop in `~/.openclaw/logs/openclaw.log` instead of having to read per-session trajectory JSONL `tool.result` payloads. Common after a gateway restart un-injects the dylib from Messages.app. (#80035) Thanks @omarshahine.
|
||||
- Codex: cross-fill missing `thread.id` and `thread.sessionId` before schema validation so live Codex app-server responses that omit `sessionId` no longer fail `thread/start` or `thread/resume`. Fixes #80124. (#80137) Thanks @kagura-agent.
|
||||
|
||||
## 2026.5.9
|
||||
|
||||
|
||||
71
extensions/codex/src/app-server/protocol-validators.test.ts
Normal file
71
extensions/codex/src/app-server/protocol-validators.test.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
assertCodexThreadStartResponse,
|
||||
assertCodexThreadResumeResponse,
|
||||
} from "./protocol-validators.js";
|
||||
|
||||
function makeMinimalThread(overrides: Record<string, unknown> = {}) {
|
||||
return {
|
||||
id: "thread-1",
|
||||
sessionId: "session-1",
|
||||
cliVersion: "0.129.0",
|
||||
createdAt: 1715299200,
|
||||
updatedAt: 1715299200,
|
||||
cwd: "/tmp",
|
||||
ephemeral: false,
|
||||
modelProvider: "openai",
|
||||
preview: "test thread",
|
||||
source: "appServer",
|
||||
status: { type: "notLoaded" },
|
||||
turns: [],
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function makeMinimalResponse(threadOverrides: Record<string, unknown> = {}) {
|
||||
return {
|
||||
approvalPolicy: "never",
|
||||
approvalsReviewer: "user",
|
||||
cwd: "/tmp",
|
||||
model: "gpt-5.4",
|
||||
modelProvider: "openai",
|
||||
sandbox: { type: "dangerFullAccess" },
|
||||
thread: makeMinimalThread(threadOverrides),
|
||||
};
|
||||
}
|
||||
|
||||
describe("assertCodexThreadStartResponse", () => {
|
||||
it("accepts response with both id and sessionId", () => {
|
||||
const response = makeMinimalResponse();
|
||||
const result = assertCodexThreadStartResponse(response);
|
||||
expect(result.thread).toMatchObject({ id: "thread-1", sessionId: "session-1" });
|
||||
});
|
||||
|
||||
it("normalizes missing sessionId from id", () => {
|
||||
const response = makeMinimalResponse({ sessionId: undefined });
|
||||
// Remove the sessionId key entirely
|
||||
delete (response.thread as Record<string, unknown>).sessionId;
|
||||
const result = assertCodexThreadStartResponse(response);
|
||||
expect(result.thread).toMatchObject({ id: "thread-1", sessionId: "thread-1" });
|
||||
});
|
||||
|
||||
it("normalizes missing id from sessionId", () => {
|
||||
const response = makeMinimalResponse({ id: undefined, sessionId: "session-1" });
|
||||
delete (response.thread as Record<string, unknown>).id;
|
||||
const result = assertCodexThreadStartResponse(response);
|
||||
expect(result.thread).toMatchObject({ id: "session-1", sessionId: "session-1" });
|
||||
});
|
||||
|
||||
it("throws on invalid response", () => {
|
||||
expect(() => assertCodexThreadStartResponse({})).toThrow("Invalid Codex app-server");
|
||||
});
|
||||
});
|
||||
|
||||
describe("assertCodexThreadResumeResponse", () => {
|
||||
it("normalizes missing sessionId from id", () => {
|
||||
const response = makeMinimalResponse({ sessionId: undefined });
|
||||
delete (response.thread as Record<string, unknown>).sessionId;
|
||||
const result = assertCodexThreadResumeResponse(response);
|
||||
expect(result.thread).toMatchObject({ id: "thread-1", sessionId: "thread-1" });
|
||||
});
|
||||
});
|
||||
@@ -43,11 +43,19 @@ const validateTurnCompletedNotification = ajv.compile<CodexTurnCompletedNotifica
|
||||
const validateTurnStartResponse = ajv.compile<CodexTurnStartResponse>(turnStartResponseSchema);
|
||||
|
||||
export function assertCodexThreadStartResponse(value: unknown): CodexThreadStartResponse {
|
||||
return assertCodexShape(validateThreadStartResponse, value, "thread/start response");
|
||||
return assertCodexShape(
|
||||
validateThreadStartResponse,
|
||||
normalizeThreadResponse(value),
|
||||
"thread/start response",
|
||||
);
|
||||
}
|
||||
|
||||
export function assertCodexThreadResumeResponse(value: unknown): CodexThreadResumeResponse {
|
||||
return assertCodexShape(validateThreadResumeResponse, value, "thread/resume response");
|
||||
return assertCodexShape(
|
||||
validateThreadResumeResponse,
|
||||
normalizeThreadResponse(value),
|
||||
"thread/resume response",
|
||||
);
|
||||
}
|
||||
|
||||
export function assertCodexTurnStartResponse(value: unknown): CodexTurnStartResponse {
|
||||
@@ -140,6 +148,23 @@ function normalizeThreadItem(value: unknown): unknown {
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeThreadResponse(value: unknown): unknown {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value) || !("thread" in value)) {
|
||||
return value;
|
||||
}
|
||||
const thread = (value as { thread?: unknown }).thread;
|
||||
if (thread && typeof thread === "object" && !Array.isArray(thread)) {
|
||||
const t = thread as { id?: string; sessionId?: string };
|
||||
if (typeof t.id === "string" && typeof t.sessionId !== "string") {
|
||||
return { ...value, thread: { ...thread, sessionId: t.id } };
|
||||
}
|
||||
if (typeof t.sessionId === "string" && typeof t.id !== "string") {
|
||||
return { ...value, thread: { ...thread, id: t.sessionId } };
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function normalizeTurnStartResponse(value: unknown): unknown {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value) || !("turn" in value)) {
|
||||
return value;
|
||||
|
||||
Reference in New Issue
Block a user