diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 98a11e10b..ae653ed90 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -27,6 +27,7 @@ - Reduced bash, grep, and glob tool guidance while preserving supported internal-URL routes and making shell/eval boundaries and broad-search timeout avoidance explicit. - Fixed the spawn-based models config validator laziness test flaking under CPU contention by giving its two sequential probe processes an explicit 60s per-test timeout instead of sharing bun's 5s default ([#7018](https://github.com/can1357/oh-my-pi/issues/7018)). +- Coalesced the models configuration resource probe into one child process to avoid startup contention while preserving retained-resource coverage. ## [17.1.8] - 2026-07-28 diff --git a/packages/coding-agent/test/fixtures/models-config-validator-construction-probe.ts b/packages/coding-agent/test/fixtures/models-config-validator-construction-probe.ts index 6753d4933..fd984381f 100644 --- a/packages/coding-agent/test/fixtures/models-config-validator-construction-probe.ts +++ b/packages/coding-agent/test/fixtures/models-config-validator-construction-probe.ts @@ -9,14 +9,52 @@ interface HeapSnapshot { snapshot: { meta: { node_fields: string[] } }; } -const root = process.argv[2]; -const mode = process.argv[3]; -if (!root || (mode !== "missing" && mode !== "custom")) { - throw new Error("Expected an isolated config root and missing|custom mode"); +interface ProbeResult { + retainedHeapNodes: number; + schemaIdentityStable?: boolean; + model?: { + provider: string; + id: string; + baseUrl: string; + api: string; + thinking?: unknown; + }; } -const configPath = path.join(root, mode, "models.yml"); -if (mode === "custom") { +const root = process.argv[2]; +if (!root) { + throw new Error("Expected an isolated config root"); +} + +function retainedHeapNodes(): number { + Bun.gc(true); + const snapshot = JSON.parse(Bun.generateHeapSnapshot("v8")) as HeapSnapshot; + return snapshot.nodes.length / snapshot.snapshot.meta.node_fields.length; +} + +async function measureMissing(): Promise { + let model: ProbeResult["model"]; + { + const authStorage = await AuthStorage.create(":memory:"); + try { + const registry = new ModelRegistry(authStorage, path.join(root, "missing", "models.yml")); + const found = registry.find("anthropic", "claude-sonnet-4-5"); + model = found && { + provider: found.provider, + id: found.id, + baseUrl: found.baseUrl, + api: found.api, + thinking: found.thinking, + }; + } finally { + authStorage.close(); + } + } + return { retainedHeapNodes: retainedHeapNodes(), model }; +} + +async function writeCustomConfig(): Promise { + const configPath = path.join(root, "custom", "models.yml"); await Bun.write( configPath, YAML.stringify( @@ -45,34 +83,38 @@ if (mode === "custom") { 2, ), ); + return configPath; } -const authStorage = await AuthStorage.create(":memory:"); -try { - const registry = new ModelRegistry(authStorage, configPath); - const model = - mode === "custom" ? registry.find("lazy-models", "lazy-model") : registry.find("anthropic", "claude-sonnet-4-5"); - const firstSchema = mode === "custom" ? ModelsConfigFile.relocate(configPath).schema : undefined; - const secondSchema = - mode === "custom" ? ModelsConfigFile.relocate(path.join(root, "second", "models.yml")).schema : undefined; - - Bun.gc(true); - const snapshot = JSON.parse(Bun.generateHeapSnapshot("v8")) as HeapSnapshot; - const nodeWidth = snapshot.snapshot.meta.node_fields.length; - - process.stdout.write( - JSON.stringify({ - retainedHeapNodes: snapshot.nodes.length / nodeWidth, - schemaIdentityStable: mode === "custom" ? firstSchema === secondSchema : undefined, - model: model && { - provider: model.provider, - id: model.id, - baseUrl: model.baseUrl, - api: model.api, - thinking: model.thinking, - }, - }), - ); -} finally { - authStorage.close(); +async function measureCustom(configPath: string): Promise { + let model: ProbeResult["model"]; + let schemaIdentityStable = false; + { + const authStorage = await AuthStorage.create(":memory:"); + try { + const registry = new ModelRegistry(authStorage, configPath); + const found = registry.find("lazy-models", "lazy-model"); + model = found && { + provider: found.provider, + id: found.id, + baseUrl: found.baseUrl, + api: found.api, + thinking: found.thinking, + }; + const firstSchema = ModelsConfigFile.relocate(configPath).schema; + const secondSchema = ModelsConfigFile.relocate(path.join(root, "second", "models.yml")).schema; + schemaIdentityStable = firstSchema === secondSchema; + } finally { + authStorage.close(); + } + } + return { + retainedHeapNodes: retainedHeapNodes(), + schemaIdentityStable, + model, + }; } + +const missing = await measureMissing(); +const custom = await measureCustom(await writeCustomConfig()); +process.stdout.write(JSON.stringify({ missing, custom })); diff --git a/packages/coding-agent/test/models-config-lazy-validator.test.ts b/packages/coding-agent/test/models-config-lazy-validator.test.ts index da2c51034..0c71bb56b 100644 --- a/packages/coding-agent/test/models-config-lazy-validator.test.ts +++ b/packages/coding-agent/test/models-config-lazy-validator.test.ts @@ -14,10 +14,15 @@ interface ProbeResult { }; } +interface ProbeResults { + missing: ProbeResult; + custom: ProbeResult; +} + const probePath = path.join(import.meta.dir, "fixtures", "models-config-validator-construction-probe.ts"); -async function runProbe(root: string, mode: "missing" | "custom"): Promise { - const proc = Bun.spawn([process.execPath, probePath, root, mode], { +async function runProbe(root: string): Promise { + const proc = Bun.spawn([process.execPath, probePath, root], { cwd: path.join(import.meta.dir, "../../.."), stdout: "pipe", stderr: "pipe", @@ -28,14 +33,13 @@ async function runProbe(root: string, mode: "missing" | "custom"): Promise { const tempDir = TempDir.createSync("@models-config-validator-"); try { - const missing = await runProbe(tempDir.path(), "missing"); - const custom = await runProbe(tempDir.path(), "custom"); + const { missing, custom } = await runProbe(tempDir.path()); expect(missing.model).toMatchObject({ provider: "anthropic", @@ -55,6 +59,7 @@ test("models config validation resources are retained only for a custom config", }, }); expect(custom.schemaIdentityStable).toBe(true); + expect(missing.retainedHeapNodes).toBeLessThan(custom.retainedHeapNodes); expect( custom.retainedHeapNodes - missing.retainedHeapNodes, "custom config validation should retain its schema bundle",