Skip to content

Commit 5373df4

Browse files
committed
fix(config): preserve explicit main route bindings
1 parent cadad3b commit 5373df4

6 files changed

Lines changed: 98 additions & 4 deletions

File tree

src/commands/doctor/shared/legacy-config-binding-repair.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// Repairs canonical binding references after agent config migration.
22
import type { OpenClawConfig } from "../../../config/types.openclaw.js";
3-
import { normalizeAgentId } from "../../../routing/session-key.js";
3+
import { DEFAULT_AGENT_ID, normalizeAgentId } from "../../../routing/session-key.js";
44

55
export function pruneBindingsForMissingAgents(
66
cfg: OpenClawConfig,
@@ -22,7 +22,11 @@ export function pruneBindingsForMissingAgents(
2222
const agentIds = new Set(validAgents.map((agent) => normalizeAgentId(agent.id)));
2323
const nextBindings = bindings.filter((binding) => {
2424
const agentId = binding && typeof binding === "object" ? binding.agentId : undefined;
25-
return typeof agentId !== "string" || agentIds.has(normalizeAgentId(agentId));
25+
return (
26+
typeof agentId !== "string" ||
27+
agentId === DEFAULT_AGENT_ID ||
28+
agentIds.has(normalizeAgentId(agentId))
29+
);
2630
});
2731
const removed = bindings.length - nextBindings.length;
2832
if (removed === 0) {

src/commands/doctor/shared/legacy-config-migrate.test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,22 @@ describe("compatibility binding repair migrate", () => {
5454
expect(res.changes).toContain("Removed 1 binding that referenced missing agents.list ids.");
5555
});
5656

57+
it("preserves exact main bindings because the implicit main agent always exists", () => {
58+
const res = repairBindingsForTest({
59+
agents: {
60+
list: [{ id: "alpha" }],
61+
},
62+
bindings: [
63+
{ agentId: "main", match: { channel: "discord" } },
64+
{ agentId: "MAIN", match: { channel: "discord" } },
65+
{ agentId: "ghost", match: { channel: "discord" } },
66+
],
67+
} as OpenClawConfig);
68+
69+
expect(res.config.bindings).toEqual([{ agentId: "main", match: { channel: "discord" } }]);
70+
expect(res.changes).toContain("Removed 2 bindings that referenced missing agents.list ids.");
71+
});
72+
5773
it("leaves bindings untouched when agents.list has malformed entries", () => {
5874
const cfg = {
5975
agents: {

src/config/config.schema-regressions.test.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -415,6 +415,43 @@ describe("config schema regressions", () => {
415415
expect(res.ok).toBe(true);
416416
});
417417

418+
it("accepts exact main bindings when agents.list omits the implicit main agent", () => {
419+
const res = validateConfigObject({
420+
agents: {
421+
list: [{ id: "alpha", model: "anthropic/claude-3-5-sonnet" }],
422+
},
423+
bindings: [
424+
{
425+
type: "route",
426+
agentId: "main",
427+
match: { channel: "discord", peer: { kind: "direct", id: "user-1" } },
428+
},
429+
],
430+
});
431+
432+
expect(res.ok).toBe(true);
433+
});
434+
435+
it("rejects normalized main binding variants when agents.list omits them", () => {
436+
const res = validateConfigObject({
437+
agents: {
438+
list: [{ id: "alpha", model: "anthropic/claude-3-5-sonnet" }],
439+
},
440+
bindings: [
441+
{
442+
type: "route",
443+
agentId: "MAIN",
444+
match: { channel: "discord", peer: { kind: "direct", id: "user-1" } },
445+
},
446+
],
447+
});
448+
449+
expect(res.ok).toBe(false);
450+
if (!res.ok) {
451+
expect(res.issues.some((iss) => iss.message.includes('Unknown agent id "MAIN"'))).toBe(true);
452+
}
453+
});
454+
418455
it("skips binding agentId check when agents.list is empty (legacy passthrough)", () => {
419456
const res = validateConfigObject({
420457
bindings: [

src/config/zod-schema.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { z } from "zod";
2-
import { normalizeAgentId } from "../routing/session-key.js";
2+
import { DEFAULT_AGENT_ID, normalizeAgentId } from "../routing/session-key.js";
33
import { OpenClawSchemaShape } from "./zod-schema.root-shape.js";
44

55
// zod@4 ships "sideEffects": false, so bundlers tree-shake the classic entry's
@@ -31,7 +31,11 @@ export const OpenClawSchema = z.strictObject(OpenClawSchemaShape).superRefine((c
3131
continue;
3232
}
3333
const agentId = (binding as { agentId?: unknown }).agentId;
34-
if (typeof agentId === "string" && !effectiveAgentIds.has(normalizeAgentId(agentId))) {
34+
if (
35+
typeof agentId === "string" &&
36+
agentId !== DEFAULT_AGENT_ID &&
37+
!effectiveAgentIds.has(normalizeAgentId(agentId))
38+
) {
3539
ctx.addIssue({
3640
code: z.ZodIssueCode.custom,
3741
path: ["bindings", idx, "agentId"],

src/routing/resolve-route.test.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,35 @@ describe("resolveAgentRoute", () => {
116116
});
117117
});
118118

119+
test("preserves explicit main bindings when agents.list has other agents", () => {
120+
const cfg: OpenClawConfig = {
121+
agents: {
122+
list: [{ id: "alpha" }],
123+
},
124+
bindings: [
125+
{
126+
type: "route",
127+
agentId: "main",
128+
match: { channel: "discord", accountId: "default" },
129+
},
130+
],
131+
};
132+
133+
const route = resolveAgentRoute({
134+
cfg,
135+
channel: "discord",
136+
accountId: "default",
137+
peer: { kind: "direct", id: "user-1" },
138+
});
139+
140+
expectResolvedRoute(route, {
141+
agentId: "main",
142+
sessionKey: "agent:main:main",
143+
matchedBy: "binding.account",
144+
lastRoutePolicy: "main",
145+
});
146+
});
147+
119148
test("uses the configured main session key for shared direct routes", () => {
120149
const route = resolveRoute({
121150
cfg: { session: { dmScope: "main", mainKey: "work" } },

src/routing/resolve-route.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
buildAgentMainSessionKey,
1818
buildAgentPeerSessionKey,
1919
DEFAULT_ACCOUNT_ID,
20+
DEFAULT_AGENT_ID,
2021
DEFAULT_MAIN_KEY,
2122
normalizeAccountId,
2223
normalizeAgentId,
@@ -160,6 +161,9 @@ export function pickFirstExistingAgentId(cfg: OpenClawConfig, agentId: string):
160161
return lookup.fallbackDefaultAgentId;
161162
}
162163
const normalized = normalizeAgentId(trimmed);
164+
if (normalized === DEFAULT_AGENT_ID) {
165+
return DEFAULT_AGENT_ID;
166+
}
163167
if (lookup.byNormalizedId.size === 0) {
164168
return sanitizeAgentId(trimmed);
165169
}

0 commit comments

Comments
 (0)