

























@@ -99,9 +99,77 @@ function installScopedSessionStores(syncUpdates = false) {
9999async function createSessionsModuleMock() {
100100const actual =
101101await vi.importActual<typeof import("../config/sessions.js")>("../config/sessions.js");
102+const resolveMockStorePath = (_store: string | undefined, opts?: { agentId?: string }) =>
103+opts?.agentId === "support" ? "/tmp/support/sessions.json" : "/tmp/main/sessions.json";
104+const cloneEntry = (entry: SessionEntry): SessionEntry => structuredClone(entry);
102105return {
103106 ...actual,
104107loadSessionStore: (storePath: string) => loadSessionStoreMock(storePath),
108+patchSessionEntryWithKey: async (
109+scope: { agentId?: string; sessionKey: string; storePath?: string },
110+update: (
111+entry: SessionEntry,
112+context: { existingEntry?: SessionEntry },
113+) => Promise<Partial<SessionEntry> | null> | Partial<SessionEntry> | null,
114+options?: { fallbackEntry?: SessionEntry; replaceEntry?: boolean },
115+) => {
116+const storePath =
117+scope.storePath ?? resolveMockStorePath(undefined, { agentId: scope.agentId });
118+const store = loadSessionStoreMock(storePath) as Record<string, SessionEntry>;
119+const resolved = actual.resolveSessionStoreEntry({ store, sessionKey: scope.sessionKey });
120+const existing = resolved.existing ?? options?.fallbackEntry;
121+if (!existing) {
122+return null;
123+}
124+const patch = await update(cloneEntry(existing), {
125+existingEntry: resolved.existing ? cloneEntry(resolved.existing) : undefined,
126+});
127+if (!patch) {
128+return { sessionKey: resolved.normalizedKey, entry: cloneEntry(existing) };
129+}
130+const next = options?.replaceEntry
131+ ? cloneEntry(patch as SessionEntry)
132+ : actual.mergeSessionEntry(existing, patch);
133+store[resolved.normalizedKey] = next;
134+updateSessionStoreMock(storePath, store);
135+return { sessionKey: resolved.normalizedKey, entry: cloneEntry(next) };
136+},
137+resolveSessionEntryCandidateTarget: (scope: {
138+agentId: string;
139+candidateKeys: readonly string[];
140+cfg: { session?: { store?: string } };
141+fallback?: { sessionKey: string; entry: SessionEntry };
142+}) => {
143+const storePath = resolveMockStorePath(scope.cfg.session?.store, { agentId: scope.agentId });
144+const store = loadSessionStoreMock(storePath) as Record<string, SessionEntry>;
145+const candidates = [...new Set(scope.candidateKeys.map((key) => key.trim()))];
146+for (const candidateKey of candidates) {
147+if (!candidateKey) {
148+continue;
149+}
150+const resolved = actual.resolveSessionStoreEntry({ store, sessionKey: candidateKey });
151+if (!resolved.existing) {
152+continue;
153+}
154+return {
155+agentId: scope.agentId,
156+ candidateKey,
157+entry: cloneEntry(resolved.existing),
158+persisted: true,
159+sessionKey: resolved.normalizedKey,
160+};
161+}
162+const fallbackKey = scope.fallback?.sessionKey.trim();
163+return fallbackKey && scope.fallback
164+ ? {
165+agentId: scope.agentId,
166+candidateKey: fallbackKey,
167+entry: cloneEntry(scope.fallback.entry),
168+persisted: false,
169+sessionKey: fallbackKey,
170+}
171+ : null;
172+},
105173updateSessionStore: async (
106174storePath: string,
107175mutator: (store: Record<string, unknown>) => Promise<void> | void,
@@ -111,8 +179,7 @@ async function createSessionsModuleMock() {
111179updateSessionStoreMock(storePath, store);
112180return store;
113181},
114-resolveStorePath: (_store: string | undefined, opts?: { agentId?: string }) =>
115-opts?.agentId === "support" ? "/tmp/support/sessions.json" : "/tmp/main/sessions.json",
182+resolveStorePath: resolveMockStorePath,
116183};
117184}
118185@@ -1191,6 +1258,41 @@ describe("session_status tool", () => {
11911258expect(saved.sessionId).toMatch(UUID_RE);
11921259});
119312601261+it("preserves an existing legacy main row when implicit fallback mutates model state", async () => {
1262+resetSessionStore({
1263+main: {
1264+sessionId: "legacy-main-session",
1265+updatedAt: 10,
1266+label: "Legacy Main",
1267+lastChannel: "telegram",
1268+},
1269+});
1270+1271+const tool = getSessionStatusTool("agent:main:main");
1272+1273+const result = await tool.execute("call-legacy-main-fallback-model", {
1274+model: "anthropic/claude-sonnet-4-6",
1275+});
1276+const details = result.details as {
1277+ok?: boolean;
1278+sessionKey?: string;
1279+modelOverride?: string | null;
1280+};
1281+expect(details.ok).toBe(true);
1282+expect(details.sessionKey).toBe("main");
1283+expect(details.modelOverride).toBe("anthropic/claude-sonnet-4-6");
1284+expect(updateSessionStoreMock).toHaveBeenCalledTimes(1);
1285+const savedStore = latestMockCallArg(updateSessionStoreMock, 1) as Record<string, SessionEntry>;
1286+expect(savedStore.main).toMatchObject({
1287+sessionId: "legacy-main-session",
1288+label: "Legacy Main",
1289+lastChannel: "telegram",
1290+providerOverride: "anthropic",
1291+modelOverride: "claude-sonnet-4-6",
1292+liveModelSwitchPending: true,
1293+});
1294+});
1295+11941296it("fires session:patch when session_status changes the persisted session model", async () => {
11951297const events: InternalHookEvent[] = [];
11961298registerInternalHook("session:patch", async (event) => {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。