Hacktoberfest 2026: le issue che i maintainer hanno segnato per ottobre, aperte e adatte ai principianti. Sfoglia le issue Hacktoberfest

Fix Orca duplicate agent role warnings after bootstrap

Aperta
#149 0 commenti 0 reazioni 0 assegnatari Vedi su GitHub

Nessuno ha ancora preso questa issue.

Valutazione

Difficoltà
4/5
Tempo stimato
3-5 giorni
Idoneità per principianti
68/100
Tipo di issue
Bug
Chiarezza
Specificata chiaramente
Stato di attività
Tranquilla
Stack tecnologico
node.js, typescript
Ambito
cli, devtools

Direzione di ricerca

Inizia eseguendo node --test --test-name-pattern='Orca mirror' test/bootstrap-setup.test.mjs, poi leggi linkBundledAgentsStep e stageBundledAgents in plugins/omo/components/bootstrap/src/setup.ts e src/agent-staging.ts. Confronta il percorso generato di dist/cli.js con l’helper di configurazione esistente. Il lavoro è concluso quando la regressione passa, le copie di runtime esterni sono escluse, le copie obsolete vengono rimosse e il normale percorso senza registrazione di runtime esterni rimane intatto.

Scritto dal modello di indicizzazione a partire dal testo della issue.

Descrizione

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 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 --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" });
 	});
 });
 

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.
Tag: lazycodex-generated

Lingua principale
TypeScript
Stelle
3.8k
Fork
243
Metriche di merge delle PR
Nessuna PR unita negli ultimi 30g

Preparare l'ambiente

Questo progetto non fornisce container di sviluppo, Dockerfile né guida per i contributori, quindi l'ambiente è a tuo carico: parti dal suo README e consulta la nostra guida al primo contributo per i passaggi generali.

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Altre issue di code-yeongyu/lazycodex

Tutte le issue di code-yeongyu/lazycodex

Issue simili

Altre issue su TypeScript

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.