oh-my-opencode 4.8.0 → 4.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +51 -23
- package/dist/cli/install-codex/codex-multi-agent-v2-config.d.ts +11 -0
- package/dist/cli/run/runnable-agent-resolver.d.ts +11 -0
- package/package.json +12 -12
- package/packages/omo-codex/plugin/components/rules/src/rules/formatter.ts +7 -8
- package/packages/omo-codex/plugin/components/rules/src/rules/truncator.ts +17 -0
- package/packages/omo-codex/plugin/components/rules/test/formatter.test.ts +20 -0
- package/packages/omo-codex/plugin/scripts/migrate-codex-config/multi-agent-v2-guard.mjs +40 -0
- package/packages/omo-codex/plugin/scripts/migrate-codex-config.mjs +23 -6
- package/packages/omo-codex/plugin/test/migrate-codex-config.test.mjs +79 -0
- package/packages/omo-codex/scripts/install/config.mjs +2 -0
- package/packages/omo-codex/scripts/install/multi-agent-v2-config.mjs +12 -10
package/dist/cli/index.js
CHANGED
|
@@ -62121,7 +62121,7 @@ var {
|
|
|
62121
62121
|
// package.json
|
|
62122
62122
|
var package_default = {
|
|
62123
62123
|
name: "oh-my-opencode",
|
|
62124
|
-
version: "4.8.
|
|
62124
|
+
version: "4.8.1",
|
|
62125
62125
|
description: "The Best AI Agent Harness - Batteries-Included OpenCode Plugin with Multi-Model Orchestration, Parallel Background Agents, and Crafted LSP/AST Tools",
|
|
62126
62126
|
main: "./dist/index.js",
|
|
62127
62127
|
types: "dist/index.d.ts",
|
|
@@ -62258,17 +62258,17 @@ var package_default = {
|
|
|
62258
62258
|
zod: "^4.4.3"
|
|
62259
62259
|
},
|
|
62260
62260
|
optionalDependencies: {
|
|
62261
|
-
"oh-my-opencode-darwin-arm64": "4.8.
|
|
62262
|
-
"oh-my-opencode-darwin-x64": "4.8.
|
|
62263
|
-
"oh-my-opencode-darwin-x64-baseline": "4.8.
|
|
62264
|
-
"oh-my-opencode-linux-arm64": "4.8.
|
|
62265
|
-
"oh-my-opencode-linux-arm64-musl": "4.8.
|
|
62266
|
-
"oh-my-opencode-linux-x64": "4.8.
|
|
62267
|
-
"oh-my-opencode-linux-x64-baseline": "4.8.
|
|
62268
|
-
"oh-my-opencode-linux-x64-musl": "4.8.
|
|
62269
|
-
"oh-my-opencode-linux-x64-musl-baseline": "4.8.
|
|
62270
|
-
"oh-my-opencode-windows-x64": "4.8.
|
|
62271
|
-
"oh-my-opencode-windows-x64-baseline": "4.8.
|
|
62261
|
+
"oh-my-opencode-darwin-arm64": "4.8.1",
|
|
62262
|
+
"oh-my-opencode-darwin-x64": "4.8.1",
|
|
62263
|
+
"oh-my-opencode-darwin-x64-baseline": "4.8.1",
|
|
62264
|
+
"oh-my-opencode-linux-arm64": "4.8.1",
|
|
62265
|
+
"oh-my-opencode-linux-arm64-musl": "4.8.1",
|
|
62266
|
+
"oh-my-opencode-linux-x64": "4.8.1",
|
|
62267
|
+
"oh-my-opencode-linux-x64-baseline": "4.8.1",
|
|
62268
|
+
"oh-my-opencode-linux-x64-musl": "4.8.1",
|
|
62269
|
+
"oh-my-opencode-linux-x64-musl-baseline": "4.8.1",
|
|
62270
|
+
"oh-my-opencode-windows-x64": "4.8.1",
|
|
62271
|
+
"oh-my-opencode-windows-x64-baseline": "4.8.1"
|
|
62272
62272
|
},
|
|
62273
62273
|
overrides: {
|
|
62274
62274
|
hono: "^4.12.18",
|
|
@@ -63921,19 +63921,10 @@ function ensureCodexMultiAgentV2Config(config) {
|
|
|
63921
63921
|
const maxThreadsValue = CODEX_MULTI_AGENT_V2_MAX_CONCURRENT_THREADS_PER_SESSION.toString();
|
|
63922
63922
|
if (!section) {
|
|
63923
63923
|
return appendBlock(normalizedConfig, `[${CODEX_MULTI_AGENT_V2_HEADER}]
|
|
63924
|
-
enabled = true
|
|
63925
|
-
max_concurrent_threads_per_session = ${maxThreadsValue}
|
|
63926
|
-
`);
|
|
63927
|
-
}
|
|
63928
|
-
const enabledConfig = replaceOrInsertSetting(normalizedConfig, section, "enabled", "true");
|
|
63929
|
-
const updatedSection = findTomlSection(enabledConfig, CODEX_MULTI_AGENT_V2_HEADER);
|
|
63930
|
-
if (!updatedSection) {
|
|
63931
|
-
return appendBlock(enabledConfig, `[${CODEX_MULTI_AGENT_V2_HEADER}]
|
|
63932
|
-
enabled = true
|
|
63933
63924
|
max_concurrent_threads_per_session = ${maxThreadsValue}
|
|
63934
63925
|
`);
|
|
63935
63926
|
}
|
|
63936
|
-
return replaceOrInsertSetting(
|
|
63927
|
+
return replaceOrInsertSetting(normalizedConfig, section, "max_concurrent_threads_per_session", maxThreadsValue);
|
|
63937
63928
|
}
|
|
63938
63929
|
function removeFeatureFlagSetting(config, featureName) {
|
|
63939
63930
|
const section = findTomlSection(config, "features");
|
|
@@ -86728,6 +86719,42 @@ function createCliPostHog2() {
|
|
|
86728
86719
|
|
|
86729
86720
|
// src/cli/run/runner.ts
|
|
86730
86721
|
init_prompt_async_gate();
|
|
86722
|
+
|
|
86723
|
+
// src/cli/run/runnable-agent-resolver.ts
|
|
86724
|
+
init_shared();
|
|
86725
|
+
init_agent_display_names();
|
|
86726
|
+
async function resolveRunnableRunAgent(client3, resolvedAgent, config2 = {}) {
|
|
86727
|
+
try {
|
|
86728
|
+
const agentsRes = await client3.app.agents();
|
|
86729
|
+
const agents = normalizeSDKResponse(agentsRes, [], {
|
|
86730
|
+
preferResponseOnMissingData: true
|
|
86731
|
+
});
|
|
86732
|
+
const exactAgent = agents.find((agent) => agent.name === resolvedAgent)?.name;
|
|
86733
|
+
if (exactAgent)
|
|
86734
|
+
return exactAgent;
|
|
86735
|
+
const resolvedConfigKey = getAgentConfigKey(resolvedAgent);
|
|
86736
|
+
const configuredDisplayName = config2.agents?.[resolvedConfigKey]?.displayName;
|
|
86737
|
+
const configuredAgent = agents.find((agent) => {
|
|
86738
|
+
if (!agent.name || !configuredDisplayName)
|
|
86739
|
+
return false;
|
|
86740
|
+
return agent.name === configuredDisplayName;
|
|
86741
|
+
})?.name;
|
|
86742
|
+
if (configuredAgent)
|
|
86743
|
+
return configuredAgent;
|
|
86744
|
+
return agents.find((agent) => {
|
|
86745
|
+
if (!agent.name)
|
|
86746
|
+
return false;
|
|
86747
|
+
return getAgentConfigKey(agent.name) === resolvedConfigKey;
|
|
86748
|
+
})?.name ?? resolvedAgent;
|
|
86749
|
+
} catch (error51) {
|
|
86750
|
+
if (!(error51 instanceof Error)) {
|
|
86751
|
+
throw error51;
|
|
86752
|
+
}
|
|
86753
|
+
return resolvedAgent;
|
|
86754
|
+
}
|
|
86755
|
+
}
|
|
86756
|
+
|
|
86757
|
+
// src/cli/run/runner.ts
|
|
86731
86758
|
var EVENT_PROCESSOR_SHUTDOWN_TIMEOUT_MS = 2000;
|
|
86732
86759
|
async function waitForEventProcessorShutdown(eventProcessor, timeoutMs = EVENT_PROCESSOR_SHUTDOWN_TIMEOUT_MS) {
|
|
86733
86760
|
const completed = await Promise.race([
|
|
@@ -86783,6 +86810,7 @@ Interrupted. Shutting down...`));
|
|
|
86783
86810
|
sessionId: options.sessionId,
|
|
86784
86811
|
directory
|
|
86785
86812
|
});
|
|
86813
|
+
const runnableAgent = await resolveRunnableRunAgent(client3, resolvedAgent, pluginConfig);
|
|
86786
86814
|
console.log(import_picocolors16.default.dim(`Session: ${sessionID}`));
|
|
86787
86815
|
if (resolvedModel) {
|
|
86788
86816
|
console.log(import_picocolors16.default.dim(`Model: ${resolvedModel.providerID}/${resolvedModel.modelID}`));
|
|
@@ -86808,7 +86836,7 @@ Interrupted. Shutting down...`));
|
|
|
86808
86836
|
input: {
|
|
86809
86837
|
path: { id: sessionID },
|
|
86810
86838
|
body: {
|
|
86811
|
-
agent:
|
|
86839
|
+
agent: runnableAgent,
|
|
86812
86840
|
...resolvedModel ? { model: resolvedModel } : {},
|
|
86813
86841
|
tools: {
|
|
86814
86842
|
question: false
|
|
@@ -1 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configure multi_agent_v2 thread limits without forcing the feature on.
|
|
3
|
+
*
|
|
4
|
+
* Whether V2 is active is determined at runtime by the model's server-side
|
|
5
|
+
* catalog entry (`ModelInfo.multi_agent_version`). Forcing `enabled = true`
|
|
6
|
+
* in config breaks models whose API does not support encrypted tool
|
|
7
|
+
* parameters (e.g. gpt-5.5-medium, API-key-only models, third-party
|
|
8
|
+
* providers). The installer therefore only sets the tuning knob
|
|
9
|
+
* (`max_concurrent_threads_per_session`) so that sessions that DO activate
|
|
10
|
+
* V2 benefit from the higher limit.
|
|
11
|
+
*/
|
|
1
12
|
export declare function ensureCodexMultiAgentV2Config(config: string): string;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface RunAgentListClient {
|
|
2
|
+
readonly app: {
|
|
3
|
+
readonly agents: () => Promise<unknown>;
|
|
4
|
+
};
|
|
5
|
+
}
|
|
6
|
+
export interface RunAgentDisplayNameConfig {
|
|
7
|
+
readonly agents?: Readonly<Record<string, {
|
|
8
|
+
readonly displayName?: string;
|
|
9
|
+
} | undefined>>;
|
|
10
|
+
}
|
|
11
|
+
export declare function resolveRunnableRunAgent(client: RunAgentListClient, resolvedAgent: string, config?: RunAgentDisplayNameConfig): Promise<string>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "oh-my-opencode",
|
|
3
|
-
"version": "4.8.
|
|
3
|
+
"version": "4.8.1",
|
|
4
4
|
"description": "The Best AI Agent Harness - Batteries-Included OpenCode Plugin with Multi-Model Orchestration, Parallel Background Agents, and Crafted LSP/AST Tools",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -137,17 +137,17 @@
|
|
|
137
137
|
"zod": "^4.4.3"
|
|
138
138
|
},
|
|
139
139
|
"optionalDependencies": {
|
|
140
|
-
"oh-my-opencode-darwin-arm64": "4.8.
|
|
141
|
-
"oh-my-opencode-darwin-x64": "4.8.
|
|
142
|
-
"oh-my-opencode-darwin-x64-baseline": "4.8.
|
|
143
|
-
"oh-my-opencode-linux-arm64": "4.8.
|
|
144
|
-
"oh-my-opencode-linux-arm64-musl": "4.8.
|
|
145
|
-
"oh-my-opencode-linux-x64": "4.8.
|
|
146
|
-
"oh-my-opencode-linux-x64-baseline": "4.8.
|
|
147
|
-
"oh-my-opencode-linux-x64-musl": "4.8.
|
|
148
|
-
"oh-my-opencode-linux-x64-musl-baseline": "4.8.
|
|
149
|
-
"oh-my-opencode-windows-x64": "4.8.
|
|
150
|
-
"oh-my-opencode-windows-x64-baseline": "4.8.
|
|
140
|
+
"oh-my-opencode-darwin-arm64": "4.8.1",
|
|
141
|
+
"oh-my-opencode-darwin-x64": "4.8.1",
|
|
142
|
+
"oh-my-opencode-darwin-x64-baseline": "4.8.1",
|
|
143
|
+
"oh-my-opencode-linux-arm64": "4.8.1",
|
|
144
|
+
"oh-my-opencode-linux-arm64-musl": "4.8.1",
|
|
145
|
+
"oh-my-opencode-linux-x64": "4.8.1",
|
|
146
|
+
"oh-my-opencode-linux-x64-baseline": "4.8.1",
|
|
147
|
+
"oh-my-opencode-linux-x64-musl": "4.8.1",
|
|
148
|
+
"oh-my-opencode-linux-x64-musl-baseline": "4.8.1",
|
|
149
|
+
"oh-my-opencode-windows-x64": "4.8.1",
|
|
150
|
+
"oh-my-opencode-windows-x64-baseline": "4.8.1"
|
|
151
151
|
},
|
|
152
152
|
"overrides": {
|
|
153
153
|
"hono": "^4.12.18",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { truncateBudget, truncateRule } from "./truncator.js";
|
|
1
|
+
import { isNeverTruncatedRule, truncateBudget, truncateRule } from "./truncator.js";
|
|
2
2
|
import type { LoadedRule } from "./types.js";
|
|
3
3
|
|
|
4
4
|
export interface FormatOptions {
|
|
@@ -35,13 +35,12 @@ function truncateRules(rules: ReadonlyArray<LoadedRule>, options: FormatOptions)
|
|
|
35
35
|
const perRuleBudgeted = perRuleNormalized.map((rule) => ({
|
|
36
36
|
path: rule.path,
|
|
37
37
|
relativePath: rule.relativePath,
|
|
38
|
-
body:
|
|
39
|
-
rule.
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}).body,
|
|
38
|
+
body: isNeverTruncatedRule(rule.relativePath)
|
|
39
|
+
? rule.body
|
|
40
|
+
: truncateRule(rule.body, {
|
|
41
|
+
maxChars: Math.min(options.maxRuleChars, perRuleResultChars),
|
|
42
|
+
relativePath: rule.relativePath,
|
|
43
|
+
}).body,
|
|
45
44
|
}));
|
|
46
45
|
const budgetedRules = truncateBudget({
|
|
47
46
|
rules: perRuleBudgeted.map((rule) => ({ body: rule.body, relativePath: rule.relativePath })),
|
|
@@ -14,6 +14,13 @@ function truncationNotice(relativePath: string): string {
|
|
|
14
14
|
return TRUNCATION_NOTICE.replace("{path}", relativePath);
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
export function isNeverTruncatedRule(relativePath: string): boolean {
|
|
18
|
+
const normalized = relativePath.replace(/\\/g, "/");
|
|
19
|
+
const segments = normalized.split("/").filter((segment) => segment.length > 0);
|
|
20
|
+
const filename = segments.at(-1) ?? normalized;
|
|
21
|
+
return filename.toLowerCase() === "hephaestus.md";
|
|
22
|
+
}
|
|
23
|
+
|
|
17
24
|
function safeSliceEnd(body: string, end: number): number {
|
|
18
25
|
if (end <= 0) {
|
|
19
26
|
return 0;
|
|
@@ -28,6 +35,10 @@ function safeSliceEnd(body: string, end: number): number {
|
|
|
28
35
|
}
|
|
29
36
|
|
|
30
37
|
export function truncateRule(body: string, options: { maxChars: number; relativePath: string }): TruncationResult {
|
|
38
|
+
if (isNeverTruncatedRule(options.relativePath)) {
|
|
39
|
+
return { body, truncated: false, originalLength: body.length };
|
|
40
|
+
}
|
|
41
|
+
|
|
31
42
|
if (body.length <= options.maxChars) {
|
|
32
43
|
return { body, truncated: false, originalLength: body.length };
|
|
33
44
|
}
|
|
@@ -46,6 +57,12 @@ export function truncateBudget(input: { rules: ReadonlyArray<BudgetRule>; maxRes
|
|
|
46
57
|
let remainingBudget = input.maxResultChars;
|
|
47
58
|
|
|
48
59
|
for (const rule of input.rules) {
|
|
60
|
+
if (isNeverTruncatedRule(rule.relativePath)) {
|
|
61
|
+
results.push({ body: rule.body, truncated: false, relativePath: rule.relativePath });
|
|
62
|
+
remainingBudget -= rule.body.length;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
|
|
49
66
|
if (remainingBudget >= rule.body.length) {
|
|
50
67
|
results.push({ body: rule.body, truncated: false, relativePath: rule.relativePath });
|
|
51
68
|
remainingBudget -= rule.body.length;
|
|
@@ -145,6 +145,26 @@ describe("rules formatter hook context", () => {
|
|
|
145
145
|
expect(block).not.toContain("must read project rules:");
|
|
146
146
|
});
|
|
147
147
|
|
|
148
|
+
it("#given an oversized Hephaestus static rule #when formatting under a tight result budget #then its body is never truncated", () => {
|
|
149
|
+
// given
|
|
150
|
+
const tailMarker = "HEPHAESTUS_TAIL_SENTINEL";
|
|
151
|
+
const rule = loadedRule({
|
|
152
|
+
path: "/repo/bundled-rules/hephaestus.md",
|
|
153
|
+
relativePath: "bundled-rules/hephaestus.md",
|
|
154
|
+
body: `${"H".repeat(500)}\n\n${tailMarker}`,
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// when
|
|
158
|
+
const block = formatStaticBlock([rule], {
|
|
159
|
+
maxRuleChars: 120,
|
|
160
|
+
maxResultChars: 200,
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// then
|
|
164
|
+
expect(block).toContain(tailMarker);
|
|
165
|
+
expect(block).not.toContain("[Truncated. Full:");
|
|
166
|
+
});
|
|
167
|
+
|
|
148
168
|
it("#given multiple oversized rules #when formatting under a tight result budget #then every rule receives a fair truncated share with a read-full guide", () => {
|
|
149
169
|
// given
|
|
150
170
|
const rules = [
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime migration: undo the installer-forced `enabled = true` on
|
|
3
|
+
* `[features.multi_agent_v2]`.
|
|
4
|
+
*
|
|
5
|
+
* Whether V2 is active should be determined at runtime by the model's
|
|
6
|
+
* server-side catalog entry (`ModelInfo.multi_agent_version`). Previous
|
|
7
|
+
* installer versions unconditionally set `enabled = true`, which forces
|
|
8
|
+
* V2 for ALL models --- including those whose API does not support
|
|
9
|
+
* encrypted tool parameters (spawn_agent). This guard removes the
|
|
10
|
+
* forced flag so the Codex runtime can make the right decision per model.
|
|
11
|
+
*/
|
|
12
|
+
export function disableMultiAgentV2IfForced(config) {
|
|
13
|
+
const section = findMultiAgentV2Section(config);
|
|
14
|
+
if (!section) return config;
|
|
15
|
+
|
|
16
|
+
const enabledPattern = /^(\s*)enabled\s*=\s*true\s*$/m;
|
|
17
|
+
if (!enabledPattern.test(section.text)) return config;
|
|
18
|
+
|
|
19
|
+
const patched = section.text.replace(enabledPattern, "$1enabled = false");
|
|
20
|
+
return config.slice(0, section.start) + patched + config.slice(section.end);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function findMultiAgentV2Section(config) {
|
|
24
|
+
const headerLine = "[features.multi_agent_v2]";
|
|
25
|
+
const lines = config.match(/[^\n]*\n?|$/g) ?? [];
|
|
26
|
+
let offset = 0;
|
|
27
|
+
let start = -1;
|
|
28
|
+
for (const line of lines) {
|
|
29
|
+
if (line.length === 0) break;
|
|
30
|
+
const trimmed = line.trim();
|
|
31
|
+
if (start === -1) {
|
|
32
|
+
if (trimmed === headerLine) start = offset;
|
|
33
|
+
} else if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
|
|
34
|
+
return { start, end: offset, text: config.slice(start, offset) };
|
|
35
|
+
}
|
|
36
|
+
offset += line.length;
|
|
37
|
+
}
|
|
38
|
+
if (start === -1) return null;
|
|
39
|
+
return { start, end: config.length, text: config.slice(start) };
|
|
40
|
+
}
|
|
@@ -6,6 +6,7 @@ import { pathToFileURL } from "node:url";
|
|
|
6
6
|
|
|
7
7
|
import { FALLBACK_CATALOG, readModelCatalog } from "./migrate-codex-config/catalog.mjs";
|
|
8
8
|
import { configPaths } from "./migrate-codex-config/config-paths.mjs";
|
|
9
|
+
import { disableMultiAgentV2IfForced } from "./migrate-codex-config/multi-agent-v2-guard.mjs";
|
|
9
10
|
import { ensureCodexReasoningConfig as applyReasoningProfile, readRootSettings } from "./migrate-codex-config/root-settings.mjs";
|
|
10
11
|
import { readState, resolveStatePath, writeState } from "./migrate-codex-config/state.mjs";
|
|
11
12
|
|
|
@@ -41,12 +42,28 @@ export async function migrateCodexConfig({ env = process.env, cwd = process.cwd(
|
|
|
41
42
|
export async function migrateConfigFile(configPath, { catalog = FALLBACK_CATALOG, previousState } = {}) {
|
|
42
43
|
const before = await readConfig(configPath);
|
|
43
44
|
const decision = shouldApplyCatalog(before, catalog, previousState);
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
45
|
+
|
|
46
|
+
let config = before;
|
|
47
|
+
let reasoningApplied = false;
|
|
48
|
+
|
|
49
|
+
if (decision.apply) {
|
|
50
|
+
config = ensureCodexReasoningConfig(config, catalog.current);
|
|
51
|
+
reasoningApplied = config !== before;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const afterMultiAgentGuard = disableMultiAgentV2IfForced(config);
|
|
55
|
+
const multiAgentChanged = afterMultiAgentGuard !== config;
|
|
56
|
+
if (multiAgentChanged) config = afterMultiAgentGuard;
|
|
57
|
+
|
|
58
|
+
const changed = reasoningApplied || multiAgentChanged;
|
|
59
|
+
if (changed) {
|
|
60
|
+
await mkdir(dirname(configPath), { recursive: true });
|
|
61
|
+
await writeFile(configPath, `${config.trimEnd()}\n`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const written = decision.apply ? catalog.current : readRootSettings(config);
|
|
65
|
+
const managed = decision.apply ? true : decision.managed;
|
|
66
|
+
return { changed, written, managed };
|
|
50
67
|
}
|
|
51
68
|
|
|
52
69
|
function shouldApplyCatalog(config, catalog, previousState) {
|
|
@@ -4,6 +4,7 @@ import { tmpdir } from "node:os";
|
|
|
4
4
|
import { dirname, join } from "node:path";
|
|
5
5
|
import test from "node:test";
|
|
6
6
|
|
|
7
|
+
import { disableMultiAgentV2IfForced } from "../scripts/migrate-codex-config/multi-agent-v2-guard.mjs";
|
|
7
8
|
import { ensureCodexReasoningConfig, migrateCodexConfig } from "../scripts/migrate-codex-config.mjs";
|
|
8
9
|
|
|
9
10
|
test("#given stale root reasoning config #when ensuring config #then replaces stale values without duplicate keys", () => {
|
|
@@ -361,6 +362,84 @@ test("#given config already matches current catalog #when catalog version advanc
|
|
|
361
362
|
assert.equal(state.files[configPath].catalogVersion, "test.role-only");
|
|
362
363
|
});
|
|
363
364
|
|
|
365
|
+
test("#given installer-forced multi_agent_v2 enabled #when migrating config #then disables it so runtime decides per model", () => {
|
|
366
|
+
const config = [
|
|
367
|
+
'model = "gpt-5.5"',
|
|
368
|
+
'model_reasoning_effort = "high"',
|
|
369
|
+
"",
|
|
370
|
+
"[features.multi_agent_v2]",
|
|
371
|
+
"enabled = true",
|
|
372
|
+
"max_concurrent_threads_per_session = 10000",
|
|
373
|
+
"",
|
|
374
|
+
].join("\n");
|
|
375
|
+
|
|
376
|
+
const result = disableMultiAgentV2IfForced(config);
|
|
377
|
+
|
|
378
|
+
assert.match(result, /enabled = false/);
|
|
379
|
+
assert.doesNotMatch(result, /enabled = true/);
|
|
380
|
+
assert.match(result, /max_concurrent_threads_per_session = 10000/);
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
test("#given no multi_agent_v2 section #when migrating config #then returns config unchanged", () => {
|
|
384
|
+
const config = [
|
|
385
|
+
'model = "gpt-5.5"',
|
|
386
|
+
'model_reasoning_effort = "high"',
|
|
387
|
+
"",
|
|
388
|
+
"[features]",
|
|
389
|
+
"plugins = true",
|
|
390
|
+
"",
|
|
391
|
+
].join("\n");
|
|
392
|
+
|
|
393
|
+
const result = disableMultiAgentV2IfForced(config);
|
|
394
|
+
|
|
395
|
+
assert.equal(result, config);
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
test("#given multi_agent_v2 already disabled #when migrating config #then returns config unchanged", () => {
|
|
399
|
+
const config = [
|
|
400
|
+
'model = "gpt-5.5"',
|
|
401
|
+
'model_reasoning_effort = "high"',
|
|
402
|
+
"",
|
|
403
|
+
"[features.multi_agent_v2]",
|
|
404
|
+
"enabled = false",
|
|
405
|
+
"max_concurrent_threads_per_session = 10000",
|
|
406
|
+
"",
|
|
407
|
+
].join("\n");
|
|
408
|
+
|
|
409
|
+
const result = disableMultiAgentV2IfForced(config);
|
|
410
|
+
|
|
411
|
+
assert.equal(result, config);
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
test("#given global config with forced multi_agent_v2 #when full migration runs #then disables it on disk", async () => {
|
|
415
|
+
const root = await mkdtemp(join(tmpdir(), "lazycodex-multi-agent-v2-guard-"));
|
|
416
|
+
const codexHome = join(root, "codex-home");
|
|
417
|
+
await mkdir(codexHome, { recursive: true });
|
|
418
|
+
const configPath = join(codexHome, "config.toml");
|
|
419
|
+
await writeFile(
|
|
420
|
+
configPath,
|
|
421
|
+
[
|
|
422
|
+
'model = "gpt-5.5"',
|
|
423
|
+
'model_reasoning_effort = "high"',
|
|
424
|
+
"",
|
|
425
|
+
"[features.multi_agent_v2]",
|
|
426
|
+
"enabled = true",
|
|
427
|
+
"max_concurrent_threads_per_session = 10000",
|
|
428
|
+
"",
|
|
429
|
+
].join("\n"),
|
|
430
|
+
);
|
|
431
|
+
|
|
432
|
+
const result = await migrateCodexConfig({
|
|
433
|
+
env: { CODEX_HOME: codexHome, LAZYCODEX_MODEL_CATALOG_STATE_PATH: join(root, "model-state.json") },
|
|
434
|
+
cwd: root,
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
assert.deepEqual(result.changed, [configPath]);
|
|
438
|
+
const content = await readFile(configPath, "utf8");
|
|
439
|
+
assert.match(content, /enabled = false/);
|
|
440
|
+
assert.doesNotMatch(content, /enabled = true/);
|
|
441
|
+
});
|
|
442
|
+
|
|
364
443
|
async function canCreateSymlink(type) {
|
|
365
444
|
const root = await mkdtemp(join(tmpdir(), "lazycodex-symlink-capability-"));
|
|
366
445
|
const target = join(root, "target");
|
|
@@ -3,6 +3,16 @@ import { appendBlock, findTomlSection, removeSetting, replaceOrInsertSetting } f
|
|
|
3
3
|
const CODEX_MULTI_AGENT_V2_HEADER = "features.multi_agent_v2";
|
|
4
4
|
const CODEX_MULTI_AGENT_V2_MAX_CONCURRENT_THREADS_PER_SESSION = 10000;
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Configure multi_agent_v2 thread limits without forcing the feature on.
|
|
8
|
+
*
|
|
9
|
+
* Whether V2 is active is determined at runtime by the model's server-side
|
|
10
|
+
* catalog entry (ModelInfo.multi_agent_version). Forcing enabled = true
|
|
11
|
+
* in config breaks models whose API does not support encrypted tool
|
|
12
|
+
* parameters. The installer therefore only sets the tuning knob
|
|
13
|
+
* (max_concurrent_threads_per_session) so that sessions that DO activate
|
|
14
|
+
* V2 benefit from the higher limit.
|
|
15
|
+
*/
|
|
6
16
|
export function ensureCodexMultiAgentV2Config(config) {
|
|
7
17
|
const normalizedConfig = removeLegacyAgentsMaxThreadsSetting(removeFeatureFlagSetting(config, "multi_agent_v2"));
|
|
8
18
|
const section = findTomlSection(normalizedConfig, CODEX_MULTI_AGENT_V2_HEADER);
|
|
@@ -10,19 +20,11 @@ export function ensureCodexMultiAgentV2Config(config) {
|
|
|
10
20
|
if (!section) {
|
|
11
21
|
return appendBlock(
|
|
12
22
|
normalizedConfig,
|
|
13
|
-
`[${CODEX_MULTI_AGENT_V2_HEADER}]\
|
|
23
|
+
`[${CODEX_MULTI_AGENT_V2_HEADER}]\nmax_concurrent_threads_per_session = ${maxThreadsValue}\n`,
|
|
14
24
|
);
|
|
15
25
|
}
|
|
16
26
|
|
|
17
|
-
|
|
18
|
-
const updatedSection = findTomlSection(enabledConfig, CODEX_MULTI_AGENT_V2_HEADER);
|
|
19
|
-
if (!updatedSection) {
|
|
20
|
-
return appendBlock(
|
|
21
|
-
enabledConfig,
|
|
22
|
-
`[${CODEX_MULTI_AGENT_V2_HEADER}]\nenabled = true\nmax_concurrent_threads_per_session = ${maxThreadsValue}\n`,
|
|
23
|
-
);
|
|
24
|
-
}
|
|
25
|
-
return replaceOrInsertSetting(enabledConfig, updatedSection, "max_concurrent_threads_per_session", maxThreadsValue);
|
|
27
|
+
return replaceOrInsertSetting(normalizedConfig, section, "max_concurrent_threads_per_session", maxThreadsValue);
|
|
26
28
|
}
|
|
27
29
|
|
|
28
30
|
function removeFeatureFlagSetting(config, featureName) {
|