fix: solve current-session resolution and valid session creation for sparse channel-plugin requesters

This commit is contained in:
bittoby
2026-04-29 15:27:52 +02:00
committed by Ayaan Zaidi
parent 8ace33be67
commit d5ad90f7ec
2 changed files with 88 additions and 6 deletions

View File

@@ -590,6 +590,76 @@ describe("session_status tool", () => {
expect(details.statusText).toContain("🧠 Model:");
});
it("resolves the default session_status lookup for a channel-plugin requester via implicit fallback", async () => {
resetSessionStore({});
const tool = getSessionStatusTool("agent:main:scope:scopy:direct:scopy");
const result = await tool.execute("call-current-channel-plugin-default", {});
const details = result.details as { ok?: boolean; sessionKey?: string; statusText?: string };
expect(details.ok).toBe(true);
expect(details.sessionKey).toBe("agent:main:scope:scopy:direct:scopy");
expect(details.statusText).toContain("OpenClaw");
expect(details.statusText).toContain("🧠 Model:");
});
it("materializes a valid persisted session entry when implicit current fallback mutates model state", async () => {
resetSessionStore({});
const tool = getSessionStatusTool("agent:main:scope:scopy:direct:scopy");
const result = await tool.execute("call-current-channel-plugin-model", {
sessionKey: "current",
model: "anthropic/claude-sonnet-4-6",
});
const details = result.details as { ok?: boolean; sessionKey?: string };
expect(details.ok).toBe(true);
expect(details.sessionKey).toBe("agent:main:scope:scopy:direct:scopy");
expect(updateSessionStoreMock).toHaveBeenCalled();
const [, savedStore] = updateSessionStoreMock.mock.calls.at(-1) as [
string,
Record<string, SessionEntry>,
];
const saved = savedStore["agent:main:scope:scopy:direct:scopy"];
expect(saved).toEqual(
expect.objectContaining({
providerOverride: "anthropic",
modelOverride: "claude-sonnet-4-6",
liveModelSwitchPending: true,
}),
);
expect(saved.sessionId).toEqual(expect.any(String));
expect(saved.sessionId.trim().length).toBeGreaterThan(0);
});
it("materializes a valid persisted session entry when the default implicit current fallback mutates model state", async () => {
resetSessionStore({});
const tool = getSessionStatusTool("agent:main:scope:scopy:direct:scopy");
const result = await tool.execute("call-current-channel-plugin-default-model", {
model: "anthropic/claude-sonnet-4-6",
});
const details = result.details as { ok?: boolean; sessionKey?: string };
expect(details.ok).toBe(true);
expect(details.sessionKey).toBe("agent:main:scope:scopy:direct:scopy");
expect(updateSessionStoreMock).toHaveBeenCalled();
const [, savedStore] = updateSessionStoreMock.mock.calls.at(-1) as [
string,
Record<string, SessionEntry>,
];
const saved = savedStore["agent:main:scope:scopy:direct:scopy"];
expect(saved).toEqual(
expect.objectContaining({
providerOverride: "anthropic",
modelOverride: "claude-sonnet-4-6",
liveModelSwitchPending: true,
}),
);
expect(saved.sessionId).toEqual(expect.any(String));
expect(saved.sessionId.trim().length).toBeGreaterThan(0);
});
it("does not synthesize a current fallback for unknown non-literal session keys", async () => {
resetSessionStore({});

View File

@@ -8,6 +8,7 @@ import type {
import { getRuntimeConfig } from "../../config/config.js";
import {
loadSessionStore,
mergeSessionEntry,
resolveStorePath,
type SessionEntry,
updateSessionStore,
@@ -145,11 +146,11 @@ function synthesizeImplicitCurrentSessionEntry(): SessionEntry {
}
function resolveImplicitCurrentSessionFallback(params: {
requestedKeyRaw: string;
allowFallback: boolean;
storeScopedRequesterKey: string;
}): { key: string; entry: SessionEntry } | null {
const requesterKey = params.storeScopedRequesterKey.trim();
if (params.requestedKeyRaw !== "current" || !requesterKey) {
if (!params.allowFallback || !requesterKey) {
return null;
}
return {
@@ -484,7 +485,7 @@ export function createSessionStatusTool(opts?: {
if (!resolved) {
const fallback = resolveImplicitCurrentSessionFallback({
requestedKeyRaw,
allowFallback: requestedKeyRaw === "current" || requestedKeyParam === undefined,
storeScopedRequesterKey,
});
if (fallback) {
@@ -539,11 +540,22 @@ export function createSessionStatusTool(opts?: {
markLiveSwitchPending: true,
});
if (applied.updated) {
store[resolved.key] = nextEntry;
const persistedEntry = nextEntry.sessionId.trim()
? nextEntry
: (() => {
const persistedEntryPatch: Partial<SessionEntry> = { ...nextEntry };
delete persistedEntryPatch.sessionId;
const existingEntry = store[resolved.key];
const existingWithValidSessionId = existingEntry?.sessionId?.trim()
? existingEntry
: undefined;
return mergeSessionEntry(existingWithValidSessionId, persistedEntryPatch);
})();
store[resolved.key] = persistedEntry;
await updateSessionStore(storePath, (nextStore) => {
nextStore[resolved.key] = nextEntry;
nextStore[resolved.key] = persistedEntry;
});
resolved.entry = nextEntry;
resolved.entry = persistedEntry;
changedModel = true;
}
}