mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-12 09:41:11 +00:00
refactor(codex): split app-server lifecycle seams
This commit is contained in:
@@ -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.",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
212
extensions/codex/src/app-server/thread-lifecycle.ts
Normal file
212
extensions/codex/src/app-server/thread-lifecycle.ts
Normal 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;
|
||||
}
|
||||
@@ -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}.`;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user