code-yeongyu / code-yeongyu/lazycodex
Fix Orca duplicate agent role warnings after bootstrap
- Dominant language
- TypeScript
- Stars
- 3.5k
- Forks
- 216
- PR merge metrics
- No merged PRs in 30d
Description
## 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= codex exec --ephemeral --skip-git-repo-check "Reply exactly PROBE_OK"` exited 0 but emitted 12 distinct `duplicate agent role name ... declared in the same config layer` warnings.
The failing regression command was:
`node --test --test-name-pattern='Orca mirror' test/bootstrap-setup.test.mjs`
Before the fix it failed with `AssertionError [ERR_ASSERTION]: Missing expected rejection.` because `agents/explorer.toml` was still materialized beside Orca's explicit explorer registration.
## Root Cause
`linkBundledAgentsStep` staged and linked every bundled agent before `updateConfigStep` filtered 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.
```diff
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 {
+ 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 {
+ return entryNames(root, (entry) => entry.isDirectory());
+}
+
+async function fileNames(root: string): Promise {
+ return entryNames(root, (entry) => entry.isFile());
+}
+
+async function entryNames(root: string, keep: (entry: { isDirectory(): boolean; isFile(): boolean }) => boolean): Promise {
+ 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 {
- 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 /agents/.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 {
- return entryNames(root, (entry) => entry.isDirectory());
-}
-
-async function fileNames(root: string): Promise {
- return entryNames(root, (entry) => entry.isFile());
-}
-
-async function entryNames(root: string, keep: (entry: { isDirectory(): boolean; isFile(): boolean }) => boolean): Promise {
- 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" });
});
});
```
## Verification
- RED: Orca mirror regression failed before the implementation because the duplicate runtime TOML still existed.
- GREEN: `node --test --test-name-pattern='Orca mirror' test/bootstrap-setup.test.mjs` -> 1 passed, 0 failed.
- Regression: `node --test test/bootstrap-setup.test.mjs` -> 13 passed, 0 failed.
- Component build: `node components/bootstrap/scripts/build.mjs` -> exit 0 using the committed standalone bootstrap dist.
- Manual QA: patched bootstrap against an isolated Orca-shaped CODEX_HOME changed runtime agent TOMLs from 12 to 0; real `codex exec --ephemeral` exited 0 with 0 duplicate-role warnings and returned the expected response.
- Live QA: the patched bootstrap was applied to the affected Orca account; a fresh Codex process returned the expected response with 0 duplicate-role warnings.
---
This fix was debugged, implemented, and verified with [LazyCodex](https://github.com/code-yeongyu/lazycodex).
Tag: lazycodex-generated
Contributor guide
No contributing guide indexed for this repository
Research direction
Start with node --test --test-name-pattern='Orca mirror' test/bootstrap-setup.test.mjs, then read linkBundledAgentsStep and stageBundledAgents in plugins/omo/components/bootstrap/src/setup.ts and src/agent-staging.ts. Compare the generated dist/cli.js path and the existing config helper. Done means the regression passes, foreign runtime copies are excluded, stale copies are removed, and the normal no-foreign-registration path remains intact.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- node.js, typescript
- Domain
- cli, devtools
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 68/100