Skip to content

Commit 126760e

Browse files
author
Ash (Bug Lab)
committed
fix(config): migrate legacy acp.stream keys on load
Fix issue #35957 by migrating old v2026.3.2 acp.stream keys to the current schema during normal legacy-config migration. - maxTurnChars -> maxOutputChars - maxToolSummaryChars -> maxSessionUpdateChars - remove maxStatusChars, maxMetaEventsPerTurn, metaMode, showUsage Add regression coverage for raw validation, migration output, normal config validation, and non-overwrite behavior when new keys already exist. Verified with Bug Lab fixture-backed check: - current main => bug_present / code-suspect - patched worktree => fixed / cannot_reproduce
1 parent 3f042ed commit 126760e

File tree

2 files changed

+197
-0
lines changed

2 files changed

+197
-0
lines changed
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import { describe, expect, it } from "vitest";
2+
import { migrateLegacyConfig } from "./legacy-migrate.js";
3+
import { validateConfigObjectRaw, validateConfigObjectWithPlugins } from "./validation.js";
4+
5+
const LEGACY_ACP_STREAM_FIXTURE = {
6+
acp: {
7+
stream: {
8+
maxTurnChars: 5000,
9+
maxToolSummaryChars: 1000,
10+
maxStatusChars: 400,
11+
maxMetaEventsPerTurn: 6,
12+
metaMode: "full",
13+
showUsage: true,
14+
},
15+
},
16+
};
17+
18+
describe("acp.stream legacy key migration (issue #35957)", () => {
19+
it("flags old acp.stream keys during raw validation", () => {
20+
const result = validateConfigObjectRaw(LEGACY_ACP_STREAM_FIXTURE);
21+
expect(result.ok).toBe(false);
22+
if (result.ok) {
23+
return;
24+
}
25+
expect(result.issues.map((issue) => issue.path)).toEqual(
26+
expect.arrayContaining([
27+
"acp.stream.maxTurnChars",
28+
"acp.stream.maxToolSummaryChars",
29+
"acp.stream.maxStatusChars",
30+
"acp.stream.maxMetaEventsPerTurn",
31+
"acp.stream.metaMode",
32+
"acp.stream.showUsage",
33+
]),
34+
);
35+
});
36+
37+
it("migrates old acp.stream keys to supported config", () => {
38+
const result = migrateLegacyConfig(LEGACY_ACP_STREAM_FIXTURE);
39+
expect(result.config).not.toBeNull();
40+
expect(result.changes).toEqual(
41+
expect.arrayContaining([
42+
"Moved acp.stream.maxTurnChars → acp.stream.maxOutputChars.",
43+
"Moved acp.stream.maxToolSummaryChars → acp.stream.maxSessionUpdateChars.",
44+
"Removed acp.stream.maxStatusChars (no replacement).",
45+
"Removed acp.stream.maxMetaEventsPerTurn (no replacement).",
46+
"Removed acp.stream.metaMode (no replacement).",
47+
"Removed acp.stream.showUsage (no replacement).",
48+
]),
49+
);
50+
expect(result.config?.acp?.stream).toEqual({
51+
maxOutputChars: 5000,
52+
maxSessionUpdateChars: 1000,
53+
});
54+
});
55+
56+
it("accepts legacy acp.stream keys through normal config validation after migration", () => {
57+
const result = validateConfigObjectWithPlugins(LEGACY_ACP_STREAM_FIXTURE);
58+
expect(result.ok).toBe(true);
59+
if (!result.ok) {
60+
return;
61+
}
62+
expect(result.config.acp?.stream).toEqual({
63+
maxOutputChars: 5000,
64+
maxSessionUpdateChars: 1000,
65+
});
66+
});
67+
68+
it("does not overwrite new keys that are already set", () => {
69+
const result = migrateLegacyConfig({
70+
acp: {
71+
stream: {
72+
maxTurnChars: 5000,
73+
maxToolSummaryChars: 1000,
74+
maxOutputChars: 9000,
75+
maxSessionUpdateChars: 300,
76+
},
77+
},
78+
});
79+
80+
expect(result.config).not.toBeNull();
81+
expect(result.changes).toEqual(
82+
expect.arrayContaining([
83+
"Removed acp.stream.maxTurnChars (acp.stream.maxOutputChars already set).",
84+
"Removed acp.stream.maxToolSummaryChars (acp.stream.maxSessionUpdateChars already set).",
85+
]),
86+
);
87+
expect(result.config?.acp?.stream).toEqual({
88+
maxOutputChars: 9000,
89+
maxSessionUpdateChars: 300,
90+
});
91+
});
92+
});

src/config/legacy.migrations.runtime.ts

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,36 @@ const HEARTBEAT_RULE: LegacyConfigRule = {
317317
"top-level heartbeat is not a valid config path; use agents.defaults.heartbeat (cadence/target/model settings) or channels.defaults.heartbeat (showOk/showAlerts/useIndicator).",
318318
};
319319

320+
const LEGACY_ACP_STREAM_RULES: LegacyConfigRule[] = [
321+
{
322+
path: ["acp", "stream", "maxTurnChars"],
323+
message:
324+
"acp.stream.maxTurnChars was renamed; use acp.stream.maxOutputChars instead (auto-migrated on load).",
325+
},
326+
{
327+
path: ["acp", "stream", "maxToolSummaryChars"],
328+
message:
329+
"acp.stream.maxToolSummaryChars was renamed; use acp.stream.maxSessionUpdateChars instead (auto-migrated on load).",
330+
},
331+
{
332+
path: ["acp", "stream", "maxStatusChars"],
333+
message: "acp.stream.maxStatusChars was removed with no replacement (auto-removed on load).",
334+
},
335+
{
336+
path: ["acp", "stream", "maxMetaEventsPerTurn"],
337+
message:
338+
"acp.stream.maxMetaEventsPerTurn was removed with no replacement (auto-removed on load).",
339+
},
340+
{
341+
path: ["acp", "stream", "metaMode"],
342+
message: "acp.stream.metaMode was removed with no replacement (auto-removed on load).",
343+
},
344+
{
345+
path: ["acp", "stream", "showUsage"],
346+
message: "acp.stream.showUsage was removed with no replacement (auto-removed on load).",
347+
},
348+
];
349+
320350
const X_SEARCH_RULE: LegacyConfigRule = {
321351
path: ["tools", "web", "x_search", "apiKey"],
322352
message:
@@ -386,6 +416,68 @@ function migrateLegacySandboxPerSession(
386416
}
387417
}
388418

419+
function migrateLegacyAcpStream(stream: Record<string, unknown>, changes: string[]): void {
420+
if (
421+
Object.prototype.hasOwnProperty.call(stream, "maxTurnChars") &&
422+
typeof stream.maxTurnChars === "number"
423+
) {
424+
if (stream.maxOutputChars === undefined) {
425+
stream.maxOutputChars = stream.maxTurnChars;
426+
changes.push("Moved acp.stream.maxTurnChars → acp.stream.maxOutputChars.");
427+
} else {
428+
changes.push("Removed acp.stream.maxTurnChars (acp.stream.maxOutputChars already set).");
429+
}
430+
delete stream.maxTurnChars;
431+
}
432+
433+
if (
434+
Object.prototype.hasOwnProperty.call(stream, "maxToolSummaryChars") &&
435+
typeof stream.maxToolSummaryChars === "number"
436+
) {
437+
if (stream.maxSessionUpdateChars === undefined) {
438+
stream.maxSessionUpdateChars = stream.maxToolSummaryChars;
439+
changes.push("Moved acp.stream.maxToolSummaryChars → acp.stream.maxSessionUpdateChars.");
440+
} else {
441+
changes.push(
442+
"Removed acp.stream.maxToolSummaryChars (acp.stream.maxSessionUpdateChars already set).",
443+
);
444+
}
445+
delete stream.maxToolSummaryChars;
446+
}
447+
448+
if (
449+
Object.prototype.hasOwnProperty.call(stream, "maxStatusChars") &&
450+
typeof stream.maxStatusChars === "number"
451+
) {
452+
delete stream.maxStatusChars;
453+
changes.push("Removed acp.stream.maxStatusChars (no replacement).");
454+
}
455+
456+
if (
457+
Object.prototype.hasOwnProperty.call(stream, "maxMetaEventsPerTurn") &&
458+
typeof stream.maxMetaEventsPerTurn === "number"
459+
) {
460+
delete stream.maxMetaEventsPerTurn;
461+
changes.push("Removed acp.stream.maxMetaEventsPerTurn (no replacement).");
462+
}
463+
464+
if (
465+
Object.prototype.hasOwnProperty.call(stream, "metaMode") &&
466+
typeof stream.metaMode === "string"
467+
) {
468+
delete stream.metaMode;
469+
changes.push("Removed acp.stream.metaMode (no replacement).");
470+
}
471+
472+
if (
473+
Object.prototype.hasOwnProperty.call(stream, "showUsage") &&
474+
typeof stream.showUsage === "boolean"
475+
) {
476+
delete stream.showUsage;
477+
changes.push("Removed acp.stream.showUsage (no replacement).");
478+
}
479+
}
480+
389481
export const LEGACY_CONFIG_MIGRATIONS_RUNTIME: LegacyConfigMigrationSpec[] = [
390482
defineLegacyConfigMigration({
391483
id: "agents.sandbox.perSession->scope",
@@ -436,6 +528,19 @@ export const LEGACY_CONFIG_MIGRATIONS_RUNTIME: LegacyConfigMigrationSpec[] = [
436528
changes.push(...migrated.changes);
437529
},
438530
}),
531+
defineLegacyConfigMigration({
532+
id: "acp.stream-v2026.3.2-keys",
533+
describe: "Migrate removed ACP stream keys from v2026.3.2 to supported config",
534+
legacyRules: LEGACY_ACP_STREAM_RULES,
535+
apply: (raw, changes) => {
536+
const acp = getRecord(raw.acp);
537+
const stream = getRecord(acp?.stream);
538+
if (!stream) {
539+
return;
540+
}
541+
migrateLegacyAcpStream(stream, changes);
542+
},
543+
}),
439544
defineLegacyConfigMigration({
440545
// v2026.2.26 added a startup guard requiring gateway.controlUi.allowedOrigins (or the
441546
// host-header fallback flag) for any non-loopback bind. The setup wizard was updated

0 commit comments

Comments
 (0)