From 3b65e2302a55619efef8104f80db118bef1804dc Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 10 Apr 2026 23:05:11 +0100 Subject: [PATCH] refactor(codex): split app-server lifecycle seams --- extensions/codex/openclaw.plugin.json | 5 + .../codex/src/app-server/capabilities.ts | 13 +- extensions/codex/src/app-server/client.ts | 14 +- extensions/codex/src/app-server/config.ts | 19 ++ .../codex/src/app-server/run-attempt.ts | 212 +----------------- .../codex/src/app-server/thread-lifecycle.ts | 212 ++++++++++++++++++ extensions/codex/src/command-handlers.ts | 71 ++++-- extensions/codex/src/command-rpc.ts | 6 +- extensions/codex/src/commands.ts | 5 +- 9 files changed, 318 insertions(+), 239 deletions(-) create mode 100644 extensions/codex/src/app-server/thread-lifecycle.ts diff --git a/extensions/codex/openclaw.plugin.json b/extensions/codex/openclaw.plugin.json index 99fd62a6a4d..eb6525a87f3 100644 --- a/extensions/codex/openclaw.plugin.json +++ b/extensions/codex/openclaw.plugin.json @@ -124,6 +124,11 @@ "sensitive": true, "advanced": true }, + "appServer.headers": { + "label": "Headers", + "help": "Additional headers sent to the WebSocket app-server.", + "advanced": true + }, "appServer.requestTimeoutMs": { "label": "Request Timeout", "help": "Maximum time to wait for Codex app-server control-plane requests.", diff --git a/extensions/codex/src/app-server/capabilities.ts b/extensions/codex/src/app-server/capabilities.ts index 27bae16408d..7d9a751e70e 100644 --- a/extensions/codex/src/app-server/capabilities.ts +++ b/extensions/codex/src/app-server/capabilities.ts @@ -1,3 +1,5 @@ +import { CodexAppServerRpcError } from "./client.js"; + export const CODEX_CONTROL_METHODS = { account: "account/read", compact: "thread/compact/start", @@ -12,10 +14,13 @@ export const CODEX_CONTROL_METHODS = { export type CodexControlName = keyof typeof CODEX_CONTROL_METHODS; export type CodexControlMethod = (typeof CODEX_CONTROL_METHODS)[CodexControlName]; -export function describeControlFailure(error: string): string { - return isUnsupportedControlError(error) ? "unsupported by this Codex app-server" : error; +export function describeControlFailure(error: unknown): string { + if (isUnsupportedControlError(error)) { + return "unsupported by this Codex app-server"; + } + return error instanceof Error ? error.message : String(error); } -function isUnsupportedControlError(error: string): boolean { - return /method not found|unknown method|unsupported method|-32601/i.test(error); +function isUnsupportedControlError(error: unknown): error is CodexAppServerRpcError { + return error instanceof CodexAppServerRpcError && error.code === -32601; } diff --git a/extensions/codex/src/app-server/client.ts b/extensions/codex/src/app-server/client.ts index 220e7ffdadf..fc3c1b94a6b 100644 --- a/extensions/codex/src/app-server/client.ts +++ b/extensions/codex/src/app-server/client.ts @@ -22,6 +22,18 @@ type PendingRequest = { reject: (error: Error) => void; }; +export class CodexAppServerRpcError extends Error { + readonly code?: number; + readonly data?: JsonValue; + + constructor(error: { code?: number; message: string; data?: JsonValue }, method: string) { + super(error.message || `${method} failed`); + this.name = "CodexAppServerRpcError"; + this.code = error.code; + this.data = error.data; + } +} + export type CodexServerRequestHandler = ( request: Required> & { params?: JsonValue }, ) => Promise | JsonValue | undefined; @@ -192,7 +204,7 @@ export class CodexAppServerClient { } this.pending.delete(response.id); if (response.error) { - pending.reject(new Error(response.error.message || `${pending.method} failed`)); + pending.reject(new CodexAppServerRpcError(response.error, pending.method)); return; } pending.resolve(response.result); diff --git a/extensions/codex/src/app-server/config.ts b/extensions/codex/src/app-server/config.ts index 45bd06e6e26..3028cbb0d7c 100644 --- a/extensions/codex/src/app-server/config.ts +++ b/extensions/codex/src/app-server/config.ts @@ -43,6 +43,20 @@ export type CodexPluginConfig = { }; }; +export const CODEX_APP_SERVER_CONFIG_KEYS = [ + "transport", + "command", + "args", + "url", + "authToken", + "headers", + "requestTimeoutMs", + "approvalPolicy", + "sandbox", + "approvalsReviewer", + "serviceTier", +] as const; + const codexAppServerTransportSchema = z.enum(["stdio", "websocket"]); const codexAppServerApprovalPolicySchema = z.enum([ "never", @@ -101,6 +115,11 @@ export function resolveCodexAppServerRuntimeOptions( const headers = normalizeHeaders(config.headers); const authToken = readNonEmptyString(config.authToken); const url = readNonEmptyString(config.url); + if (transport === "websocket" && !url) { + throw new Error( + "plugins.entries.codex.config.appServer.url is required when appServer.transport is websocket", + ); + } return { start: { diff --git a/extensions/codex/src/app-server/run-attempt.ts b/extensions/codex/src/app-server/run-attempt.ts index 1ab35228d3d..318a17f64d6 100644 --- a/extensions/codex/src/app-server/run-attempt.ts +++ b/extensions/codex/src/app-server/run-attempt.ts @@ -19,33 +19,20 @@ import { } from "openclaw/plugin-sdk/agent-harness"; import { handleCodexAppServerApprovalRequest } from "./approval-bridge.js"; import { isCodexAppServerApprovalRequest, type CodexAppServerClient } from "./client.js"; -import { - resolveCodexAppServerRuntimeOptions, - type CodexAppServerRuntimeOptions, - type CodexAppServerStartOptions, -} from "./config.js"; +import { resolveCodexAppServerRuntimeOptions, type CodexAppServerStartOptions } from "./config.js"; import { createCodexDynamicToolBridge } from "./dynamic-tools.js"; import { CodexAppServerEventProjector } from "./event-projector.js"; import { isJsonObject, type CodexServerNotification, type CodexDynamicToolCallParams, - type CodexThreadResumeParams, - type CodexThreadResumeResponse, - type CodexThreadStartResponse, - type CodexTurnStartParams, type CodexTurnStartResponse, - type CodexUserInput, type JsonObject, type JsonValue, } from "./protocol.js"; -import { - clearCodexAppServerBinding, - readCodexAppServerBinding, - writeCodexAppServerBinding, - type CodexAppServerThreadBinding, -} from "./session-binding.js"; +import type { CodexAppServerThreadBinding } from "./session-binding.js"; import { clearSharedCodexAppServerClient, getSharedCodexAppServerClient } from "./shared-client.js"; +import { buildTurnStartParams, startOrResumeThread } from "./thread-lifecycle.js"; import { mirrorCodexAppServerTranscript } from "./transcript-mirror.js"; type CodexAppServerClientFactory = ( @@ -397,199 +384,6 @@ async function withCodexStartupTimeout(params: { } } -async function startOrResumeThread(params: { - client: CodexAppServerClient; - params: EmbeddedRunAttemptParams; - cwd: string; - dynamicTools: JsonValue[]; - appServer: CodexAppServerRuntimeOptions; -}): Promise { - const dynamicToolsFingerprint = fingerprintDynamicTools(params.dynamicTools); - const binding = await readCodexAppServerBinding(params.params.sessionFile); - if (binding?.threadId) { - // `/codex resume ` writes a binding before the next turn can know - // the dynamic tool catalog, so only invalidate fingerprints we actually have. - if ( - binding.dynamicToolsFingerprint && - binding.dynamicToolsFingerprint !== dynamicToolsFingerprint - ) { - embeddedAgentLog.debug( - "codex app-server dynamic tool catalog changed; starting a new thread", - { - threadId: binding.threadId, - }, - ); - await clearCodexAppServerBinding(params.params.sessionFile); - } else { - try { - const response = await params.client.request( - "thread/resume", - buildThreadResumeParams(params.params, { - threadId: binding.threadId, - appServer: params.appServer, - }), - ); - await writeCodexAppServerBinding(params.params.sessionFile, { - threadId: response.thread.id, - cwd: params.cwd, - model: params.params.modelId, - modelProvider: response.modelProvider ?? normalizeModelProvider(params.params.provider), - dynamicToolsFingerprint, - createdAt: binding.createdAt, - }); - return { - ...binding, - threadId: response.thread.id, - cwd: params.cwd, - model: params.params.modelId, - modelProvider: response.modelProvider ?? normalizeModelProvider(params.params.provider), - dynamicToolsFingerprint, - }; - } catch (error) { - embeddedAgentLog.warn("codex app-server thread resume failed; starting a new thread", { - error, - }); - await clearCodexAppServerBinding(params.params.sessionFile); - } - } - } - - const response = await params.client.request("thread/start", { - model: params.params.modelId, - modelProvider: normalizeModelProvider(params.params.provider), - cwd: params.cwd, - approvalPolicy: params.appServer.approvalPolicy, - approvalsReviewer: params.appServer.approvalsReviewer, - sandbox: params.appServer.sandbox, - ...(params.appServer.serviceTier ? { serviceTier: params.appServer.serviceTier } : {}), - serviceName: "OpenClaw", - developerInstructions: buildDeveloperInstructions(params.params), - dynamicTools: params.dynamicTools, - experimentalRawEvents: true, - persistExtendedHistory: true, - }); - const createdAt = new Date().toISOString(); - await writeCodexAppServerBinding(params.params.sessionFile, { - threadId: response.thread.id, - cwd: params.cwd, - model: response.model ?? params.params.modelId, - modelProvider: response.modelProvider ?? normalizeModelProvider(params.params.provider), - dynamicToolsFingerprint, - createdAt, - }); - return { - schemaVersion: 1, - threadId: response.thread.id, - sessionFile: params.params.sessionFile, - cwd: params.cwd, - model: response.model ?? params.params.modelId, - modelProvider: response.modelProvider ?? normalizeModelProvider(params.params.provider), - dynamicToolsFingerprint, - createdAt, - updatedAt: createdAt, - }; -} - -export function buildThreadResumeParams( - params: EmbeddedRunAttemptParams, - options: { - threadId: string; - appServer: CodexAppServerRuntimeOptions; - }, -): CodexThreadResumeParams { - return { - threadId: options.threadId, - model: params.modelId, - modelProvider: normalizeModelProvider(params.provider), - approvalPolicy: options.appServer.approvalPolicy, - approvalsReviewer: options.appServer.approvalsReviewer, - sandbox: options.appServer.sandbox, - ...(options.appServer.serviceTier ? { serviceTier: options.appServer.serviceTier } : {}), - persistExtendedHistory: true, - }; -} - -export function buildTurnStartParams( - params: EmbeddedRunAttemptParams, - options: { - threadId: string; - cwd: string; - appServer: CodexAppServerRuntimeOptions; - }, -): CodexTurnStartParams { - return { - threadId: options.threadId, - input: buildUserInput(params), - cwd: options.cwd, - approvalPolicy: options.appServer.approvalPolicy, - approvalsReviewer: options.appServer.approvalsReviewer, - model: params.modelId, - ...(options.appServer.serviceTier ? { serviceTier: options.appServer.serviceTier } : {}), - effort: resolveReasoningEffort(params.thinkLevel), - }; -} - -function fingerprintDynamicTools(dynamicTools: JsonValue[]): string { - return JSON.stringify(dynamicTools.map(stabilizeJsonValue)); -} - -function stabilizeJsonValue(value: JsonValue): JsonValue { - if (Array.isArray(value)) { - return value.map(stabilizeJsonValue); - } - if (!isJsonObject(value)) { - return value; - } - const stable: JsonObject = {}; - for (const [key, child] of Object.entries(value).toSorted(([left], [right]) => - left.localeCompare(right), - )) { - stable[key] = stabilizeJsonValue(child); - } - return stable; -} - -function buildDeveloperInstructions(params: EmbeddedRunAttemptParams): string { - const sections = [ - "You are running inside OpenClaw. Use OpenClaw dynamic tools for messaging, cron, sessions, and host actions when available.", - "Preserve the user's existing channel/session context. If sending a channel reply, use the OpenClaw messaging tool instead of describing that you would reply.", - params.extraSystemPrompt, - params.skillsSnapshot?.prompt, - ]; - return sections.filter((section) => typeof section === "string" && section.trim()).join("\n\n"); -} - -function buildUserInput(params: EmbeddedRunAttemptParams): CodexUserInput[] { - return [ - { type: "text", text: params.prompt }, - ...(params.images ?? []).map( - (image): CodexUserInput => ({ - type: "image", - url: `data:${image.mimeType};base64,${image.data}`, - }), - ), - ]; -} - -function normalizeModelProvider(provider: string): string { - return provider === "codex" || provider === "openai-codex" ? "openai" : provider; -} - -function resolveReasoningEffort( - thinkLevel: EmbeddedRunAttemptParams["thinkLevel"], -): "minimal" | "low" | "medium" | "high" | "xhigh" | null { - if ( - thinkLevel === "minimal" || - thinkLevel === "low" || - thinkLevel === "medium" || - thinkLevel === "high" || - thinkLevel === "xhigh" - ) { - return thinkLevel; - } - return null; -} - function readDynamicToolCallParams( value: JsonValue | undefined, ): CodexDynamicToolCallParams | undefined { diff --git a/extensions/codex/src/app-server/thread-lifecycle.ts b/extensions/codex/src/app-server/thread-lifecycle.ts new file mode 100644 index 00000000000..a0dd23eeaa3 --- /dev/null +++ b/extensions/codex/src/app-server/thread-lifecycle.ts @@ -0,0 +1,212 @@ +import { embeddedAgentLog, type EmbeddedRunAttemptParams } from "openclaw/plugin-sdk/agent-harness"; +import type { CodexAppServerClient } from "./client.js"; +import type { CodexAppServerRuntimeOptions } from "./config.js"; +import { + isJsonObject, + type CodexThreadResumeParams, + type CodexThreadResumeResponse, + type CodexThreadStartResponse, + type CodexTurnStartParams, + type CodexUserInput, + type JsonObject, + type JsonValue, +} from "./protocol.js"; +import { + clearCodexAppServerBinding, + readCodexAppServerBinding, + writeCodexAppServerBinding, + type CodexAppServerThreadBinding, +} from "./session-binding.js"; + +export async function startOrResumeThread(params: { + client: CodexAppServerClient; + params: EmbeddedRunAttemptParams; + cwd: string; + dynamicTools: JsonValue[]; + appServer: CodexAppServerRuntimeOptions; +}): Promise { + const dynamicToolsFingerprint = fingerprintDynamicTools(params.dynamicTools); + const binding = await readCodexAppServerBinding(params.params.sessionFile); + if (binding?.threadId) { + // `/codex resume ` writes a binding before the next turn can know + // the dynamic tool catalog, so only invalidate fingerprints we actually have. + if ( + binding.dynamicToolsFingerprint && + binding.dynamicToolsFingerprint !== dynamicToolsFingerprint + ) { + embeddedAgentLog.debug( + "codex app-server dynamic tool catalog changed; starting a new thread", + { + threadId: binding.threadId, + }, + ); + await clearCodexAppServerBinding(params.params.sessionFile); + } else { + try { + const response = await params.client.request( + "thread/resume", + buildThreadResumeParams(params.params, { + threadId: binding.threadId, + appServer: params.appServer, + }), + ); + await writeCodexAppServerBinding(params.params.sessionFile, { + threadId: response.thread.id, + cwd: params.cwd, + model: params.params.modelId, + modelProvider: response.modelProvider ?? normalizeModelProvider(params.params.provider), + dynamicToolsFingerprint, + createdAt: binding.createdAt, + }); + return { + ...binding, + threadId: response.thread.id, + cwd: params.cwd, + model: params.params.modelId, + modelProvider: response.modelProvider ?? normalizeModelProvider(params.params.provider), + dynamicToolsFingerprint, + }; + } catch (error) { + embeddedAgentLog.warn("codex app-server thread resume failed; starting a new thread", { + error, + }); + await clearCodexAppServerBinding(params.params.sessionFile); + } + } + } + + const response = await params.client.request("thread/start", { + model: params.params.modelId, + modelProvider: normalizeModelProvider(params.params.provider), + cwd: params.cwd, + approvalPolicy: params.appServer.approvalPolicy, + approvalsReviewer: params.appServer.approvalsReviewer, + sandbox: params.appServer.sandbox, + ...(params.appServer.serviceTier ? { serviceTier: params.appServer.serviceTier } : {}), + serviceName: "OpenClaw", + developerInstructions: buildDeveloperInstructions(params.params), + dynamicTools: params.dynamicTools, + experimentalRawEvents: true, + persistExtendedHistory: true, + }); + const createdAt = new Date().toISOString(); + await writeCodexAppServerBinding(params.params.sessionFile, { + threadId: response.thread.id, + cwd: params.cwd, + model: response.model ?? params.params.modelId, + modelProvider: response.modelProvider ?? normalizeModelProvider(params.params.provider), + dynamicToolsFingerprint, + createdAt, + }); + return { + schemaVersion: 1, + threadId: response.thread.id, + sessionFile: params.params.sessionFile, + cwd: params.cwd, + model: response.model ?? params.params.modelId, + modelProvider: response.modelProvider ?? normalizeModelProvider(params.params.provider), + dynamicToolsFingerprint, + createdAt, + updatedAt: createdAt, + }; +} + +export function buildThreadResumeParams( + params: EmbeddedRunAttemptParams, + options: { + threadId: string; + appServer: CodexAppServerRuntimeOptions; + }, +): CodexThreadResumeParams { + return { + threadId: options.threadId, + model: params.modelId, + modelProvider: normalizeModelProvider(params.provider), + approvalPolicy: options.appServer.approvalPolicy, + approvalsReviewer: options.appServer.approvalsReviewer, + sandbox: options.appServer.sandbox, + ...(options.appServer.serviceTier ? { serviceTier: options.appServer.serviceTier } : {}), + persistExtendedHistory: true, + }; +} + +export function buildTurnStartParams( + params: EmbeddedRunAttemptParams, + options: { + threadId: string; + cwd: string; + appServer: CodexAppServerRuntimeOptions; + }, +): CodexTurnStartParams { + return { + threadId: options.threadId, + input: buildUserInput(params), + cwd: options.cwd, + approvalPolicy: options.appServer.approvalPolicy, + approvalsReviewer: options.appServer.approvalsReviewer, + model: params.modelId, + ...(options.appServer.serviceTier ? { serviceTier: options.appServer.serviceTier } : {}), + effort: resolveReasoningEffort(params.thinkLevel), + }; +} + +function fingerprintDynamicTools(dynamicTools: JsonValue[]): string { + return JSON.stringify(dynamicTools.map(stabilizeJsonValue)); +} + +function stabilizeJsonValue(value: JsonValue): JsonValue { + if (Array.isArray(value)) { + return value.map(stabilizeJsonValue); + } + if (!isJsonObject(value)) { + return value; + } + const stable: JsonObject = {}; + for (const [key, child] of Object.entries(value).toSorted(([left], [right]) => + left.localeCompare(right), + )) { + stable[key] = stabilizeJsonValue(child); + } + return stable; +} + +function buildDeveloperInstructions(params: EmbeddedRunAttemptParams): string { + const sections = [ + "You are running inside OpenClaw. Use OpenClaw dynamic tools for messaging, cron, sessions, and host actions when available.", + "Preserve the user's existing channel/session context. If sending a channel reply, use the OpenClaw messaging tool instead of describing that you would reply.", + params.extraSystemPrompt, + params.skillsSnapshot?.prompt, + ]; + return sections.filter((section) => typeof section === "string" && section.trim()).join("\n\n"); +} + +function buildUserInput(params: EmbeddedRunAttemptParams): CodexUserInput[] { + return [ + { type: "text", text: params.prompt }, + ...(params.images ?? []).map( + (image): CodexUserInput => ({ + type: "image", + url: `data:${image.mimeType};base64,${image.data}`, + }), + ), + ]; +} + +function normalizeModelProvider(provider: string): string { + return provider === "codex" || provider === "openai-codex" ? "openai" : provider; +} + +function resolveReasoningEffort( + thinkLevel: EmbeddedRunAttemptParams["thinkLevel"], +): "minimal" | "low" | "medium" | "high" | "xhigh" | null { + if ( + thinkLevel === "minimal" || + thinkLevel === "low" || + thinkLevel === "medium" || + thinkLevel === "high" || + thinkLevel === "xhigh" + ) { + return thinkLevel; + } + return null; +} diff --git a/extensions/codex/src/command-handlers.ts b/extensions/codex/src/command-handlers.ts index 51463c2eba7..3612c4dde12 100644 --- a/extensions/codex/src/command-handlers.ts +++ b/extensions/codex/src/command-handlers.ts @@ -22,32 +22,56 @@ import { safeCodexControlRequest, } from "./command-rpc.js"; +export type CodexCommandDeps = { + codexControlRequest: typeof codexControlRequest; + listCodexAppServerModels: typeof listCodexAppServerModels; + readCodexStatusProbes: typeof readCodexStatusProbes; + readCodexAppServerBinding: typeof readCodexAppServerBinding; + requestOptions: typeof requestOptions; + safeCodexControlRequest: typeof safeCodexControlRequest; + writeCodexAppServerBinding: typeof writeCodexAppServerBinding; +}; + +const defaultCodexCommandDeps: CodexCommandDeps = { + codexControlRequest, + listCodexAppServerModels, + readCodexStatusProbes, + readCodexAppServerBinding, + requestOptions, + safeCodexControlRequest, + writeCodexAppServerBinding, +}; + export async function handleCodexSubcommand( ctx: PluginCommandContext, - options: { pluginConfig?: unknown }, + options: { pluginConfig?: unknown; deps?: Partial }, ): Promise<{ text: string }> { + const deps: CodexCommandDeps = { ...defaultCodexCommandDeps, ...options.deps }; const [subcommand = "status", ...rest] = splitArgs(ctx.args); const normalized = subcommand.toLowerCase(); if (normalized === "help") { return { text: buildHelp() }; } if (normalized === "status") { - return { text: formatCodexStatus(await readCodexStatusProbes(options.pluginConfig)) }; + return { text: formatCodexStatus(await deps.readCodexStatusProbes(options.pluginConfig)) }; } if (normalized === "models") { return { - text: formatModels(await listCodexAppServerModels(requestOptions(options.pluginConfig, 100))), + text: formatModels( + await deps.listCodexAppServerModels(deps.requestOptions(options.pluginConfig, 100)), + ), }; } if (normalized === "threads") { - return { text: await buildThreads(options.pluginConfig, rest.join(" ")) }; + return { text: await buildThreads(deps, options.pluginConfig, rest.join(" ")) }; } if (normalized === "resume") { - return { text: await resumeThread(ctx, options.pluginConfig, rest[0]) }; + return { text: await resumeThread(deps, ctx, options.pluginConfig, rest[0]) }; } if (normalized === "compact") { return { text: await startThreadAction( + deps, ctx, options.pluginConfig, CODEX_CONTROL_METHODS.compact, @@ -58,6 +82,7 @@ export async function handleCodexSubcommand( if (normalized === "review") { return { text: await startThreadAction( + deps, ctx, options.pluginConfig, CODEX_CONTROL_METHODS.review, @@ -68,7 +93,7 @@ export async function handleCodexSubcommand( if (normalized === "mcp") { return { text: formatList( - await codexControlRequest(options.pluginConfig, CODEX_CONTROL_METHODS.listMcpServers, { + await deps.codexControlRequest(options.pluginConfig, CODEX_CONTROL_METHODS.listMcpServers, { limit: 100, }), "MCP servers", @@ -78,23 +103,27 @@ export async function handleCodexSubcommand( if (normalized === "skills") { return { text: formatList( - await codexControlRequest(options.pluginConfig, CODEX_CONTROL_METHODS.listSkills, {}), + await deps.codexControlRequest(options.pluginConfig, CODEX_CONTROL_METHODS.listSkills, {}), "Codex skills", ), }; } if (normalized === "account") { const [account, limits] = await Promise.all([ - safeCodexControlRequest(options.pluginConfig, CODEX_CONTROL_METHODS.account, {}), - safeCodexControlRequest(options.pluginConfig, CODEX_CONTROL_METHODS.rateLimits, {}), + deps.safeCodexControlRequest(options.pluginConfig, CODEX_CONTROL_METHODS.account, {}), + deps.safeCodexControlRequest(options.pluginConfig, CODEX_CONTROL_METHODS.rateLimits, {}), ]); return { text: formatAccount(account, limits) }; } return { text: `Unknown Codex command: ${subcommand}\n\n${buildHelp()}` }; } -async function buildThreads(pluginConfig: unknown, filter: string): Promise { - const response = await codexControlRequest(pluginConfig, CODEX_CONTROL_METHODS.listThreads, { +async function buildThreads( + deps: CodexCommandDeps, + pluginConfig: unknown, + filter: string, +): Promise { + const response = await deps.codexControlRequest(pluginConfig, CODEX_CONTROL_METHODS.listThreads, { limit: 10, ...(filter.trim() ? { filter: filter.trim() } : {}), }); @@ -102,6 +131,7 @@ async function buildThreads(pluginConfig: unknown, filter: string): Promise(read: () => Promise): Promise try { return { ok: true, value: await read() }; } catch (error) { - return { ok: false, error: describeControlFailure(formatError(error)) }; + return { ok: false, error: describeControlFailure(error) }; } } - -function formatError(error: unknown): string { - return error instanceof Error ? error.message : String(error); -} diff --git a/extensions/codex/src/commands.ts b/extensions/codex/src/commands.ts index ad4d20a8a82..d26ae0e00c8 100644 --- a/extensions/codex/src/commands.ts +++ b/extensions/codex/src/commands.ts @@ -2,10 +2,11 @@ import type { OpenClawPluginCommandDefinition, PluginCommandContext, } from "openclaw/plugin-sdk/plugin-entry"; -import { handleCodexSubcommand } from "./command-handlers.js"; +import { handleCodexSubcommand, type CodexCommandDeps } from "./command-handlers.js"; export function createCodexCommand(options: { pluginConfig?: unknown; + deps?: Partial; }): OpenClawPluginCommandDefinition { return { name: "codex", @@ -18,7 +19,7 @@ export function createCodexCommand(options: { export async function handleCodexCommand( ctx: PluginCommandContext, - options: { pluginConfig?: unknown } = {}, + options: { pluginConfig?: unknown; deps?: Partial } = {}, ): Promise<{ text: string }> { return await handleCodexSubcommand(ctx, options); }