clawdbot-55f: route sdk session reads through seam

This commit is contained in:
Josh Lehman
2026-05-31 18:17:27 -07:00
parent da45b43789
commit a61602995b
5 changed files with 320 additions and 16 deletions

View File

@@ -5,6 +5,12 @@
*/
import { loadSessionStore as loadSessionStoreImpl } from "../config/sessions/store-load.js";
export {
getSessionEntry,
listSessionEntries,
updateSessionStoreEntry,
upsertSessionEntry,
} from "./session-store-runtime.js";
/**
* @deprecated Use getSessionEntry/listSessionEntries for reads and
@@ -141,16 +147,12 @@ export type {
} from "../config/types.js";
export {
clearSessionStoreCacheForTest,
getSessionEntry,
listSessionEntries,
patchSessionEntry,
readSessionUpdatedAt,
recordSessionMetaFromInbound,
saveSessionStore,
updateLastRoute,
updateSessionStore,
updateSessionStoreEntry,
upsertSessionEntry,
resolveSessionStoreEntry,
} from "../config/sessions/store.js";
export { resolveSessionKey } from "../config/sessions/session-key.js";

View File

@@ -0,0 +1,94 @@
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import {
getSessionEntry,
listSessionEntries,
updateSessionStoreEntry,
upsertSessionEntry,
} from "./session-store-runtime.js";
describe("session-store-runtime compatibility surface", () => {
let tempDir: string;
let storePath: string;
beforeEach(() => {
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-sdk-session-store-"));
storePath = path.join(tempDir, "sessions.json");
});
afterEach(() => {
fs.rmSync(tempDir, { recursive: true, force: true });
});
it("keeps the public session read shape while using the accessor-backed exports", async () => {
const sessionKey = "agent:main:main";
await upsertSessionEntry({
sessionKey,
storePath,
entry: {
model: "gpt-5.5",
sessionId: "session-1",
updatedAt: 10,
},
});
expect(getSessionEntry({ sessionKey, storePath })).toMatchObject({
model: "gpt-5.5",
sessionId: "session-1",
updatedAt: 10,
});
expect(listSessionEntries({ storePath })).toEqual([
{
sessionKey,
entry: expect.objectContaining({
model: "gpt-5.5",
sessionId: "session-1",
updatedAt: 10,
}),
},
]);
await upsertSessionEntry({
sessionKey,
storePath,
entry: {
sessionId: "session-1",
updatedAt: 20,
},
});
expect(getSessionEntry({ sessionKey, storePath })?.model).toBeUndefined();
});
it("keeps the public entry-update signature while delegating to the seam", async () => {
const sessionKey = "agent:main:main";
await expect(
updateSessionStoreEntry({
sessionKey,
storePath,
update: () => ({ model: "gpt-5.5" }),
}),
).resolves.toBeNull();
await upsertSessionEntry({
sessionKey,
storePath,
entry: {
sessionId: "session-1",
updatedAt: 10,
},
});
await expect(
updateSessionStoreEntry({
sessionKey,
storePath,
update: () => ({ model: "gpt-5.5" }),
}),
).resolves.toMatchObject({
model: "gpt-5.5",
sessionId: "session-1",
});
});
});

View File

@@ -1,6 +1,44 @@
// Narrow session-store helpers for channel hot paths.
import {
listSessionEntries as listAccessorSessionEntries,
loadSessionEntry,
replaceSessionEntry,
updateSessionEntry,
} from "../config/sessions/session-accessor.js";
import { loadSessionStore as loadSessionStoreImpl } from "../config/sessions/store-load.js";
import type { SessionEntry } from "../config/sessions/types.js";
type SessionStoreReadParams = {
agentId?: string;
env?: NodeJS.ProcessEnv;
hydrateSkillPromptRefs?: boolean;
sessionKey: string;
storePath?: string;
};
type SessionStoreListParams = Partial<Omit<SessionStoreReadParams, "sessionKey">>;
type SessionStoreEntrySummary = {
sessionKey: string;
entry: SessionEntry;
};
type SessionStoreEntryUpdate = (
entry: SessionEntry,
) => Promise<Partial<SessionEntry> | null> | Partial<SessionEntry> | null;
type UpdateSessionStoreEntryParams = {
storePath: string;
sessionKey: string;
update: SessionStoreEntryUpdate;
skipMaintenance?: boolean;
takeCacheOwnership?: boolean;
};
type UpsertSessionEntryParams = SessionStoreReadParams & {
entry: SessionEntry;
};
/**
* @deprecated Use getSessionEntry/listSessionEntries for reads and
@@ -9,6 +47,60 @@ import { loadSessionStore as loadSessionStoreImpl } from "../config/sessions/sto
*/
export const loadSessionStore = loadSessionStoreImpl;
/** Loads one session entry through the accessor seam. */
export function getSessionEntry(params: SessionStoreReadParams): SessionEntry | undefined {
return loadSessionEntry({
agentId: params.agentId,
env: params.env,
hydrateSkillPromptRefs: params.hydrateSkillPromptRefs,
sessionKey: params.sessionKey,
storePath: params.storePath,
});
}
/** Lists session entries through the accessor seam. */
export function listSessionEntries(
params: SessionStoreListParams = {},
): SessionStoreEntrySummary[] {
return listAccessorSessionEntries({
agentId: params.agentId,
env: params.env,
hydrateSkillPromptRefs: params.hydrateSkillPromptRefs,
storePath: params.storePath,
});
}
/** Updates an existing session entry through the accessor seam. */
export async function updateSessionStoreEntry(
params: UpdateSessionStoreEntryParams,
): Promise<SessionEntry | null> {
return await updateSessionEntry(
{
sessionKey: params.sessionKey,
storePath: params.storePath,
},
params.update,
{
skipMaintenance: params.skipMaintenance,
takeCacheOwnership: params.takeCacheOwnership,
},
);
}
/** Replaces or creates one session entry through the accessor seam. */
export async function upsertSessionEntry(params: UpsertSessionEntryParams): Promise<void> {
await replaceSessionEntry(
{
agentId: params.agentId,
env: params.env,
hydrateSkillPromptRefs: params.hydrateSkillPromptRefs,
sessionKey: params.sessionKey,
storePath: params.storePath,
},
params.entry,
);
}
export { resolveSessionStoreEntry } from "../config/sessions/store-entry.js";
export {
resolveSessionFilePath,
@@ -22,16 +114,12 @@ export { resolveGroupSessionKey } from "../config/sessions/group.js";
export { canonicalizeMainSessionAlias } from "../config/sessions/main-session.js";
export {
clearSessionStoreCacheForTest,
getSessionEntry,
listSessionEntries,
patchSessionEntry,
readSessionUpdatedAt,
recordSessionMetaFromInbound,
saveSessionStore,
updateLastRoute,
updateSessionStore,
updateSessionStoreEntry,
upsertSessionEntry,
} from "../config/sessions/store.js";
export {
evaluateSessionFreshness,

View File

@@ -11,19 +11,51 @@ import { normalizeThinkLevel, resolveThinkingProfile } from "../../auto-reply/th
import { getRuntimeConfig } from "../../config/config.js";
import { resolveSessionFilePath, resolveStorePath } from "../../config/sessions/paths.js";
import {
getSessionEntry,
listSessionEntries,
listSessionEntries as listAccessorSessionEntries,
loadSessionEntry,
replaceSessionEntry,
updateSessionEntry,
} from "../../config/sessions/session-accessor.js";
import {
loadSessionStore,
patchSessionEntry,
saveSessionStore,
updateSessionStore,
updateSessionStoreEntry,
upsertSessionEntry,
} from "../../config/sessions/store.js";
import type { SessionEntry } from "../../config/sessions/types.js";
import { createLazyRuntimeMethod, createLazyRuntimeModule } from "../../shared/lazy-runtime.js";
import { defineCachedValue } from "./runtime-cache.js";
import type { PluginRuntime } from "./types.js";
type RuntimeSessionStoreReadParams = {
agentId?: string;
env?: NodeJS.ProcessEnv;
hydrateSkillPromptRefs?: boolean;
sessionKey: string;
storePath?: string;
};
type RuntimeSessionStoreListParams = Partial<Omit<RuntimeSessionStoreReadParams, "sessionKey">>;
type RuntimeSessionStoreEntrySummary = {
sessionKey: string;
entry: SessionEntry;
};
type RuntimeSessionStoreEntryUpdateParams = {
storePath: string;
sessionKey: string;
update: (
entry: SessionEntry,
) => Promise<Partial<SessionEntry> | null> | Partial<SessionEntry> | null;
skipMaintenance?: boolean;
takeCacheOwnership?: boolean;
};
type RuntimeUpsertSessionEntryParams = RuntimeSessionStoreReadParams & {
entry: SessionEntry;
};
const loadEmbeddedAgentRuntime = createLazyRuntimeModule(
() => import("./runtime-embedded-agent.runtime.js"),
);
@@ -38,6 +70,60 @@ function resolveRuntimeThinkingCatalog(
return configuredCatalog.length > 0 ? configuredCatalog : undefined;
}
function getSessionEntry(params: RuntimeSessionStoreReadParams): SessionEntry | undefined {
return loadSessionEntry({
agentId: params.agentId,
env: params.env,
hydrateSkillPromptRefs: params.hydrateSkillPromptRefs,
sessionKey: params.sessionKey,
storePath: params.storePath,
});
}
function listSessionEntries(
params: RuntimeSessionStoreListParams = {},
): RuntimeSessionStoreEntrySummary[] {
return listAccessorSessionEntries({
agentId: params.agentId,
env: params.env,
hydrateSkillPromptRefs: params.hydrateSkillPromptRefs,
storePath: params.storePath,
});
}
async function updateSessionStoreEntry(
params: RuntimeSessionStoreEntryUpdateParams,
): Promise<SessionEntry | null> {
// Preserve the plugin runtime's object-parameter API while routing the actual
// mutation through the storage-neutral session accessor seam.
return await updateSessionEntry(
{
sessionKey: params.sessionKey,
storePath: params.storePath,
},
params.update,
{
skipMaintenance: params.skipMaintenance,
takeCacheOwnership: params.takeCacheOwnership,
},
);
}
async function upsertSessionEntry(params: RuntimeUpsertSessionEntryParams): Promise<void> {
// The public runtime helper historically replaced the full entry. Use the
// replace seam so removed fields do not survive as merge leftovers.
await replaceSessionEntry(
{
agentId: params.agentId,
env: params.env,
hydrateSkillPromptRefs: params.hydrateSkillPromptRefs,
sessionKey: params.sessionKey,
storePath: params.storePath,
},
params.entry,
);
}
/** Creates the plugin runtime agent facade with lazy embedded-agent/session helpers. */
export function createRuntimeAgent(): PluginRuntime["agent"] {
const agentRuntime = {

View File

@@ -204,10 +204,45 @@ export type PluginRuntimeCore = {
ensureAgentWorkspace: typeof import("../../agents/workspace.js").ensureAgentWorkspace;
session: {
resolveStorePath: typeof import("../../config/sessions/paths.js").resolveStorePath;
getSessionEntry: typeof import("../../config/sessions/store.js").getSessionEntry;
listSessionEntries: typeof import("../../config/sessions/store.js").listSessionEntries;
getSessionEntry: (params: {
agentId?: string;
env?: NodeJS.ProcessEnv;
hydrateSkillPromptRefs?: boolean;
sessionKey: string;
storePath?: string;
}) => import("../../config/sessions/types.js").SessionEntry | undefined;
listSessionEntries: (
params?: Partial<{
agentId: string;
env: NodeJS.ProcessEnv;
hydrateSkillPromptRefs: boolean;
storePath: string;
}>,
) => Array<{
sessionKey: string;
entry: import("../../config/sessions/types.js").SessionEntry;
}>;
patchSessionEntry: typeof import("../../config/sessions/store.js").patchSessionEntry;
upsertSessionEntry: typeof import("../../config/sessions/store.js").upsertSessionEntry;
upsertSessionEntry: (params: {
agentId?: string;
env?: NodeJS.ProcessEnv;
hydrateSkillPromptRefs?: boolean;
sessionKey: string;
storePath?: string;
entry: import("../../config/sessions/types.js").SessionEntry;
}) => Promise<void>;
updateSessionStoreEntry: (params: {
storePath: string;
sessionKey: string;
update: (
entry: import("../../config/sessions/types.js").SessionEntry,
) =>
| Promise<Partial<import("../../config/sessions/types.js").SessionEntry> | null>
| Partial<import("../../config/sessions/types.js").SessionEntry>
| null;
skipMaintenance?: boolean;
takeCacheOwnership?: boolean;
}) => Promise<import("../../config/sessions/types.js").SessionEntry | null>;
/**
* @deprecated Use getSessionEntry/listSessionEntries for reads and
* patchSessionEntry/upsertSessionEntry for writes. This keeps the legacy
@@ -216,7 +251,6 @@ export type PluginRuntimeCore = {
loadSessionStore: typeof import("../../config/sessions/store-load.js").loadSessionStore;
saveSessionStore: import("../../config/sessions/runtime-types.js").SaveSessionStore;
updateSessionStore: typeof import("../../config/sessions/store.js").updateSessionStore;
updateSessionStoreEntry: typeof import("../../config/sessions/store.js").updateSessionStoreEntry;
resolveSessionFilePath: typeof import("../../config/sessions/paths.js").resolveSessionFilePath;
};
};