refactor(codex): split app-server lifecycle seams

This commit is contained in:
Peter Steinberger
2026-04-10 23:05:11 +01:00
parent 979ae0bb53
commit 3b65e2302a
9 changed files with 318 additions and 239 deletions

View File

@@ -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;
}

View File

@@ -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<Pick<RpcRequest, "id" | "method">> & { params?: JsonValue },
) => Promise<JsonValue | undefined> | 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);

View File

@@ -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: {

View File

@@ -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<T>(params: {
}
}
async function startOrResumeThread(params: {
client: CodexAppServerClient;
params: EmbeddedRunAttemptParams;
cwd: string;
dynamicTools: JsonValue[];
appServer: CodexAppServerRuntimeOptions;
}): Promise<CodexAppServerThreadBinding> {
const dynamicToolsFingerprint = fingerprintDynamicTools(params.dynamicTools);
const binding = await readCodexAppServerBinding(params.params.sessionFile);
if (binding?.threadId) {
// `/codex resume <thread>` 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<CodexThreadResumeResponse>(
"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<CodexThreadStartResponse>("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 {

View File

@@ -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<CodexAppServerThreadBinding> {
const dynamicToolsFingerprint = fingerprintDynamicTools(params.dynamicTools);
const binding = await readCodexAppServerBinding(params.params.sessionFile);
if (binding?.threadId) {
// `/codex resume <thread>` 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<CodexThreadResumeResponse>(
"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<CodexThreadStartResponse>("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;
}

View File

@@ -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<CodexCommandDeps> },
): 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<string> {
const response = await codexControlRequest(pluginConfig, CODEX_CONTROL_METHODS.listThreads, {
async function buildThreads(
deps: CodexCommandDeps,
pluginConfig: unknown,
filter: string,
): Promise<string> {
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<stri
}
async function resumeThread(
deps: CodexCommandDeps,
ctx: PluginCommandContext,
pluginConfig: unknown,
threadId: string | undefined,
@@ -113,13 +143,17 @@ async function resumeThread(
if (!ctx.sessionFile) {
return "Cannot attach a Codex thread because this command did not include an OpenClaw session file.";
}
const response = await codexControlRequest(pluginConfig, CODEX_CONTROL_METHODS.resumeThread, {
threadId: normalizedThreadId,
persistExtendedHistory: true,
});
const response = await deps.codexControlRequest(
pluginConfig,
CODEX_CONTROL_METHODS.resumeThread,
{
threadId: normalizedThreadId,
persistExtendedHistory: true,
},
);
const thread = isJsonObject(response) && isJsonObject(response.thread) ? response.thread : {};
const effectiveThreadId = readString(thread, "id") ?? normalizedThreadId;
await writeCodexAppServerBinding(ctx.sessionFile, {
await deps.writeCodexAppServerBinding(ctx.sessionFile, {
threadId: effectiveThreadId,
cwd: readString(thread, "cwd") ?? "",
model: isJsonObject(response) ? readString(response, "model") : undefined,
@@ -129,6 +163,7 @@ async function resumeThread(
}
async function startThreadAction(
deps: CodexCommandDeps,
ctx: PluginCommandContext,
pluginConfig: unknown,
method: typeof CODEX_CONTROL_METHODS.compact | typeof CODEX_CONTROL_METHODS.review,
@@ -137,11 +172,11 @@ async function startThreadAction(
if (!ctx.sessionFile) {
return `Cannot start Codex ${label} because this command did not include an OpenClaw session file.`;
}
const binding = await readCodexAppServerBinding(ctx.sessionFile);
const binding = await deps.readCodexAppServerBinding(ctx.sessionFile);
if (!binding?.threadId) {
return `No Codex thread is attached to this OpenClaw session yet.`;
}
await codexControlRequest(pluginConfig, method, { threadId: binding.threadId });
await deps.codexControlRequest(pluginConfig, method, { threadId: binding.threadId });
return `Started Codex ${label} for thread ${binding.threadId}.`;
}

View File

@@ -65,10 +65,6 @@ export async function safeValue<T>(read: () => Promise<T>): Promise<SafeValue<T>
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);
}

View File

@@ -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<CodexCommandDeps>;
}): 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<CodexCommandDeps> } = {},
): Promise<{ text: string }> {
return await handleCodexSubcommand(ctx, options);
}