Fix Orca duplicate agent role warnings after bootstrap
还没有人认领这个 Issue。
评估
- 难度
- 4/5
- 预计耗时
- 3-5 天
- 新手友好度
- 68/100
- Issue 类型
- 缺陷
- 描述清晰度
- 描述清楚
- 活跃度
- 冷清
- 技术栈
- node.js, typescript
调研方向
先运行 node --test --test-name-pattern='Orca mirror' test/bootstrap-setup.test.mjs,然后阅读 plugins/omo/components/bootstrap/src/setup.ts 和 src/agent-staging.ts 中的 linkBundledAgentsStep 和 stageBundledAgents。比较生成的 dist/cli.js 路径与现有的配置辅助工具。回归测试通过、排除外部运行时副本、删除过时副本,并且不注册外部运行时的正常路径保持不变,即表示完成。
由索引模型根据 Issue 内容生成。
描述
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 --ephemeralexited 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
- 主要语言
- TypeScript
- 星标
- 3.8k
- 派生
- 246
- PR 合并指标
- 30 天内没有已合并 PR
环境准备
这个项目没有提供开发容器、Dockerfile 或贡献指南,环境需要你自己搭建:先看它的 README,通用步骤见我们的新手贡献指南。
从这里开始
- 先读完整个 Issue,再读项目的贡献指南。
- 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
- Fork 仓库,在一个分支上完成修改。
- 提交 Pull Request,并在描述里引用这个 Issue 编号。
code-yeongyu/lazycodex 的其他 Issue
-
难度 2/5 1-3 小时 新手友好度 68/100
code-yeongyu/lazycodex#170 ·
-
Fix Codex browser QA routing before standalone Playwright可能已有人在做 @berkshirehathaways 于 84 天前认领。 未关闭
难度 2/5 1-2 天 新手友好度 72/100
code-yeongyu/lazycodex#148 ·
-
难度 5/5 一周以上 新手友好度 35/100
code-yeongyu/lazycodex#172 ·
-
[5.0.0-beta.78] High cost and rapid quota consumption: use role-appropriate agent model defaults未关闭
难度 4/5 3-5 天 新手友好度 48/100
code-yeongyu/lazycodex#166 ·
-
难度 4/5 3-5 天 新手友好度 35/100
code-yeongyu/lazycodex#163 ·
查看 code-yeongyu/lazycodex 的全部 Issue
相似的 Issue
-
难度 2/5 1-3 小时 新手友好度 68/100
awesome-dsh-plugin/awesome-dsh-plugin#9184 ·
维护者通常 1 天内回复
-
难度 2/5 1-3 小时 新手友好度 75/100
维护者通常 1 天内回复
-
难度 2/5 1-3 小时 新手友好度 85/100
farion1231/cc-switch#8829 ·
维护者通常 1 天内回复
-
[core-client] Remove unused hand-maintained transport helpers after the definition-table cutover未关闭doc-audit enhancement
难度 2/5 1-3 小时 新手友好度 88/100
维护者通常 1 天内回复
-
难度 2/5 1-3 小时 新手友好度 85/100