Orca mirrors LazyCodex agent registrations into its runtime CODEX_HOME with absolute config_file paths. LazyCodex bootstrap also materializes the same roles under the runtime agents directory, so Codex reports 12 malformed same-layer duplicate role definitions on every launch.
The warning list exactly matched the intersection between Orca's explicit agent registrations and LazyCodex-managed runtime TOMLs. A configured-only role without a runtime-local copy did not warn.
Filter foreign explicit registrations before staging, remove stale LazyCodex-managed runtime copies for those roles, and let the existing explicit config remain authoritative. The installed-agent manifest now contains only roles that were actually materialized. This is idempotent and preserves the normal no-foreign-registration path.
diff --git a/plugins/omo/components/bootstrap/dist/cli.js b/plugins/omo/components/bootstrap/dist/cli.js
index 8a81451..1d1bb2c 100755
--- a/plugins/omo/components/bootstrap/dist/cli.js
+++ b/plugins/omo/components/bootstrap/dist/cli.js
@@ -3543,7 +3543,10 @@ async function linkBundledAgentsStep(options) {
const agentsTarget = join21(options.codexHome, "agents");
try {
const stageRoot = join21(options.pluginData, "bootstrap", "agents-stage");
- await stageBundledAgents(options.pluginRoot, stageRoot);
+ const existingConfig = await readConfigIfPresent(join21(options.codexHome, "config.toml"));
+ const foreignAgentFiles = await stageBundledAgents(options.pluginRoot, stageRoot, existingConfig);
+ for (const agentFile of foreignAgentFiles)
+ await rm10(join21(agentsTarget, agentFile), { force: true });
const preservedReasoning = await capturePreservedAgentReasoning({ codexHome: options.codexHome });
const preservedServiceTier = await capturePreservedAgentServiceTier({ codexHome: options.codexHome });
const linked = await linkCachedPluginAgents({
@@ -3567,9 +3570,10 @@ async function linkBundledAgentsStep(options) {
};
}
}
-async function stageBundledAgents(pluginRoot, stageRoot) {
+async function stageBundledAgents(pluginRoot, stageRoot, existingConfig) {
await rm10(stageRoot, { force: true, recursive: true });
await mkdir7(stageRoot, { recursive: true });
+ const foreignAgentFiles = [];
const componentsRoot = join21(pluginRoot, "components");
for (const componentName of await directoryNames(componentsRoot)) {
const agentsDir = join21(componentsRoot, componentName, "agents");
@@ -3579,9 +3583,15 @@ async function stageBundledAgents(pluginRoot, stageRoot) {
const stagedAgentsDir = join21(stageRoot, "components", componentName, "agents");
await mkdir7(stagedAgentsDir, { recursive: true });
for (const agentFile of agentFiles) {
+ const agentConfig = { configFile: `./agents/${agentFile}`, name: agentNameFromToml3(agentFile) };
+ if (hasForeignAgentRegistration(existingConfig, agentConfig)) {
+ foreignAgentFiles.push(agentFile);
+ continue;
+ }
await copyFile2(join21(agentsDir, agentFile), join21(stagedAgentsDir, agentFile));
}
}
+ return foreignAgentFiles;
}
async function updateConfigStep(options, inputs, degraded) {
const configPath = join21(options.codexHome, "config.toml");
diff --git a/plugins/omo/components/bootstrap/src/agent-staging.ts b/plugins/omo/components/bootstrap/src/agent-staging.ts
new file mode 100644
index 0000000..b97b616
--- /dev/null
+++ b/plugins/omo/components/bootstrap/src/agent-staging.ts
@@ -0,0 +1,56 @@
+import { copyFile, mkdir, readdir, rm } from "node:fs/promises";
+import { join } from "node:path";
+
+import { hasForeignAgentRegistration } from "../../../../src/install/codex-config-agents.ts";
+
+export async function stageBundledAgents(
+ pluginRoot: string,
+ stageRoot: string,
+ existingConfig: string,
+): Promise<readonly string[]> {
+ await rm(stageRoot, { force: true, recursive: true });
+ await mkdir(stageRoot, { recursive: true });
+ const foreignAgentFiles: string[] = [];
+ const componentsRoot = join(pluginRoot, "components");
+ for (const componentName of await directoryNames(componentsRoot)) {
+ const agentsDir = join(componentsRoot, componentName, "agents");
+ const agentFiles = (await fileNames(agentsDir)).filter((name) => name.endsWith(".toml"));
+ if (agentFiles.length === 0) continue;
+ const stagedAgentsDir = join(stageRoot, "components", componentName, "agents");
+ await mkdir(stagedAgentsDir, { recursive: true });
+ for (const agentFile of agentFiles) {
+ const agentConfig = { configFile: `./agents/${agentFile}`, name: agentNameFromToml(agentFile) };
+ if (hasForeignAgentRegistration(existingConfig, agentConfig)) {
+ foreignAgentFiles.push(agentFile);
+ continue;
+ }
+ await copyFile(join(agentsDir, agentFile), join(stagedAgentsDir, agentFile));
+ }
+ }
+ return foreignAgentFiles;
+}
+
+export function agentNameFromToml(fileName: string): string {
+ return fileName.endsWith(".toml") ? fileName.slice(0, -".toml".length) : fileName;
+}
+
+async function directoryNames(root: string): Promise<string[]> {
+ return entryNames(root, (entry) => entry.isDirectory());
+}
+
+async function fileNames(root: string): Promise<string[]> {
+ return entryNames(root, (entry) => entry.isFile());
+}
+
+async function entryNames(root: string, keep: (entry: { isDirectory(): boolean; isFile(): boolean }) => boolean): Promise<string[]> {
+ try {
+ const entries = await readdir(root, { withFileTypes: true });
+ return entries
+ .filter((entry) => keep(entry))
+ .map((entry) => entry.name)
+ .sort();
+ } catch (error) {
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") return [];
+ throw error;
+ }
+}
diff --git a/plugins/omo/components/bootstrap/src/setup.ts b/plugins/omo/components/bootstrap/src/setup.ts
index 1a03421..a343824 100644
--- a/plugins/omo/components/bootstrap/src/setup.ts
+++ b/plugins/omo/components/bootstrap/src/setup.ts
@@ -1,4 +1,4 @@
-import { copyFile, mkdir, readFile, readdir, rm, stat } from "node:fs/promises";
+import { readFile, rm, stat } from "node:fs/promises";
import { join } from "node:path";
// These relative imports resolve at BUILD time in the monorepo; esbuild
@@ -17,6 +17,7 @@ import { trustedHookStatesForPlugin } from "../../../../src/install/codex-hook-t
import { resolveCodexInstallerBinDir } from "../../../../src/install/codex-installer-bin-dir.ts";
import { prepareGitBashForInstall } from "../../../../src/install/git-bash.ts";
import type { CodexAgentConfig, GitBashResolution } from "../../../../src/install/types.ts";
+import { agentNameFromToml, stageBundledAgents } from "./agent-staging.ts";
import { appendBootstrapLog, BOOTSTRAP_DOCTOR_HINT } from "./worker.ts";
import type { BootstrapDegradedEntry, BootstrapStepOutcome } from "./worker.ts";
@@ -91,7 +92,9 @@ async function linkBundledAgentsStep(options: WorkerSetupOptions): Promise<Agent
// first: bootstrap must never persist anything under PLUGIN_ROOT (the
// Codex-managed marketplace cache).
const stageRoot = join(options.pluginData, "bootstrap", "agents-stage");
- await stageBundledAgents(options.pluginRoot, stageRoot);
+ const existingConfig = await readConfigIfPresent(join(options.codexHome, "config.toml"));
+ const foreignAgentFiles = await stageBundledAgents(options.pluginRoot, stageRoot, existingConfig);
+ for (const agentFile of foreignAgentFiles) await rm(join(agentsTarget, agentFile), { force: true });
const preservedReasoning = await capturePreservedAgentReasoning({ codexHome: options.codexHome });
const preservedServiceTier = await capturePreservedAgentServiceTier({ codexHome: options.codexHome });
const linked = await linkCachedPluginAgents({
@@ -118,22 +121,6 @@ async function linkBundledAgentsStep(options: WorkerSetupOptions): Promise<Agent
}
}
-async function stageBundledAgents(pluginRoot: string, stageRoot: string): Promise<void> {
- await rm(stageRoot, { force: true, recursive: true });
- await mkdir(stageRoot, { recursive: true });
- const componentsRoot = join(pluginRoot, "components");
- for (const componentName of await directoryNames(componentsRoot)) {
- const agentsDir = join(componentsRoot, componentName, "agents");
- const agentFiles = (await fileNames(agentsDir)).filter((name) => name.endsWith(".toml"));
- if (agentFiles.length === 0) continue;
- const stagedAgentsDir = join(stageRoot, "components", componentName, "agents");
- await mkdir(stagedAgentsDir, { recursive: true });
- for (const agentFile of agentFiles) {
- await copyFile(join(agentsDir, agentFile), join(stagedAgentsDir, agentFile));
- }
- }
-}
-
async function updateConfigStep(
options: WorkerSetupOptions,
inputs: { agentConfigs: readonly CodexAgentConfig[]; gitBashEnabled: boolean },
@@ -147,7 +134,7 @@ async function updateConfigStep(
// for such a role would collide with the mirrored entry once Codex
// discovers <codexHome>/agents/<name>.toml (two different file paths for
// one role name -> upstream warning), so foreign pre-existing blocks are
- // left untouched; directory discovery still loads the linked toml.
+ // left untouched and their runtime-local copies are excluded from staging.
const existingConfig = await readConfigIfPresent(configPath);
const agentConfigs = inputs.agentConfigs.filter(
(agentConfig) => !hasForeignAgentRegistration(existingConfig, agentConfig),
@@ -268,31 +255,6 @@ async function stampGitBashEnvStep(options: WorkerSetupOptions, degraded: Bootst
}
}
-async function directoryNames(root: string): Promise<string[]> {
- return entryNames(root, (entry) => entry.isDirectory());
-}
-
-async function fileNames(root: string): Promise<string[]> {
- return entryNames(root, (entry) => entry.isFile());
-}
-
-async function entryNames(root: string, keep: (entry: { isDirectory(): boolean; isFile(): boolean }) => boolean): Promise<string[]> {
- try {
- const entries = await readdir(root, { withFileTypes: true });
- return entries
- .filter((entry) => keep(entry))
- .map((entry) => entry.name)
- .sort();
- } catch (error) {
- if (error instanceof Error && "code" in error && error.code === "ENOENT") return [];
- throw error;
- }
-}
-
-function agentNameFromToml(fileName: string): string {
- return fileName.endsWith(".toml") ? fileName.slice(0, -".toml".length) : fileName;
-}
-
function errorMessage(error: unknown): string {
return error instanceof Error ? error.message : String(error);
}
diff --git a/plugins/omo/test/bootstrap-setup.test.mjs b/plugins/omo/test/bootstrap-setup.test.mjs
index 4ef3311..3d8b298 100644
--- a/plugins/omo/test/bootstrap-setup.test.mjs
+++ b/plugins/omo/test/bootstrap-setup.test.mjs
@@ -127,7 +127,7 @@ test("#given a completed first run #when the worker setup runs again #then confi
});
});
-test("#given a config.toml that already declares [agents.explorer] at a different path (Orca mirror) #when the worker setup runs #then no second colliding registration is added for that role", async () => {
+test("#given a config.toml that already declares [agents.explorer] at a different path (Orca mirror) #when the worker setup runs #then the mirrored role is not also linked into the runtime agents directory", async () => {
await withSetupFixture(async (fixture) => {
const orcaMirrorPath = "/orca-mirrored-home/.codex/agents/explorer.toml";
await writeFile(
@@ -149,11 +149,15 @@ test("#given a config.toml that already declares [agents.explorer] at a differen
"no colliding ./agents registration for the mirrored role",
);
assert.match(config, /\[agents\.metis\]\nconfig_file = "\.\/agents\/metis\.toml"/);
- assert.equal(
- await readFile(join(fixture.codexHome, "agents", "explorer.toml"), "utf8"),
- BUNDLED_EXPLORER_TOML,
- "the linked toml is still staged for Codex directory discovery",
+ await assert.rejects(() => stat(join(fixture.codexHome, "agents", "explorer.toml")), { code: "ENOENT" });
+ assert.equal(await readFile(join(fixture.codexHome, "agents", "metis.toml"), "utf8"), BUNDLED_METIS_TOML);
+ const manifest = JSON.parse(
+ await readFile(join(fixture.pluginData, "bootstrap", "agents-stage", ".installed-agents.json"), "utf8"),
);
+ assert.deepEqual(manifest.agents, [join(fixture.codexHome, "agents", "metis.toml")]);
+ await runWorkerSetup(setupOptions(fixture));
+ assert.equal(await readConfig(fixture), config);
+ await assert.rejects(() => stat(join(fixture.codexHome, "agents", "explorer.toml")), { code: "ENOENT" });
});
});
Problem Situation
Orca mirrors LazyCodex agent registrations into its runtime CODEX_HOME with absolute config_file paths. LazyCodex bootstrap also materializes the same roles under the runtime agents directory, so Codex reports 12 malformed same-layer duplicate role definitions on every launch.
Reproduction Logs
CODEX_HOME=<isolated-orca-home> codex exec --ephemeral --skip-git-repo-check "Reply exactly PROBE_OK"exited 0 but emitted 12 distinctduplicate agent role name ... declared in the same config layerwarnings.The failing regression command was:
node --test --test-name-pattern='Orca mirror' test/bootstrap-setup.test.mjsBefore the fix it failed with
AssertionError [ERR_ASSERTION]: Missing expected rejection.becauseagents/explorer.tomlwas still materialized beside Orca's explicit explorer registration.Root Cause
linkBundledAgentsStepstaged and linked every bundled agent beforeupdateConfigStepfiltered foreign registrations. The config filter prevented a second TOML block, but it did not prevent Codex directory discovery from loading the runtime-local file with the same embedded role name.The warning list exactly matched the intersection between Orca's explicit agent registrations and LazyCodex-managed runtime TOMLs. A configured-only role without a runtime-local copy did not warn.
Verified Fix
Filter foreign explicit registrations before staging, remove stale LazyCodex-managed runtime copies for those roles, and let the existing explicit config remain authoritative. The installed-agent manifest now contains only roles that were actually materialized. This is idempotent and preserves the normal no-foreign-registration path.
Verification
node --test --test-name-pattern='Orca mirror' test/bootstrap-setup.test.mjs-> 1 passed, 0 failed.node --test test/bootstrap-setup.test.mjs-> 13 passed, 0 failed.node components/bootstrap/scripts/build.mjs-> exit 0 using the committed standalone bootstrap dist.codex exec --ephemeralexited 0 with 0 duplicate-role warnings and returned the expected response.This fix was debugged, implemented, and verified with LazyCodex.
Tag: lazycodex-generated