diff --git a/src/plugin-sdk/config-runtime.ts b/src/plugin-sdk/config-runtime.ts index 08d4d5f54011..abc13e0da0fb 100644 --- a/src/plugin-sdk/config-runtime.ts +++ b/src/plugin-sdk/config-runtime.ts @@ -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"; diff --git a/src/plugin-sdk/session-store-runtime.test.ts b/src/plugin-sdk/session-store-runtime.test.ts new file mode 100644 index 000000000000..fc3b670bf5af --- /dev/null +++ b/src/plugin-sdk/session-store-runtime.test.ts @@ -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", + }); + }); +}); diff --git a/src/plugin-sdk/session-store-runtime.ts b/src/plugin-sdk/session-store-runtime.ts index c2761409401c..f225557a4da9 100644 --- a/src/plugin-sdk/session-store-runtime.ts +++ b/src/plugin-sdk/session-store-runtime.ts @@ -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>; + +type SessionStoreEntrySummary = { + sessionKey: string; + entry: SessionEntry; +}; + +type SessionStoreEntryUpdate = ( + entry: SessionEntry, +) => Promise | null> | Partial | 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 { + 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 { + 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, diff --git a/src/plugins/runtime/runtime-agent.ts b/src/plugins/runtime/runtime-agent.ts index 70576a9325bf..8777eba0bc6b 100644 --- a/src/plugins/runtime/runtime-agent.ts +++ b/src/plugins/runtime/runtime-agent.ts @@ -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>; + +type RuntimeSessionStoreEntrySummary = { + sessionKey: string; + entry: SessionEntry; +}; + +type RuntimeSessionStoreEntryUpdateParams = { + storePath: string; + sessionKey: string; + update: ( + entry: SessionEntry, + ) => Promise | null> | Partial | 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 { + // 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 { + // 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 = { diff --git a/src/plugins/runtime/types-core.ts b/src/plugins/runtime/types-core.ts index 8b1a0a0cd3d9..cfa4cd152d3f 100644 --- a/src/plugins/runtime/types-core.ts +++ b/src/plugins/runtime/types-core.ts @@ -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; + updateSessionStoreEntry: (params: { + storePath: string; + sessionKey: string; + update: ( + entry: import("../../config/sessions/types.js").SessionEntry, + ) => + | Promise | null> + | Partial + | null; + skipMaintenance?: boolean; + takeCacheOwnership?: boolean; + }) => Promise; /** * @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; }; };