code-yeongyu / code-yeongyu/lazycodex
Fix duplicate agent roles in Orca-managed Codex homes
- Dominant language
- TypeScript
- Stars
- 3.5k
- Forks
- 216
- PR merge metrics
- No merged PRs in 30d
Description
## Problem Situation
When Codex runs inside Orca, Orca mirrors global `[agents.*]` registrations into the account CODEX_HOME. OMO's SessionStart bootstrap also copies the same bundled role TOMLs into `CODEX_HOME/agents/`. Codex directory discovery then loads the local TOMLs in addition to Orca's mirrored paths and warns that the role names are duplicated.
## Reproduction Logs
Focused bootstrap regression before the fix:
```text
not ok - #given a config.toml that already declares [agents.explorer] at a different path (Orca mirror)
AssertionError [ERR_ASSERTION]: Missing expected rejection.
tests 13, pass 12, fail 1
```
Full log: `/tmp/lazycodex-fix-orca-agent-duplicates-repro.log`
## Root Cause
The bootstrap only filtered foreign registrations while updating `config.toml`. It still staged and copied every bundled TOML into `CODEX_HOME/agents/`. Since Codex auto-discovers that directory, filtering the config block alone could not prevent the duplicate role.
## Verified Fix
Read the existing account config before staging. For each bundled role already registered at a foreign path, remove the stale OMO-managed account-local TOML and omit it from the staging tree. Other bundled roles continue to install normally. The change is entirely in OMO bootstrap code; no Orca service or app files are modified.
```diff
diff --git a/plugins/omo/components/bootstrap/dist/cli.js b/plugins/omo/components/bootstrap/dist/cli.js
index 8a81451..15789b5 100755
--- a/plugins/omo/components/bootstrap/dist/cli.js
+++ b/plugins/omo/components/bootstrap/dist/cli.js
@@ -3543,7 +3543,8 @@ 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"));
+ await stageBundledAgents(options.pluginRoot, stageRoot, agentsTarget, existingConfig);
const preservedReasoning = await capturePreservedAgentReasoning({ codexHome: options.codexHome });
const preservedServiceTier = await capturePreservedAgentServiceTier({ codexHome: options.codexHome });
const linked = await linkCachedPluginAgents({
@@ -3567,7 +3568,7 @@ async function linkBundledAgentsStep(options) {
};
}
}
-async function stageBundledAgents(pluginRoot, stageRoot) {
+async function stageBundledAgents(pluginRoot, stageRoot, agentsTarget, existingConfig) {
await rm10(stageRoot, { force: true, recursive: true });
await mkdir7(stageRoot, { recursive: true });
const componentsRoot = join21(pluginRoot, "components");
@@ -3579,6 +3580,14 @@ 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)) {
+ await rm10(join21(agentsTarget, agentFile), { force: true });
+ continue;
+ }
await copyFile2(join21(agentsDir, agentFile), join21(stagedAgentsDir, agentFile));
}
}
diff --git a/plugins/omo/components/bootstrap/src/agent-stage.ts b/plugins/omo/components/bootstrap/src/agent-stage.ts
new file mode 100644
index 0000000..7cdc660
--- /dev/null
+++ b/plugins/omo/components/bootstrap/src/agent-stage.ts
@@ -0,0 +1,62 @@
+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,
+ agentsTarget: string,
+ existingConfig: string,
+): 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) {
+ const agentConfig = {
+ configFile: `./agents/${agentFile}`,
+ name: agentNameFromToml(agentFile),
+ };
+ if (hasForeignAgentRegistration(existingConfig, agentConfig)) {
+ // Codex discovers CODEX_HOME/agents automatically. Leaving an OMO copy
+ // beside Orca's mirrored registration creates the same role twice.
+ await rm(join(agentsTarget, agentFile), { force: true });
+ continue;
+ }
+ await copyFile(join(agentsDir, agentFile), join(stagedAgentsDir, agentFile));
+ }
+ }
+}
+
+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;
+ }
+}
+
+export function agentNameFromToml(fileName: string): string {
+ return fileName.endsWith(".toml") ? fileName.slice(0, -".toml".length) : fileName;
+}
diff --git a/plugins/omo/components/bootstrap/src/setup.ts b/plugins/omo/components/bootstrap/src/setup.ts
index 1a03421..79efe16 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, 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-stage.ts";
import { appendBootstrapLog, BOOTSTRAP_DOCTOR_HINT } from "./worker.ts";
import type { BootstrapDegradedEntry, BootstrapStepOutcome } from "./worker.ts";
@@ -91,7 +92,8 @@ 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 },
@@ -268,31 +254,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..a5dcf88 100644
--- a/plugins/omo/test/bootstrap-setup.test.mjs
+++ b/plugins/omo/test/bootstrap-setup.test.mjs
@@ -130,6 +130,8 @@ 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 () => {
await withSetupFixture(async (fixture) => {
const orcaMirrorPath = "/orca-mirrored-home/.codex/agents/explorer.toml";
+ await mkdir(join(fixture.codexHome, "agents"), { recursive: true });
+ await writeFile(join(fixture.codexHome, "agents", "explorer.toml"), BUNDLED_EXPLORER_TOML);
await writeFile(
join(fixture.codexHome, "config.toml"),
`[marketplaces.sisyphuslabs]\n${MARKETPLACE_SOURCE_LINE}\n\n[agents.explorer]\nconfig_file = "${orcaMirrorPath}"\n`,
@@ -149,10 +151,10 @@ 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")),
+ (error) => error?.code === "ENOENT",
+ "the stale local toml is removed because Codex directory discovery would register it again",
);
});
});
```
## Verification
- RED: focused regression test failed before the fix (12 passed, 1 failed).
- GREEN: `node --test plugins/omo/test/bootstrap-setup.test.mjs` passed 13/13.
- Adjacent: setup, binlinks, hooks, and orchestration bootstrap suites passed with 0 failures.
- Generated runtime bundle and TypeScript source were reviewed for behavioral parity.
- Independent LazyCodex code review: APPROVE; gate review: APPROVE.
---
This fix was debugged, implemented, and verified by [LazyCodex](https://github.com/code-yeongyu/lazycodex).
Tag: lazycodex-generated
Contributor guide
No contributing guide indexed for this repository
Research direction
Start with plugins/omo/components/bootstrap/src/agent-stage.ts and setup.ts, then inspect the regression in plugins/omo/test/bootstrap-setup.test.mjs. Run node --test plugins/omo/test/bootstrap-setup.test.mjs and the adjacent bootstrap suites mentioned in the issue. Done means foreign mirrored roles no longer leave local TOMLs, while other bundled roles still install and all listed suites pass.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- node.js, typescript
- Domain
- cli, tooling
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 58/100