mirror of
https://github.com/moltbot/moltbot.git
synced 2026-05-13 15:47:28 +00:00
Manage the Codex app-server binary in OpenClaw (#71808)
* Manage Codex app-server binary * Use plugin deps for Codex app-server binary * Stabilize media model registry test * Exclude checkpoint transcripts from memory ingestion
This commit is contained in:
@@ -103,8 +103,9 @@ Codex after changing config.
|
|||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
- OpenClaw with the bundled `codex` plugin available.
|
- OpenClaw with the bundled `codex` plugin available.
|
||||||
- Codex app-server `0.125.0` or newer. Native MCP hook payloads landed in Codex
|
- Codex app-server `0.125.0` or newer. The bundled plugin manages a compatible
|
||||||
`0.124.0`; OpenClaw uses `0.125.0` as the tested support floor.
|
Codex app-server binary by default, so local `codex` commands on `PATH` do
|
||||||
|
not affect normal harness startup.
|
||||||
- Codex auth available to the app-server process.
|
- Codex auth available to the app-server process.
|
||||||
|
|
||||||
The plugin blocks older or unversioned app-server handshakes. That keeps
|
The plugin blocks older or unversioned app-server handshakes. That keeps
|
||||||
@@ -340,12 +341,18 @@ fallback catalog:
|
|||||||
|
|
||||||
## App-server connection and policy
|
## App-server connection and policy
|
||||||
|
|
||||||
By default, the plugin starts Codex locally with:
|
By default, the plugin starts OpenClaw's managed Codex binary locally with:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
codex app-server --listen stdio://
|
codex app-server --listen stdio://
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The managed binary is declared as a bundled plugin runtime dependency and staged
|
||||||
|
with the rest of the `codex` plugin dependencies. This keeps the app-server
|
||||||
|
version tied to the bundled plugin instead of whichever separate Codex CLI
|
||||||
|
happens to be installed locally. Set `appServer.command` only when you
|
||||||
|
intentionally want to run a different executable.
|
||||||
|
|
||||||
By default, OpenClaw starts local Codex harness sessions in YOLO mode:
|
By default, OpenClaw starts local Codex harness sessions in YOLO mode:
|
||||||
`approvalPolicy: "never"`, `approvalsReviewer: "user"`, and
|
`approvalPolicy: "never"`, `approvalsReviewer: "user"`, and
|
||||||
`sandbox: "danger-full-access"`. This is the trusted local operator posture used
|
`sandbox: "danger-full-access"`. This is the trusted local operator posture used
|
||||||
@@ -414,7 +421,7 @@ Supported `appServer` fields:
|
|||||||
| Field | Default | Meaning |
|
| Field | Default | Meaning |
|
||||||
| ------------------- | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
|
| ------------------- | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
|
||||||
| `transport` | `"stdio"` | `"stdio"` spawns Codex; `"websocket"` connects to `url`. |
|
| `transport` | `"stdio"` | `"stdio"` spawns Codex; `"websocket"` connects to `url`. |
|
||||||
| `command` | `"codex"` | Executable for stdio transport. |
|
| `command` | managed Codex binary | Executable for stdio transport. Leave unset to use the managed binary; set it only for an explicit override. |
|
||||||
| `args` | `["app-server", "--listen", "stdio://"]` | Arguments for stdio transport. |
|
| `args` | `["app-server", "--listen", "stdio://"]` | Arguments for stdio transport. |
|
||||||
| `url` | unset | WebSocket app-server URL. |
|
| `url` | unset | WebSocket app-server URL. |
|
||||||
| `authToken` | unset | Bearer token for WebSocket transport. |
|
| `authToken` | unset | Bearer token for WebSocket transport. |
|
||||||
@@ -426,8 +433,7 @@ Supported `appServer` fields:
|
|||||||
| `approvalsReviewer` | `"user"` | Use `"auto_review"` to let Codex review native approval prompts. `guardian_subagent` remains a legacy alias. |
|
| `approvalsReviewer` | `"user"` | Use `"auto_review"` to let Codex review native approval prompts. `guardian_subagent` remains a legacy alias. |
|
||||||
| `serviceTier` | unset | Optional Codex app-server service tier: `"fast"`, `"flex"`, or `null`. Invalid legacy values are ignored. |
|
| `serviceTier` | unset | Optional Codex app-server service tier: `"fast"`, `"flex"`, or `null`. Invalid legacy values are ignored. |
|
||||||
|
|
||||||
The older environment variables still work as fallbacks for local testing when
|
Environment overrides remain available for local testing:
|
||||||
the matching config field is unset:
|
|
||||||
|
|
||||||
- `OPENCLAW_CODEX_APP_SERVER_BIN`
|
- `OPENCLAW_CODEX_APP_SERVER_BIN`
|
||||||
- `OPENCLAW_CODEX_APP_SERVER_ARGS`
|
- `OPENCLAW_CODEX_APP_SERVER_ARGS`
|
||||||
@@ -435,6 +441,9 @@ the matching config field is unset:
|
|||||||
- `OPENCLAW_CODEX_APP_SERVER_APPROVAL_POLICY`
|
- `OPENCLAW_CODEX_APP_SERVER_APPROVAL_POLICY`
|
||||||
- `OPENCLAW_CODEX_APP_SERVER_SANDBOX`
|
- `OPENCLAW_CODEX_APP_SERVER_SANDBOX`
|
||||||
|
|
||||||
|
`OPENCLAW_CODEX_APP_SERVER_BIN` bypasses the managed binary when
|
||||||
|
`appServer.command` is unset.
|
||||||
|
|
||||||
`OPENCLAW_CODEX_APP_SERVER_GUARDIAN=1` was removed. Use
|
`OPENCLAW_CODEX_APP_SERVER_GUARDIAN=1` was removed. Use
|
||||||
`plugins.entries.codex.config.appServer.mode: "guardian"` instead, or
|
`plugins.entries.codex.config.appServer.mode: "guardian"` instead, or
|
||||||
`OPENCLAW_CODEX_APP_SERVER_MODE=guardian` for one-off local testing. Config is
|
`OPENCLAW_CODEX_APP_SERVER_MODE=guardian` for one-off local testing. Config is
|
||||||
|
|||||||
@@ -57,10 +57,7 @@
|
|||||||
"enum": ["stdio", "websocket"],
|
"enum": ["stdio", "websocket"],
|
||||||
"default": "stdio"
|
"default": "stdio"
|
||||||
},
|
},
|
||||||
"command": {
|
"command": { "type": "string" },
|
||||||
"type": "string",
|
|
||||||
"default": "codex"
|
|
||||||
},
|
|
||||||
"args": {
|
"args": {
|
||||||
"oneOf": [
|
"oneOf": [
|
||||||
{
|
{
|
||||||
@@ -132,7 +129,7 @@
|
|||||||
},
|
},
|
||||||
"appServer.command": {
|
"appServer.command": {
|
||||||
"label": "Command",
|
"label": "Command",
|
||||||
"help": "Executable used for stdio transport.",
|
"help": "Executable used for stdio transport. Leave unset to use OpenClaw's managed Codex binary.",
|
||||||
"advanced": true
|
"advanced": true
|
||||||
},
|
},
|
||||||
"appServer.args": {
|
"appServer.args": {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mariozechner/pi-coding-agent": "0.70.2",
|
"@mariozechner/pi-coding-agent": "0.70.2",
|
||||||
|
"@openai/codex": "0.125.0",
|
||||||
"ajv": "^8.18.0",
|
"ajv": "^8.18.0",
|
||||||
"ws": "^8.20.0",
|
"ws": "^8.20.0",
|
||||||
"zod": "^4.3.6"
|
"zod": "^4.3.6"
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { createInterface, type Interface as ReadlineInterface } from "node:readline";
|
import { createInterface, type Interface as ReadlineInterface } from "node:readline";
|
||||||
import { embeddedAgentLog, OPENCLAW_VERSION } from "openclaw/plugin-sdk/agent-harness-runtime";
|
import { embeddedAgentLog, OPENCLAW_VERSION } from "openclaw/plugin-sdk/agent-harness-runtime";
|
||||||
import { resolveCodexAppServerRuntimeOptions, type CodexAppServerStartOptions } from "./config.js";
|
import { resolveCodexAppServerRuntimeOptions, type CodexAppServerStartOptions } from "./config.js";
|
||||||
|
import { MIN_CODEX_APP_SERVER_VERSION } from "./version.js";
|
||||||
import {
|
import {
|
||||||
type CodexAppServerRequestMethod,
|
type CodexAppServerRequestMethod,
|
||||||
type CodexAppServerRequestParams,
|
type CodexAppServerRequestParams,
|
||||||
@@ -22,7 +23,7 @@ import {
|
|||||||
type CodexAppServerTransport,
|
type CodexAppServerTransport,
|
||||||
} from "./transport.js";
|
} from "./transport.js";
|
||||||
|
|
||||||
export const MIN_CODEX_APP_SERVER_VERSION = "0.125.0";
|
export { MIN_CODEX_APP_SERVER_VERSION } from "./version.js";
|
||||||
const CODEX_APP_SERVER_PARSE_LOG_MAX = 500;
|
const CODEX_APP_SERVER_PARSE_LOG_MAX = 500;
|
||||||
|
|
||||||
type PendingRequest = {
|
type PendingRequest = {
|
||||||
@@ -99,6 +100,9 @@ export class CodexAppServerClient {
|
|||||||
...options,
|
...options,
|
||||||
headers: options?.headers ?? defaults.headers,
|
headers: options?.headers ?? defaults.headers,
|
||||||
};
|
};
|
||||||
|
if (startOptions.transport === "stdio" && startOptions.commandSource === "managed") {
|
||||||
|
throw new Error("Managed Codex app-server start options must be resolved before spawn.");
|
||||||
|
}
|
||||||
if (startOptions.transport === "websocket") {
|
if (startOptions.transport === "websocket") {
|
||||||
return new CodexAppServerClient(createWebSocketTransport(startOptions));
|
return new CodexAppServerClient(createWebSocketTransport(startOptions));
|
||||||
}
|
}
|
||||||
@@ -407,12 +411,12 @@ function assertSupportedCodexAppServerVersion(response: CodexInitializeResponse)
|
|||||||
const detectedVersion = readCodexVersionFromUserAgent(response.userAgent);
|
const detectedVersion = readCodexVersionFromUserAgent(response.userAgent);
|
||||||
if (!detectedVersion) {
|
if (!detectedVersion) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Codex app-server ${MIN_CODEX_APP_SERVER_VERSION} or newer is required, but OpenClaw could not determine the running Codex version. Upgrade Codex CLI and retry.`,
|
`Codex app-server ${MIN_CODEX_APP_SERVER_VERSION} or newer is required, but OpenClaw could not determine the running Codex version. Update the configured Codex app-server binary, or remove custom command overrides to use the managed binary.`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (compareVersions(detectedVersion, MIN_CODEX_APP_SERVER_VERSION) < 0) {
|
if (compareVersions(detectedVersion, MIN_CODEX_APP_SERVER_VERSION) < 0) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Codex app-server ${MIN_CODEX_APP_SERVER_VERSION} or newer is required, but detected ${detectedVersion}. Upgrade Codex CLI and retry.`,
|
`Codex app-server ${MIN_CODEX_APP_SERVER_VERSION} or newer is required, but detected ${detectedVersion}. Update the configured Codex app-server binary, or remove custom command overrides to use the managed binary.`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -96,6 +96,36 @@ describe("Codex app-server config", () => {
|
|||||||
approvalPolicy: "never",
|
approvalPolicy: "never",
|
||||||
sandbox: "danger-full-access",
|
sandbox: "danger-full-access",
|
||||||
approvalsReviewer: "user",
|
approvalsReviewer: "user",
|
||||||
|
start: expect.objectContaining({
|
||||||
|
command: "codex",
|
||||||
|
commandSource: "managed",
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("treats configured and environment commands as explicit overrides", () => {
|
||||||
|
expect(
|
||||||
|
resolveCodexAppServerRuntimeOptions({
|
||||||
|
pluginConfig: { appServer: { command: "/opt/codex/bin/codex" } },
|
||||||
|
env: { OPENCLAW_CODEX_APP_SERVER_BIN: "/usr/local/bin/codex" },
|
||||||
|
}).start,
|
||||||
|
).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
command: "/opt/codex/bin/codex",
|
||||||
|
commandSource: "config",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
resolveCodexAppServerRuntimeOptions({
|
||||||
|
pluginConfig: {},
|
||||||
|
env: { OPENCLAW_CODEX_APP_SERVER_BIN: "/usr/local/bin/codex" },
|
||||||
|
}).start,
|
||||||
|
).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
command: "/usr/local/bin/codex",
|
||||||
|
commandSource: "env",
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -244,6 +274,7 @@ describe("Codex app-server config", () => {
|
|||||||
};
|
};
|
||||||
const appServerProperties = manifest.configSchema.properties.appServer.properties;
|
const appServerProperties = manifest.configSchema.properties.appServer.properties;
|
||||||
|
|
||||||
|
expect(appServerProperties.command?.default).toBeUndefined();
|
||||||
expect(appServerProperties.approvalPolicy?.default).toBeUndefined();
|
expect(appServerProperties.approvalPolicy?.default).toBeUndefined();
|
||||||
expect(appServerProperties.sandbox?.default).toBeUndefined();
|
expect(appServerProperties.sandbox?.default).toBeUndefined();
|
||||||
expect(appServerProperties.approvalsReviewer?.default).toBeUndefined();
|
expect(appServerProperties.approvalsReviewer?.default).toBeUndefined();
|
||||||
|
|||||||
@@ -7,10 +7,12 @@ export type CodexAppServerPolicyMode = "yolo" | "guardian";
|
|||||||
export type CodexAppServerApprovalPolicy = "never" | "on-request" | "on-failure" | "untrusted";
|
export type CodexAppServerApprovalPolicy = "never" | "on-request" | "on-failure" | "untrusted";
|
||||||
export type CodexAppServerSandboxMode = "read-only" | "workspace-write" | "danger-full-access";
|
export type CodexAppServerSandboxMode = "read-only" | "workspace-write" | "danger-full-access";
|
||||||
export type CodexAppServerApprovalsReviewer = "user" | "auto_review" | "guardian_subagent";
|
export type CodexAppServerApprovalsReviewer = "user" | "auto_review" | "guardian_subagent";
|
||||||
|
export type CodexAppServerCommandSource = "managed" | "resolved-managed" | "config" | "env";
|
||||||
|
|
||||||
export type CodexAppServerStartOptions = {
|
export type CodexAppServerStartOptions = {
|
||||||
transport: CodexAppServerTransportMode;
|
transport: CodexAppServerTransportMode;
|
||||||
command: string;
|
command: string;
|
||||||
|
commandSource?: CodexAppServerCommandSource;
|
||||||
args: string[];
|
args: string[];
|
||||||
url?: string;
|
url?: string;
|
||||||
authToken?: string;
|
authToken?: string;
|
||||||
@@ -125,8 +127,14 @@ export function resolveCodexAppServerRuntimeOptions(
|
|||||||
const env = params.env ?? process.env;
|
const env = params.env ?? process.env;
|
||||||
const config = readCodexPluginConfig(params.pluginConfig).appServer ?? {};
|
const config = readCodexPluginConfig(params.pluginConfig).appServer ?? {};
|
||||||
const transport = resolveTransport(config.transport);
|
const transport = resolveTransport(config.transport);
|
||||||
const command =
|
const configCommand = readNonEmptyString(config.command);
|
||||||
readNonEmptyString(config.command) ?? env.OPENCLAW_CODEX_APP_SERVER_BIN ?? "codex";
|
const envCommand = readNonEmptyString(env.OPENCLAW_CODEX_APP_SERVER_BIN);
|
||||||
|
const command = configCommand ?? envCommand ?? "codex";
|
||||||
|
const commandSource: CodexAppServerCommandSource = configCommand
|
||||||
|
? "config"
|
||||||
|
: envCommand
|
||||||
|
? "env"
|
||||||
|
: "managed";
|
||||||
const args = resolveArgs(config.args, env.OPENCLAW_CODEX_APP_SERVER_ARGS);
|
const args = resolveArgs(config.args, env.OPENCLAW_CODEX_APP_SERVER_ARGS);
|
||||||
const headers = normalizeHeaders(config.headers);
|
const headers = normalizeHeaders(config.headers);
|
||||||
const authToken = readNonEmptyString(config.authToken);
|
const authToken = readNonEmptyString(config.authToken);
|
||||||
@@ -146,6 +154,7 @@ export function resolveCodexAppServerRuntimeOptions(
|
|||||||
start: {
|
start: {
|
||||||
transport,
|
transport,
|
||||||
command,
|
command,
|
||||||
|
commandSource,
|
||||||
args: args.length > 0 ? args : ["app-server", "--listen", "stdio://"],
|
args: args.length > 0 ? args : ["app-server", "--listen", "stdio://"],
|
||||||
...(url ? { url } : {}),
|
...(url ? { url } : {}),
|
||||||
...(authToken ? { authToken } : {}),
|
...(authToken ? { authToken } : {}),
|
||||||
@@ -174,6 +183,7 @@ export function codexAppServerStartOptionsKey(
|
|||||||
return JSON.stringify({
|
return JSON.stringify({
|
||||||
transport: options.transport,
|
transport: options.transport,
|
||||||
command: options.command,
|
command: options.command,
|
||||||
|
commandSource: options.commandSource ?? null,
|
||||||
args: options.args,
|
args: options.args,
|
||||||
url: options.url ?? null,
|
url: options.url ?? null,
|
||||||
authToken: hashSecretForKey(options.authToken),
|
authToken: hashSecretForKey(options.authToken),
|
||||||
|
|||||||
95
extensions/codex/src/app-server/managed-binary.test.ts
Normal file
95
extensions/codex/src/app-server/managed-binary.test.ts
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
import path from "node:path";
|
||||||
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
import type { CodexAppServerStartOptions } from "./config.js";
|
||||||
|
import {
|
||||||
|
resolveManagedCodexAppServerPaths,
|
||||||
|
resolveManagedCodexAppServerStartOptions,
|
||||||
|
} from "./managed-binary.js";
|
||||||
|
|
||||||
|
function startOptions(
|
||||||
|
commandSource: CodexAppServerStartOptions["commandSource"],
|
||||||
|
): CodexAppServerStartOptions {
|
||||||
|
return {
|
||||||
|
transport: "stdio",
|
||||||
|
command: "codex",
|
||||||
|
commandSource,
|
||||||
|
args: ["app-server", "--listen", "stdio://"],
|
||||||
|
headers: {},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function managedCommandPath(root: string, platform: NodeJS.Platform): string {
|
||||||
|
return path.join(root, "node_modules", ".bin", platform === "win32" ? "codex.cmd" : "codex");
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("managed Codex app-server binary", () => {
|
||||||
|
it("leaves explicit command overrides unchanged", async () => {
|
||||||
|
const explicitOptions = startOptions("config");
|
||||||
|
const pathExists = vi.fn(async () => false);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
resolveManagedCodexAppServerStartOptions(explicitOptions, {
|
||||||
|
platform: "darwin",
|
||||||
|
pathExists,
|
||||||
|
}),
|
||||||
|
).resolves.toBe(explicitOptions);
|
||||||
|
expect(pathExists).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("resolves the plugin-local bundled Codex binary", async () => {
|
||||||
|
const pluginRoot = path.join("/tmp", "openclaw", "extensions", "codex");
|
||||||
|
const paths = resolveManagedCodexAppServerPaths({ platform: "darwin", pluginRoot });
|
||||||
|
const pathExists = vi.fn(async (filePath: string) => filePath === paths.commandPath);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
resolveManagedCodexAppServerStartOptions(startOptions("managed"), {
|
||||||
|
platform: "darwin",
|
||||||
|
pluginRoot,
|
||||||
|
pathExists,
|
||||||
|
}),
|
||||||
|
).resolves.toEqual({
|
||||||
|
...startOptions("managed"),
|
||||||
|
command: paths.commandPath,
|
||||||
|
commandSource: "resolved-managed",
|
||||||
|
});
|
||||||
|
expect(paths.commandPath).toBe(managedCommandPath(pluginRoot, "darwin"));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("resolves Windows Codex command shims", () => {
|
||||||
|
const pluginRoot = path.win32.join("C:\\", "OpenClaw", "dist", "extensions", "codex");
|
||||||
|
const paths = resolveManagedCodexAppServerPaths({ platform: "win32", pluginRoot });
|
||||||
|
|
||||||
|
expect(paths.commandPath.endsWith(path.win32.join("node_modules", ".bin", "codex.cmd"))).toBe(
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("finds Codex in the external runtime-deps install root used by packaged plugins", async () => {
|
||||||
|
const installRoot = path.join("/tmp", "openclaw-runtime-deps", "codex");
|
||||||
|
const pluginRoot = path.join(installRoot, "dist", "extensions", "codex");
|
||||||
|
const installedCommand = managedCommandPath(installRoot, "linux");
|
||||||
|
const pathExists = vi.fn(async (filePath: string) => filePath === installedCommand);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
resolveManagedCodexAppServerStartOptions(startOptions("managed"), {
|
||||||
|
platform: "linux",
|
||||||
|
pluginRoot,
|
||||||
|
pathExists,
|
||||||
|
}),
|
||||||
|
).resolves.toEqual({
|
||||||
|
...startOptions("managed"),
|
||||||
|
command: installedCommand,
|
||||||
|
commandSource: "resolved-managed",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("fails clearly when bundled runtime deps did not stage Codex", async () => {
|
||||||
|
await expect(
|
||||||
|
resolveManagedCodexAppServerStartOptions(startOptions("managed"), {
|
||||||
|
platform: "darwin",
|
||||||
|
pluginRoot: path.join("/tmp", "openclaw", "extensions", "codex"),
|
||||||
|
pathExists: vi.fn(async () => false),
|
||||||
|
}),
|
||||||
|
).rejects.toThrow("Managed Codex app-server binary was not found");
|
||||||
|
});
|
||||||
|
});
|
||||||
121
extensions/codex/src/app-server/managed-binary.ts
Normal file
121
extensions/codex/src/app-server/managed-binary.ts
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
import { constants as fsConstants } from "node:fs";
|
||||||
|
import { access } from "node:fs/promises";
|
||||||
|
import path from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
import type { CodexAppServerStartOptions } from "./config.js";
|
||||||
|
import { MANAGED_CODEX_APP_SERVER_PACKAGE } from "./version.js";
|
||||||
|
|
||||||
|
const CODEX_PLUGIN_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
|
||||||
|
|
||||||
|
type ManagedCodexAppServerPaths = {
|
||||||
|
commandPath: string;
|
||||||
|
candidateCommandPaths: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ResolveManagedCodexAppServerOptions = {
|
||||||
|
platform?: NodeJS.Platform;
|
||||||
|
pluginRoot?: string;
|
||||||
|
pathExists?: (filePath: string, platform: NodeJS.Platform) => Promise<boolean>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function resolveManagedCodexAppServerStartOptions(
|
||||||
|
startOptions: CodexAppServerStartOptions,
|
||||||
|
options: ResolveManagedCodexAppServerOptions = {},
|
||||||
|
): Promise<CodexAppServerStartOptions> {
|
||||||
|
if (startOptions.transport !== "stdio" || startOptions.commandSource !== "managed") {
|
||||||
|
return startOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
const platform = options.platform ?? process.platform;
|
||||||
|
const paths = resolveManagedCodexAppServerPaths({
|
||||||
|
platform,
|
||||||
|
pluginRoot: options.pluginRoot,
|
||||||
|
});
|
||||||
|
const pathExists = options.pathExists ?? commandPathExists;
|
||||||
|
const commandPath = await findManagedCodexAppServerCommandPath({
|
||||||
|
candidateCommandPaths: paths.candidateCommandPaths,
|
||||||
|
pathExists,
|
||||||
|
platform,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
...startOptions,
|
||||||
|
command: commandPath,
|
||||||
|
commandSource: "resolved-managed",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveManagedCodexAppServerPaths(params: {
|
||||||
|
platform?: NodeJS.Platform;
|
||||||
|
pluginRoot?: string;
|
||||||
|
}): ManagedCodexAppServerPaths {
|
||||||
|
const platform = params.platform ?? process.platform;
|
||||||
|
const candidateCommandPaths = resolveManagedCodexAppServerCommandCandidates(
|
||||||
|
params.pluginRoot ?? CODEX_PLUGIN_ROOT,
|
||||||
|
platform,
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
commandPath: candidateCommandPaths[0] ?? "",
|
||||||
|
candidateCommandPaths,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveManagedCodexAppServerCommandCandidates(
|
||||||
|
pluginRoot: string,
|
||||||
|
platform: NodeJS.Platform,
|
||||||
|
): string[] {
|
||||||
|
const pathApi = pathForPlatform(platform);
|
||||||
|
const commandName = platform === "win32" ? "codex.cmd" : "codex";
|
||||||
|
const roots = [
|
||||||
|
pluginRoot,
|
||||||
|
pathApi.dirname(pluginRoot),
|
||||||
|
pathApi.dirname(pathApi.dirname(pluginRoot)),
|
||||||
|
isDistExtensionRoot(pluginRoot, platform)
|
||||||
|
? pathApi.dirname(pathApi.dirname(pathApi.dirname(pluginRoot)))
|
||||||
|
: null,
|
||||||
|
].filter((root): root is string => Boolean(root));
|
||||||
|
return [...new Set(roots.map((root) => pathApi.join(root, "node_modules", ".bin", commandName)))];
|
||||||
|
}
|
||||||
|
|
||||||
|
function isDistExtensionRoot(pluginRoot: string, platform: NodeJS.Platform): boolean {
|
||||||
|
const pathApi = pathForPlatform(platform);
|
||||||
|
const extensionsDir = pathApi.dirname(pluginRoot);
|
||||||
|
const distDir = pathApi.dirname(extensionsDir);
|
||||||
|
return (
|
||||||
|
pathApi.basename(extensionsDir) === "extensions" &&
|
||||||
|
(pathApi.basename(distDir) === "dist" || pathApi.basename(distDir) === "dist-runtime")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function pathForPlatform(platform: NodeJS.Platform): typeof path {
|
||||||
|
return platform === "win32" ? path.win32 : path.posix;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function findManagedCodexAppServerCommandPath(params: {
|
||||||
|
candidateCommandPaths: readonly string[];
|
||||||
|
pathExists: (filePath: string, platform: NodeJS.Platform) => Promise<boolean>;
|
||||||
|
platform: NodeJS.Platform;
|
||||||
|
}): Promise<string> {
|
||||||
|
for (const commandPath of params.candidateCommandPaths) {
|
||||||
|
if (await params.pathExists(commandPath, params.platform)) {
|
||||||
|
return commandPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(
|
||||||
|
[
|
||||||
|
`Managed Codex app-server binary was not found for ${MANAGED_CODEX_APP_SERVER_PACKAGE}.`,
|
||||||
|
"Run OpenClaw with bundled plugin runtime dependencies enabled, or run pnpm install in a source checkout.",
|
||||||
|
"Set plugins.entries.codex.config.appServer.command or OPENCLAW_CODEX_APP_SERVER_BIN to use a custom Codex binary.",
|
||||||
|
].join(" "),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function commandPathExists(filePath: string, platform: NodeJS.Platform): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
await access(filePath, platform === "win32" ? fsConstants.F_OK : fsConstants.X_OK);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,10 +7,13 @@ const mocks = vi.hoisted(() => {
|
|||||||
applyAuthProfile: vi.fn(async () => undefined),
|
applyAuthProfile: vi.fn(async () => undefined),
|
||||||
startOptions: vi.fn(async ({ startOptions }) => startOptions),
|
startOptions: vi.fn(async ({ startOptions }) => startOptions),
|
||||||
};
|
};
|
||||||
|
const managedBinary = {
|
||||||
|
startOptions: vi.fn(async (startOptions) => startOptions),
|
||||||
|
};
|
||||||
const providerAuth = {
|
const providerAuth = {
|
||||||
agentDir: vi.fn(() => "/tmp/openclaw-agent"),
|
agentDir: vi.fn(() => "/tmp/openclaw-agent"),
|
||||||
};
|
};
|
||||||
return { authBridge, providerAuth };
|
return { authBridge, managedBinary, providerAuth };
|
||||||
});
|
});
|
||||||
|
|
||||||
vi.mock("./auth-bridge.js", () => ({
|
vi.mock("./auth-bridge.js", () => ({
|
||||||
@@ -18,6 +21,10 @@ vi.mock("./auth-bridge.js", () => ({
|
|||||||
bridgeCodexAppServerStartOptions: mocks.authBridge.startOptions,
|
bridgeCodexAppServerStartOptions: mocks.authBridge.startOptions,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
vi.mock("./managed-binary.js", () => ({
|
||||||
|
resolveManagedCodexAppServerStartOptions: mocks.managedBinary.startOptions,
|
||||||
|
}));
|
||||||
|
|
||||||
vi.mock("openclaw/plugin-sdk/provider-auth", () => ({
|
vi.mock("openclaw/plugin-sdk/provider-auth", () => ({
|
||||||
resolveOpenClawAgentDir: mocks.providerAuth.agentDir,
|
resolveOpenClawAgentDir: mocks.providerAuth.agentDir,
|
||||||
}));
|
}));
|
||||||
@@ -38,6 +45,8 @@ describe("listCodexAppServerModels", () => {
|
|||||||
vi.restoreAllMocks();
|
vi.restoreAllMocks();
|
||||||
mocks.authBridge.applyAuthProfile.mockClear();
|
mocks.authBridge.applyAuthProfile.mockClear();
|
||||||
mocks.authBridge.startOptions.mockClear();
|
mocks.authBridge.startOptions.mockClear();
|
||||||
|
mocks.managedBinary.startOptions.mockClear();
|
||||||
|
mocks.managedBinary.startOptions.mockImplementation(async (startOptions) => startOptions);
|
||||||
mocks.providerAuth.agentDir.mockClear();
|
mocks.providerAuth.agentDir.mockClear();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import { createClientHarness } from "./test-support.js";
|
|||||||
const mocks = vi.hoisted(() => ({
|
const mocks = vi.hoisted(() => ({
|
||||||
bridgeCodexAppServerStartOptions: vi.fn(async ({ startOptions }) => startOptions),
|
bridgeCodexAppServerStartOptions: vi.fn(async ({ startOptions }) => startOptions),
|
||||||
applyCodexAppServerAuthProfile: vi.fn(async () => undefined),
|
applyCodexAppServerAuthProfile: vi.fn(async () => undefined),
|
||||||
|
resolveManagedCodexAppServerStartOptions: vi.fn(async (startOptions) => startOptions),
|
||||||
|
embeddedAgentLog: { debug: vi.fn(), warn: vi.fn() },
|
||||||
resolveOpenClawAgentDir: vi.fn(() => "/tmp/openclaw-agent"),
|
resolveOpenClawAgentDir: vi.fn(() => "/tmp/openclaw-agent"),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -14,6 +16,15 @@ vi.mock("./auth-bridge.js", () => ({
|
|||||||
bridgeCodexAppServerStartOptions: mocks.bridgeCodexAppServerStartOptions,
|
bridgeCodexAppServerStartOptions: mocks.bridgeCodexAppServerStartOptions,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
vi.mock("./managed-binary.js", () => ({
|
||||||
|
resolveManagedCodexAppServerStartOptions: mocks.resolveManagedCodexAppServerStartOptions,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("openclaw/plugin-sdk/agent-harness-runtime", () => ({
|
||||||
|
embeddedAgentLog: mocks.embeddedAgentLog,
|
||||||
|
OPENCLAW_VERSION: "test",
|
||||||
|
}));
|
||||||
|
|
||||||
vi.mock("openclaw/plugin-sdk/provider-auth", () => ({
|
vi.mock("openclaw/plugin-sdk/provider-auth", () => ({
|
||||||
resolveOpenClawAgentDir: mocks.resolveOpenClawAgentDir,
|
resolveOpenClawAgentDir: mocks.resolveOpenClawAgentDir,
|
||||||
}));
|
}));
|
||||||
@@ -54,6 +65,12 @@ describe("shared Codex app-server client", () => {
|
|||||||
vi.restoreAllMocks();
|
vi.restoreAllMocks();
|
||||||
mocks.bridgeCodexAppServerStartOptions.mockClear();
|
mocks.bridgeCodexAppServerStartOptions.mockClear();
|
||||||
mocks.applyCodexAppServerAuthProfile.mockClear();
|
mocks.applyCodexAppServerAuthProfile.mockClear();
|
||||||
|
mocks.resolveManagedCodexAppServerStartOptions.mockClear();
|
||||||
|
mocks.resolveManagedCodexAppServerStartOptions.mockImplementation(
|
||||||
|
async (startOptions) => startOptions,
|
||||||
|
);
|
||||||
|
mocks.embeddedAgentLog.debug.mockClear();
|
||||||
|
mocks.embeddedAgentLog.warn.mockClear();
|
||||||
mocks.resolveOpenClawAgentDir.mockClear();
|
mocks.resolveOpenClawAgentDir.mockClear();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -128,6 +145,42 @@ describe("shared Codex app-server client", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("resolves the managed binary before bridging and spawning the shared client", async () => {
|
||||||
|
const harness = createClientHarness();
|
||||||
|
const startSpy = vi.spyOn(CodexAppServerClient, "start").mockReturnValue(harness.client);
|
||||||
|
mocks.resolveManagedCodexAppServerStartOptions.mockImplementationOnce(async (startOptions) => ({
|
||||||
|
...startOptions,
|
||||||
|
command: "/cache/openclaw/codex",
|
||||||
|
commandSource: "resolved-managed",
|
||||||
|
}));
|
||||||
|
|
||||||
|
const listPromise = listCodexAppServerModels({ timeoutMs: 1000 });
|
||||||
|
await sendInitializeResult(harness, "openclaw/0.125.0 (macOS; test)");
|
||||||
|
await sendEmptyModelList(harness);
|
||||||
|
|
||||||
|
await expect(listPromise).resolves.toEqual({ models: [] });
|
||||||
|
expect(mocks.resolveManagedCodexAppServerStartOptions).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
command: "codex",
|
||||||
|
commandSource: "managed",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
expect(mocks.bridgeCodexAppServerStartOptions).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
startOptions: expect.objectContaining({
|
||||||
|
command: "/cache/openclaw/codex",
|
||||||
|
commandSource: "resolved-managed",
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
expect(startSpy).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
command: "/cache/openclaw/codex",
|
||||||
|
commandSource: "resolved-managed",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it("restarts the shared client when the bridged auth token changes", async () => {
|
it("restarts the shared client when the bridged auth token changes", async () => {
|
||||||
const first = createClientHarness();
|
const first = createClientHarness();
|
||||||
const second = createClientHarness();
|
const second = createClientHarness();
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
resolveCodexAppServerRuntimeOptions,
|
resolveCodexAppServerRuntimeOptions,
|
||||||
type CodexAppServerStartOptions,
|
type CodexAppServerStartOptions,
|
||||||
} from "./config.js";
|
} from "./config.js";
|
||||||
|
import { resolveManagedCodexAppServerStartOptions } from "./managed-binary.js";
|
||||||
import { withTimeout } from "./timeout.js";
|
import { withTimeout } from "./timeout.js";
|
||||||
|
|
||||||
type SharedCodexAppServerClientState = {
|
type SharedCodexAppServerClientState = {
|
||||||
@@ -30,9 +31,13 @@ export async function getSharedCodexAppServerClient(options?: {
|
|||||||
authProfileId?: string;
|
authProfileId?: string;
|
||||||
}): Promise<CodexAppServerClient> {
|
}): Promise<CodexAppServerClient> {
|
||||||
const state = getSharedCodexAppServerClientState();
|
const state = getSharedCodexAppServerClientState();
|
||||||
|
const agentDir = resolveOpenClawAgentDir();
|
||||||
|
const requestedStartOptions =
|
||||||
|
options?.startOptions ?? resolveCodexAppServerRuntimeOptions().start;
|
||||||
|
const managedStartOptions = await resolveManagedCodexAppServerStartOptions(requestedStartOptions);
|
||||||
const startOptions = await bridgeCodexAppServerStartOptions({
|
const startOptions = await bridgeCodexAppServerStartOptions({
|
||||||
startOptions: options?.startOptions ?? resolveCodexAppServerRuntimeOptions().start,
|
startOptions: managedStartOptions,
|
||||||
agentDir: resolveOpenClawAgentDir(),
|
agentDir,
|
||||||
authProfileId: options?.authProfileId,
|
authProfileId: options?.authProfileId,
|
||||||
});
|
});
|
||||||
const key = codexAppServerStartOptionsKey(startOptions, {
|
const key = codexAppServerStartOptionsKey(startOptions, {
|
||||||
@@ -52,7 +57,7 @@ export async function getSharedCodexAppServerClient(options?: {
|
|||||||
await client.initialize();
|
await client.initialize();
|
||||||
await applyCodexAppServerAuthProfile({
|
await applyCodexAppServerAuthProfile({
|
||||||
client,
|
client,
|
||||||
agentDir: resolveOpenClawAgentDir(),
|
agentDir,
|
||||||
authProfileId: options?.authProfileId,
|
authProfileId: options?.authProfileId,
|
||||||
});
|
});
|
||||||
return client;
|
return client;
|
||||||
@@ -82,9 +87,13 @@ export async function createIsolatedCodexAppServerClient(options?: {
|
|||||||
timeoutMs?: number;
|
timeoutMs?: number;
|
||||||
authProfileId?: string;
|
authProfileId?: string;
|
||||||
}): Promise<CodexAppServerClient> {
|
}): Promise<CodexAppServerClient> {
|
||||||
|
const agentDir = resolveOpenClawAgentDir();
|
||||||
|
const requestedStartOptions =
|
||||||
|
options?.startOptions ?? resolveCodexAppServerRuntimeOptions().start;
|
||||||
|
const managedStartOptions = await resolveManagedCodexAppServerStartOptions(requestedStartOptions);
|
||||||
const startOptions = await bridgeCodexAppServerStartOptions({
|
const startOptions = await bridgeCodexAppServerStartOptions({
|
||||||
startOptions: options?.startOptions ?? resolveCodexAppServerRuntimeOptions().start,
|
startOptions: managedStartOptions,
|
||||||
agentDir: resolveOpenClawAgentDir(),
|
agentDir,
|
||||||
authProfileId: options?.authProfileId,
|
authProfileId: options?.authProfileId,
|
||||||
});
|
});
|
||||||
const client = CodexAppServerClient.start(startOptions);
|
const client = CodexAppServerClient.start(startOptions);
|
||||||
@@ -93,7 +102,7 @@ export async function createIsolatedCodexAppServerClient(options?: {
|
|||||||
await withTimeout(initialize, options?.timeoutMs ?? 0, "codex app-server initialize timed out");
|
await withTimeout(initialize, options?.timeoutMs ?? 0, "codex app-server initialize timed out");
|
||||||
await applyCodexAppServerAuthProfile({
|
await applyCodexAppServerAuthProfile({
|
||||||
client,
|
client,
|
||||||
agentDir: resolveOpenClawAgentDir(),
|
agentDir,
|
||||||
authProfileId: options?.authProfileId,
|
authProfileId: options?.authProfileId,
|
||||||
});
|
});
|
||||||
return client;
|
return client;
|
||||||
|
|||||||
@@ -44,6 +44,22 @@ describe("resolveCodexAppServerSpawnInvocation", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("requires managed Codex commands to be resolved before spawn", () => {
|
||||||
|
expect(() =>
|
||||||
|
resolveCodexAppServerSpawnInvocation(
|
||||||
|
{
|
||||||
|
...startOptions("codex"),
|
||||||
|
commandSource: "managed",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
platform: "darwin",
|
||||||
|
env: {},
|
||||||
|
execPath: "/usr/local/bin/node",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
).toThrow("must be resolved before spawn");
|
||||||
|
});
|
||||||
|
|
||||||
it("resolves Windows npm .cmd Codex shims through Node instead of raw spawn", async () => {
|
it("resolves Windows npm .cmd Codex shims through Node instead of raw spawn", async () => {
|
||||||
const binDir = await createTempDir();
|
const binDir = await createTempDir();
|
||||||
const entryPath = path.join(binDir, "node_modules", "@openai", "codex", "bin", "codex.js");
|
const entryPath = path.join(binDir, "node_modules", "@openai", "codex", "bin", "codex.js");
|
||||||
|
|||||||
@@ -22,6 +22,9 @@ export function resolveCodexAppServerSpawnInvocation(
|
|||||||
options: CodexAppServerStartOptions,
|
options: CodexAppServerStartOptions,
|
||||||
runtime: CodexAppServerSpawnRuntime = DEFAULT_SPAWN_RUNTIME,
|
runtime: CodexAppServerSpawnRuntime = DEFAULT_SPAWN_RUNTIME,
|
||||||
): { command: string; args: string[]; shell?: boolean; windowsHide?: boolean } {
|
): { command: string; args: string[]; shell?: boolean; windowsHide?: boolean } {
|
||||||
|
if (options.commandSource === "managed") {
|
||||||
|
throw new Error("Managed Codex app-server start options must be resolved before spawn.");
|
||||||
|
}
|
||||||
const program = resolveWindowsSpawnProgram({
|
const program = resolveWindowsSpawnProgram({
|
||||||
command: options.command,
|
command: options.command,
|
||||||
platform: runtime.platform,
|
platform: runtime.platform,
|
||||||
|
|||||||
3
extensions/codex/src/app-server/version.ts
Normal file
3
extensions/codex/src/app-server/version.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export const MIN_CODEX_APP_SERVER_VERSION = "0.125.0";
|
||||||
|
export const MANAGED_CODEX_APP_SERVER_PACKAGE = "@openai/codex";
|
||||||
|
export const MANAGED_CODEX_APP_SERVER_PACKAGE_VERSION = MIN_CODEX_APP_SERVER_VERSION;
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { MANAGED_CODEX_APP_SERVER_PACKAGE_VERSION } from "./app-server/version.js";
|
||||||
|
|
||||||
type CodexPackageManifest = {
|
type CodexPackageManifest = {
|
||||||
dependencies?: Record<string, string>;
|
dependencies?: Record<string, string>;
|
||||||
@@ -17,6 +18,9 @@ describe("codex package manifest", () => {
|
|||||||
) as CodexPackageManifest;
|
) as CodexPackageManifest;
|
||||||
|
|
||||||
expect(packageJson.dependencies?.["@mariozechner/pi-coding-agent"]).toBeDefined();
|
expect(packageJson.dependencies?.["@mariozechner/pi-coding-agent"]).toBeDefined();
|
||||||
|
expect(packageJson.dependencies?.["@openai/codex"]).toBe(
|
||||||
|
MANAGED_CODEX_APP_SERVER_PACKAGE_VERSION,
|
||||||
|
);
|
||||||
expect(packageJson.openclaw?.bundle?.stageRuntimeDependencies).toBe(true);
|
expect(packageJson.openclaw?.bundle?.stageRuntimeDependencies).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1215,7 +1215,7 @@ describe("memory-core dreaming phases", () => {
|
|||||||
"utf-8",
|
"utf-8",
|
||||||
);
|
);
|
||||||
await fs.writeFile(
|
await fs.writeFile(
|
||||||
path.join(sessionsDir, "ordinary.checkpoint.abc123.jsonl"),
|
path.join(sessionsDir, "ordinary.checkpoint.11111111-1111-4111-8111-111111111111.jsonl"),
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
type: "message",
|
type: "message",
|
||||||
message: {
|
message: {
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ describe("listSessionFilesForAgent", () => {
|
|||||||
"active.jsonl.deleted.2026-02-16T22-27-33.000Z",
|
"active.jsonl.deleted.2026-02-16T22-27-33.000Z",
|
||||||
];
|
];
|
||||||
const excluded = ["active.jsonl.bak.2026-02-16T22-28-33.000Z", "sessions.json", "notes.md"];
|
const excluded = ["active.jsonl.bak.2026-02-16T22-28-33.000Z", "sessions.json", "notes.md"];
|
||||||
|
excluded.push("active.checkpoint.11111111-1111-4111-8111-111111111111.jsonl");
|
||||||
|
|
||||||
for (const fileName of [...included, ...excluded]) {
|
for (const fileName of [...included, ...excluded]) {
|
||||||
fsSync.writeFileSync(path.join(sessionsDir, fileName), "");
|
fsSync.writeFileSync(path.join(sessionsDir, fileName), "");
|
||||||
@@ -115,6 +116,30 @@ describe("buildSessionEntry", () => {
|
|||||||
expect(entry!.lineMap).toEqual([]);
|
expect(entry!.lineMap).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("skips deleted and checkpoint transcripts for dreaming ingestion", async () => {
|
||||||
|
const deletedPath = path.join(tmpDir, "ordinary.jsonl.deleted.2026-02-16T22-27-33.000Z");
|
||||||
|
const checkpointPath = path.join(
|
||||||
|
tmpDir,
|
||||||
|
"ordinary.checkpoint.11111111-1111-4111-8111-111111111111.jsonl",
|
||||||
|
);
|
||||||
|
const content = JSON.stringify({
|
||||||
|
type: "message",
|
||||||
|
message: { role: "user", content: "This should never reach the dreaming corpus." },
|
||||||
|
});
|
||||||
|
fsSync.writeFileSync(deletedPath, content);
|
||||||
|
fsSync.writeFileSync(checkpointPath, content);
|
||||||
|
|
||||||
|
const deletedEntry = await buildSessionEntry(deletedPath);
|
||||||
|
const checkpointEntry = await buildSessionEntry(checkpointPath);
|
||||||
|
|
||||||
|
expect(deletedEntry).not.toBeNull();
|
||||||
|
expect(deletedEntry?.content).toBe("");
|
||||||
|
expect(deletedEntry?.lineMap).toEqual([]);
|
||||||
|
expect(checkpointEntry).not.toBeNull();
|
||||||
|
expect(checkpointEntry?.content).toBe("");
|
||||||
|
expect(checkpointEntry?.lineMap).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
it("skips blank lines and invalid JSON without breaking lineMap", async () => {
|
it("skips blank lines and invalid JSON without breaking lineMap", async () => {
|
||||||
const jsonlLines = [
|
const jsonlLines = [
|
||||||
"",
|
"",
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
import fs from "node:fs/promises";
|
import fs from "node:fs/promises";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { stripInboundMetadata } from "../../../../src/auto-reply/reply/strip-inbound-meta.js";
|
import { stripInboundMetadata } from "../../../../src/auto-reply/reply/strip-inbound-meta.js";
|
||||||
import { isUsageCountedSessionTranscriptFileName } from "../../../../src/config/sessions/artifacts.js";
|
import {
|
||||||
|
isCompactionCheckpointTranscriptFileName,
|
||||||
|
isSessionArchiveArtifactName,
|
||||||
|
isUsageCountedSessionTranscriptFileName,
|
||||||
|
} from "../../../../src/config/sessions/artifacts.js";
|
||||||
import { resolveSessionTranscriptsDirForAgent } from "../../../../src/config/sessions/paths.js";
|
import { resolveSessionTranscriptsDirForAgent } from "../../../../src/config/sessions/paths.js";
|
||||||
import { redactSensitiveText } from "../../../../src/logging/redact.js";
|
import { redactSensitiveText } from "../../../../src/logging/redact.js";
|
||||||
import { hashText } from "./hash.js";
|
import { hashText } from "./hash.js";
|
||||||
@@ -41,6 +45,13 @@ function isDreamingNarrativeBootstrapRecord(record: unknown): boolean {
|
|||||||
return typeof runId === "string" && runId.startsWith("dreaming-narrative-");
|
return typeof runId === "string" && runId.startsWith("dreaming-narrative-");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function shouldSkipTranscriptFileForDreaming(absPath: string): boolean {
|
||||||
|
const fileName = path.basename(absPath);
|
||||||
|
return (
|
||||||
|
isSessionArchiveArtifactName(fileName) || isCompactionCheckpointTranscriptFileName(fileName)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export async function listSessionFilesForAgent(agentId: string): Promise<string[]> {
|
export async function listSessionFilesForAgent(agentId: string): Promise<string[]> {
|
||||||
const dir = resolveSessionTranscriptsDirForAgent(agentId);
|
const dir = resolveSessionTranscriptsDirForAgent(agentId);
|
||||||
try {
|
try {
|
||||||
@@ -120,6 +131,18 @@ export function extractSessionText(
|
|||||||
export async function buildSessionEntry(absPath: string): Promise<SessionFileEntry | null> {
|
export async function buildSessionEntry(absPath: string): Promise<SessionFileEntry | null> {
|
||||||
try {
|
try {
|
||||||
const stat = await fs.stat(absPath);
|
const stat = await fs.stat(absPath);
|
||||||
|
if (shouldSkipTranscriptFileForDreaming(absPath)) {
|
||||||
|
return {
|
||||||
|
path: sessionPathForFile(absPath),
|
||||||
|
absPath,
|
||||||
|
mtimeMs: stat.mtimeMs,
|
||||||
|
size: stat.size,
|
||||||
|
hash: hashText("\n\n"),
|
||||||
|
content: "",
|
||||||
|
lineMap: [],
|
||||||
|
generatedByDreamingNarrative: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
const raw = await fs.readFile(absPath, "utf-8");
|
const raw = await fs.readFile(absPath, "utf-8");
|
||||||
const lines = raw.split("\n");
|
const lines = raw.split("\n");
|
||||||
const collected: string[] = [];
|
const collected: string[] = [];
|
||||||
|
|||||||
71
pnpm-lock.yaml
generated
71
pnpm-lock.yaml
generated
@@ -372,6 +372,9 @@ importers:
|
|||||||
'@mariozechner/pi-coding-agent':
|
'@mariozechner/pi-coding-agent':
|
||||||
specifier: 0.70.2
|
specifier: 0.70.2
|
||||||
version: 0.70.2(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(ws@8.20.0)(zod@4.3.6)
|
version: 0.70.2(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(ws@8.20.0)(zod@4.3.6)
|
||||||
|
'@openai/codex':
|
||||||
|
specifier: 0.125.0
|
||||||
|
version: 0.125.0
|
||||||
ajv:
|
ajv:
|
||||||
specifier: ^8.18.0
|
specifier: ^8.18.0
|
||||||
version: 8.18.0
|
version: 8.18.0
|
||||||
@@ -2899,6 +2902,47 @@ packages:
|
|||||||
resolution: {integrity: sha512-tlc/FcYIv5i8RYsl2iDil4A0gOihaas1R5jPcIC4Zw3GhjKsVilw90aHcVlhZPTBLGBzd379S+VcnsDjd9ChiA==}
|
resolution: {integrity: sha512-tlc/FcYIv5i8RYsl2iDil4A0gOihaas1R5jPcIC4Zw3GhjKsVilw90aHcVlhZPTBLGBzd379S+VcnsDjd9ChiA==}
|
||||||
engines: {node: '>=12.4.0'}
|
engines: {node: '>=12.4.0'}
|
||||||
|
|
||||||
|
'@openai/codex@0.125.0':
|
||||||
|
resolution: {integrity: sha512-GiE9wlgL95u/5BRirY5d3EaRLU1tu7Y1R09R8lCHHVmcQdSmhS809FdPDWH3gIYHS7ZriAPqXwJ3aLA0WKl40Q==}
|
||||||
|
engines: {node: '>=16'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
'@openai/codex@0.125.0-darwin-arm64':
|
||||||
|
resolution: {integrity: sha512-Gn2fHiSO0XgyHp1OSd5DWUTm66Bv9UEuipW5pVEj1E+hWZCOrdqnYttllKFWtRGj5yiKefNX3JIxONgh/ZwlOQ==}
|
||||||
|
engines: {node: '>=16'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@openai/codex@0.125.0-darwin-x64':
|
||||||
|
resolution: {integrity: sha512-TZ5Lek2X/UXTI9LXFxzarvQaJeuTrqVh4POc7soO/8RclVnCxADnCf15sivxLd5eiFW4t0myGoeVoM4lciRiRg==}
|
||||||
|
engines: {node: '>=16'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@openai/codex@0.125.0-linux-arm64':
|
||||||
|
resolution: {integrity: sha512-pPnJoJD6rZ2Iin0zNt/up36bO2/EOp2B+1/rPHu/lSq3PJbT3Fmnfut2kJy5LylXb7bGA2XQbtqOogZzIbnlkA==}
|
||||||
|
engines: {node: '>=16'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@openai/codex@0.125.0-linux-x64':
|
||||||
|
resolution: {integrity: sha512-K2NTTEeBpz/G+N2x17UGWfauRt3So+ir4f+U/60l5PPnYEJB/w3YZrlXo2G9og8Dm9BqtoBAjoPV74sRv9tWWQ==}
|
||||||
|
engines: {node: '>=16'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@openai/codex@0.125.0-win32-arm64':
|
||||||
|
resolution: {integrity: sha512-zxoUakw9oIHIFrAyk400XkkLBJFA6nOym0NDq6sQ/jhdcYraKqNSRCII2nsBwZHk+/4zgUvuk52iuutgysY/rQ==}
|
||||||
|
engines: {node: '>=16'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
'@openai/codex@0.125.0-win32-x64':
|
||||||
|
resolution: {integrity: sha512-ofpOK+OWH5QFuUZ9pTM0d/PcXUXiIP5z5DpRcE9MlucJoyOl4Zy4Nu3NcuHF4YzCkZMQb6x3j0tjDEPHKqNQzw==}
|
||||||
|
engines: {node: '>=16'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
'@opentelemetry/api-logs@0.215.0':
|
'@opentelemetry/api-logs@0.215.0':
|
||||||
resolution: {integrity: sha512-xrFlqhdhUyO8wSRn6DjE0145/HPWSJ5Nm0C7vWua6TdL/FSEAZvEyvdsa9CRXuxo9ebb7j/NEPhEcO62IJ0qUA==}
|
resolution: {integrity: sha512-xrFlqhdhUyO8wSRn6DjE0145/HPWSJ5Nm0C7vWua6TdL/FSEAZvEyvdsa9CRXuxo9ebb7j/NEPhEcO62IJ0qUA==}
|
||||||
engines: {node: '>=8.0.0'}
|
engines: {node: '>=8.0.0'}
|
||||||
@@ -9459,6 +9503,33 @@ snapshots:
|
|||||||
|
|
||||||
'@nolyfill/domexception@1.0.28': {}
|
'@nolyfill/domexception@1.0.28': {}
|
||||||
|
|
||||||
|
'@openai/codex@0.125.0':
|
||||||
|
optionalDependencies:
|
||||||
|
'@openai/codex-darwin-arm64': '@openai/codex@0.125.0-darwin-arm64'
|
||||||
|
'@openai/codex-darwin-x64': '@openai/codex@0.125.0-darwin-x64'
|
||||||
|
'@openai/codex-linux-arm64': '@openai/codex@0.125.0-linux-arm64'
|
||||||
|
'@openai/codex-linux-x64': '@openai/codex@0.125.0-linux-x64'
|
||||||
|
'@openai/codex-win32-arm64': '@openai/codex@0.125.0-win32-arm64'
|
||||||
|
'@openai/codex-win32-x64': '@openai/codex@0.125.0-win32-x64'
|
||||||
|
|
||||||
|
'@openai/codex@0.125.0-darwin-arm64':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@openai/codex@0.125.0-darwin-x64':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@openai/codex@0.125.0-linux-arm64':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@openai/codex@0.125.0-linux-x64':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@openai/codex@0.125.0-win32-arm64':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@openai/codex@0.125.0-win32-x64':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@opentelemetry/api-logs@0.215.0':
|
'@opentelemetry/api-logs@0.215.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@opentelemetry/api': 1.9.1
|
'@opentelemetry/api': 1.9.1
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ minimumReleaseAgeExclude:
|
|||||||
- "@cloudflare/workers-types"
|
- "@cloudflare/workers-types"
|
||||||
- "@hono/node-server"
|
- "@hono/node-server"
|
||||||
- "@mariozechner/*"
|
- "@mariozechner/*"
|
||||||
|
- "@openai/codex"
|
||||||
|
- "@openai/codex-*"
|
||||||
- "@typescript/native-preview*"
|
- "@typescript/native-preview*"
|
||||||
- "@types/node"
|
- "@types/node"
|
||||||
- "@rolldown/*"
|
- "@rolldown/*"
|
||||||
|
|||||||
@@ -7,6 +7,22 @@ function normalizeHostPath(value: string): string {
|
|||||||
return path.normalize(path.resolve(value));
|
return path.normalize(path.resolve(value));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createModelRegistryStub(resolve: (provider: string, modelId: string) => unknown): {
|
||||||
|
calls: Array<[string, string]>;
|
||||||
|
registry: { find: (provider: string, modelId: string) => unknown };
|
||||||
|
} {
|
||||||
|
const calls: Array<[string, string]> = [];
|
||||||
|
return {
|
||||||
|
calls,
|
||||||
|
registry: {
|
||||||
|
find(provider, modelId) {
|
||||||
|
calls.push([provider, modelId]);
|
||||||
|
return resolve(provider, modelId);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
describe("resolveMediaToolLocalRoots", () => {
|
describe("resolveMediaToolLocalRoots", () => {
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
vi.unstubAllEnvs();
|
vi.unstubAllEnvs();
|
||||||
@@ -39,24 +55,24 @@ describe("resolveMediaToolLocalRoots", () => {
|
|||||||
describe("resolveModelFromRegistry", () => {
|
describe("resolveModelFromRegistry", () => {
|
||||||
it("normalizes provider and model refs before registry lookup", () => {
|
it("normalizes provider and model refs before registry lookup", () => {
|
||||||
const foundModel = { provider: "ollama", id: "qwen3.5:397b-cloud" };
|
const foundModel = { provider: "ollama", id: "qwen3.5:397b-cloud" };
|
||||||
const find = vi.fn(() => foundModel);
|
const { calls, registry } = createModelRegistryStub(() => foundModel);
|
||||||
|
|
||||||
const result = resolveModelFromRegistry({
|
const result = resolveModelFromRegistry({
|
||||||
modelRegistry: { find },
|
modelRegistry: registry,
|
||||||
provider: " OLLAMA ",
|
provider: " OLLAMA ",
|
||||||
modelId: " qwen3.5:397b-cloud ",
|
modelId: " qwen3.5:397b-cloud ",
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(find).toHaveBeenCalledWith("ollama", "qwen3.5:397b-cloud");
|
expect(calls).toEqual([["ollama", "qwen3.5:397b-cloud"]]);
|
||||||
expect(result).toBe(foundModel);
|
expect(result).toBe(foundModel);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("reports the normalized ref when the registry lookup misses", () => {
|
it("reports the normalized ref when the registry lookup misses", () => {
|
||||||
const find = vi.fn(() => null);
|
const { registry } = createModelRegistryStub(() => null);
|
||||||
|
|
||||||
expect(() =>
|
expect(() =>
|
||||||
resolveModelFromRegistry({
|
resolveModelFromRegistry({
|
||||||
modelRegistry: { find },
|
modelRegistry: registry,
|
||||||
provider: " OLLAMA ",
|
provider: " OLLAMA ",
|
||||||
modelId: " qwen3.5:397b-cloud ",
|
modelId: " qwen3.5:397b-cloud ",
|
||||||
}),
|
}),
|
||||||
@@ -65,15 +81,17 @@ describe("resolveModelFromRegistry", () => {
|
|||||||
|
|
||||||
it("falls back to provider-prefixed custom model IDs", () => {
|
it("falls back to provider-prefixed custom model IDs", () => {
|
||||||
const foundModel = { provider: "kimchi", id: "kimchi/claude-opus-4-6" };
|
const foundModel = { provider: "kimchi", id: "kimchi/claude-opus-4-6" };
|
||||||
const find = vi.fn().mockReturnValueOnce(null).mockReturnValueOnce(foundModel);
|
const { calls, registry } = createModelRegistryStub((_, modelId) =>
|
||||||
|
modelId === "kimchi/claude-opus-4-6" ? foundModel : null,
|
||||||
|
);
|
||||||
|
|
||||||
const result = resolveModelFromRegistry({
|
const result = resolveModelFromRegistry({
|
||||||
modelRegistry: { find },
|
modelRegistry: registry,
|
||||||
provider: "kimchi",
|
provider: "kimchi",
|
||||||
modelId: "claude-opus-4-6",
|
modelId: "claude-opus-4-6",
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(find.mock.calls).toEqual([
|
expect(calls).toEqual([
|
||||||
["kimchi", "claude-opus-4-6"],
|
["kimchi", "claude-opus-4-6"],
|
||||||
["kimchi", "kimchi/claude-opus-4-6"],
|
["kimchi", "kimchi/claude-opus-4-6"],
|
||||||
]);
|
]);
|
||||||
|
|||||||
Reference in New Issue
Block a user