refactor: route SDK session compatibility through seam (#89203)

This commit is contained in:
Josh Lehman 2026-06-18 20:52:46 -07:00 committed by GitHub
parent 87854e841e
commit f1cab04966
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 655 additions and 66 deletions

View file

@ -1,2 +1,2 @@
f24065e760a9fafbd2a50962beba4d752b2d6166043170d37cdd6137640e7eef plugin-sdk-api-baseline.json
89a332c206f639d5faef730bac2d23f75751b306419e5dfeae1b731166bbc41c plugin-sdk-api-baseline.jsonl
d991fae868626a78a65ea66389f3d62a48479fe6af3bb2db71a22802aa41ceea plugin-sdk-api-baseline.json
4aa4f623cfc221bca16c00995cd278b15d1706aac074d8016d14e97e1d9423f7 plugin-sdk-api-baseline.jsonl

View file

@ -164,7 +164,9 @@ two-party event loops that do not go through the shared inbound reply runner.
});
```
Prefer `getSessionEntry(...)`, `listSessionEntries(...)`, `patchSessionEntry(...)`, or `upsertSessionEntry(...)` for session workflows. These helpers address sessions by agent/session identity so plugins do not depend on the legacy `sessions.json` storage shape. Use `preserveActivity: true` for metadata-only patches that should not refresh session activity, and `replaceEntry: true` only when the callback returns a complete entry and deleted fields must stay deleted. `loadSessionStore(...)` remains as a deprecated compatibility escape hatch for callers that intentionally need a mutable whole-store clone.
Prefer `getSessionEntry(...)`, `listSessionEntries(...)`, `patchSessionEntry(...)`, or `upsertSessionEntry(...)` for session workflows. These helpers address sessions by agent/session identity so plugins do not depend on the legacy `sessions.json` storage shape. Use `preserveActivity: true` for metadata-only patches that should not refresh session activity, and `replaceEntry: true` only when the callback returns a complete entry and deleted fields must stay deleted.
`loadSessionStore(...)`, `saveSessionStore(...)`, `updateSessionStore(...)`, and `resolveSessionFilePath(...)` are kept only during the transition before SQLite migration for plugins that still intentionally depend on the legacy whole-store or transcript-file shape. New plugin code must not use those helpers, and existing callers must migrate to entry helpers before the SQLite storage flip.
</Accordion>
<Accordion title="api.runtime.agent.defaults">

View file

@ -247,7 +247,7 @@ usage endpoint failed or returned no usable usage data.
| `plugin-sdk/reply-history` | Shared short-window reply-history helpers. New message-turn code should use `createChannelHistoryWindow`; lower-level map helpers remain deprecated compatibility exports only |
| `plugin-sdk/reply-reference` | `createReplyReferencePlanner` |
| `plugin-sdk/reply-chunking` | Narrow text/markdown chunking helpers |
| `plugin-sdk/session-store-runtime` | Session workflow helpers (`getSessionEntry`, `listSessionEntries`, `patchSessionEntry`, `upsertSessionEntry`), legacy session store path/session-key helpers, updated-at reads, and deprecated whole-store mutation helpers |
| `plugin-sdk/session-store-runtime` | Session workflow helpers (`getSessionEntry`, `listSessionEntries`, `patchSessionEntry`, `upsertSessionEntry`), legacy session store path/session-key helpers, updated-at reads, and transition-only whole-store/file-path compatibility helpers |
| `plugin-sdk/sqlite-runtime` | Focused SQLite agent-schema, path, and transaction helpers for first-party runtime |
| `plugin-sdk/cron-store-runtime` | Cron store path/load/save helpers |
| `plugin-sdk/state-paths` | State/OAuth dir path helpers |

View file

@ -278,6 +278,8 @@ export type SessionEntryUpdateOptions = {
skipMaintenance?: boolean;
/** Let the writer cache retain the updated object without cloning. */
takeCacheOwnership?: boolean;
/** Throw when best-effort store recovery cannot confirm the requested write. */
requireWriteSuccess?: boolean;
};
export type SessionLifecycleTranscriptInfo = {
@ -625,6 +627,7 @@ export async function updateSessionEntry(
sessionKey: scope.sessionKey,
skipMaintenance: options.skipMaintenance,
takeCacheOwnership: options.takeCacheOwnership,
requireWriteSuccess: options.requireWriteSuccess,
update,
});
}

View file

@ -2,11 +2,6 @@
* Tests config runtime exports and snapshot/cache behavior exposed through the SDK.
*/
import { describe, expect, it } from "vitest";
import {
listSessionEntries as listAccessorSessionEntries,
loadSessionEntry,
readSessionUpdatedAt as readAccessorSessionUpdatedAt,
} from "../config/sessions/session-accessor.js";
import {
getSessionEntry,
listSessionEntries,
@ -15,12 +10,17 @@ import {
resolvePluginConfigObject,
type OpenClawConfig,
} from "./config-runtime.js";
import {
getSessionEntry as getSessionStoreEntry,
listSessionEntries as listSessionStoreEntries,
readSessionUpdatedAt as readSessionStoreUpdatedAt,
} from "./session-store-runtime.js";
describe("config-runtime session read exports", () => {
it("routes read helpers through the session accessor seam", () => {
expect(getSessionEntry).toBe(loadSessionEntry);
expect(listSessionEntries).toBe(listAccessorSessionEntries);
expect(readSessionUpdatedAt).toBe(readAccessorSessionUpdatedAt);
it("re-exports the session-store runtime seam wrappers", () => {
expect(getSessionEntry).toBe(getSessionStoreEntry);
expect(listSessionEntries).toBe(listSessionStoreEntries);
expect(readSessionUpdatedAt).toBe(readSessionStoreUpdatedAt);
});
});

View file

@ -4,20 +4,23 @@
* config-mutation, and runtime-config-snapshot.
*/
import {
listSessionEntries,
loadSessionEntry as getSessionEntry,
readSessionUpdatedAt,
} from "../config/sessions/session-accessor.js";
import { loadSessionStore as loadSessionStoreImpl } from "../config/sessions/store-load.js";
export {
getSessionEntry,
listSessionEntries,
patchSessionEntry,
readSessionUpdatedAt,
updateSessionStoreEntry,
upsertSessionEntry,
} from "./session-store-runtime.js";
/**
* @deprecated Use getSessionEntry/listSessionEntries for reads and
* patchSessionEntry/upsertSessionEntry for writes. loadSessionStore keeps the
* legacy mutable whole-store shape and will remain a compatibility escape hatch.
* patchSessionEntry/upsertSessionEntry for writes. This whole-store helper is
* kept only during the transition before SQLite migration. Callers must
* migrate away from reading sessions.json directly.
*/
export const loadSessionStore = loadSessionStoreImpl;
export { getSessionEntry, listSessionEntries, readSessionUpdatedAt };
export { resolveDefaultAgentId } from "../agents/agent-scope.js";
export {
@ -147,13 +150,10 @@ export type {
} from "../config/types.js";
export {
clearSessionStoreCacheForTest,
patchSessionEntry,
recordSessionMetaFromInbound,
saveSessionStore,
updateLastRoute,
updateSessionStore,
updateSessionStoreEntry,
upsertSessionEntry,
resolveSessionStoreEntry,
} from "../config/sessions/store.js";
export { resolveSessionKey } from "../config/sessions/session-key.js";

View file

@ -1,19 +1,254 @@
import { describe, expect, it } from "vitest";
import {
listSessionEntries as listAccessorSessionEntries,
loadSessionEntry,
readSessionUpdatedAt as readAccessorSessionUpdatedAt,
} from "../config/sessions/session-accessor.js";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import * as jsonFiles from "../infra/json-files.js";
import {
getSessionEntry,
listSessionEntries,
patchSessionEntry,
readSessionUpdatedAt,
saveSessionStore,
updateSessionStore,
updateSessionStoreEntry,
upsertSessionEntry,
} from "./session-store-runtime.js";
describe("session-store-runtime", () => {
it("routes read helpers through the session accessor seam", () => {
expect(getSessionEntry).toBe(loadSessionEntry);
expect(listSessionEntries).toBe(listAccessorSessionEntries);
expect(readSessionUpdatedAt).toBe(readAccessorSessionUpdatedAt);
const DAY_MS = 24 * 60 * 60 * 1000;
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 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(readSessionUpdatedAt({ sessionKey, storePath })).toEqual(expect.any(Number));
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 mutation 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,
},
});
const beforePatch = getSessionEntry({ sessionKey, storePath });
await expect(
patchSessionEntry({
sessionKey,
storePath,
preserveActivity: true,
update: (_entry, context) => ({
providerOverride: context.existingEntry ? "openai" : "missing",
updatedAt: 20,
}),
}),
).resolves.toMatchObject({
providerOverride: "openai",
sessionId: "session-1",
updatedAt: beforePatch?.updatedAt,
});
await expect(
updateSessionStoreEntry({
sessionKey,
storePath,
update: () => ({ model: "gpt-5.5" }),
}),
).resolves.toMatchObject({
model: "gpt-5.5",
providerOverride: "openai",
sessionId: "session-1",
});
});
it("preserves resolved maintenance settings through entry patches", async () => {
const staleSessionKey = "agent:main:stale";
const activeSessionKey = "agent:main:active";
await saveSessionStore(
storePath,
{
[staleSessionKey]: {
sessionId: "session-stale",
updatedAt: 10,
},
[activeSessionKey]: {
sessionId: "session-active",
updatedAt: 20,
},
},
{ skipMaintenance: true },
);
await expect(
patchSessionEntry({
sessionKey: activeSessionKey,
storePath,
maintenanceConfig: {
mode: "enforce",
pruneAfterMs: 7 * DAY_MS,
maxEntries: 1,
resetArchiveRetentionMs: 7 * DAY_MS,
maxDiskBytes: null,
highWaterBytes: null,
},
update: () => ({ model: "gpt-5.5" }),
}),
).resolves.toMatchObject({
model: "gpt-5.5",
sessionId: "session-active",
});
expect(getSessionEntry({ sessionKey: activeSessionKey, storePath })).toMatchObject({
model: "gpt-5.5",
sessionId: "session-active",
});
expect(getSessionEntry({ sessionKey: staleSessionKey, storePath })).toBeUndefined();
});
it("keeps deprecated whole-store mutations grouped as one compatibility operation", async () => {
const firstSessionKey = "agent:main:first";
const secondSessionKey = "agent:main:second";
const deletedSessionKey = "agent:main:deleted";
await saveSessionStore(
storePath,
{
[firstSessionKey]: {
sessionId: "session-1",
updatedAt: 10,
},
[secondSessionKey]: {
sessionId: "session-2",
updatedAt: 10,
},
[deletedSessionKey]: {
sessionId: "session-3",
updatedAt: 10,
},
},
{ skipMaintenance: true },
);
await expect(
updateSessionStore(
storePath,
(store) => {
const first = store[firstSessionKey];
const second = store[secondSessionKey];
if (!first || !second) {
throw new Error("seed session entries missing");
}
store[firstSessionKey] = {
...first,
model: "gpt-5.5",
updatedAt: 20,
};
store[secondSessionKey] = {
...second,
providerOverride: "openai",
updatedAt: 30,
};
delete store[deletedSessionKey];
return "whole-store-updated";
},
{ skipMaintenance: true },
),
).resolves.toBe("whole-store-updated");
expect(getSessionEntry({ sessionKey: firstSessionKey, storePath })).toMatchObject({
model: "gpt-5.5",
sessionId: "session-1",
updatedAt: 20,
});
expect(getSessionEntry({ sessionKey: secondSessionKey, storePath })).toMatchObject({
providerOverride: "openai",
sessionId: "session-2",
updatedAt: 30,
});
expect(getSessionEntry({ sessionKey: deletedSessionKey, storePath })).toBeUndefined();
});
it("preserves requireWriteSuccess for critical session entry updates", async () => {
const sessionKey = "agent:main:main";
await upsertSessionEntry({
sessionKey,
storePath,
entry: {
sessionId: "session-1",
updatedAt: 10,
},
});
const writeError = Object.assign(new Error("write failed"), { code: "ENOENT" });
const writeSpy = vi.spyOn(jsonFiles, "writeTextAtomic").mockRejectedValue(writeError);
try {
await expect(
updateSessionStoreEntry({
sessionKey,
storePath,
requireWriteSuccess: true,
update: () => ({ model: "gpt-5.5" }),
}),
).rejects.toBe(writeError);
} finally {
writeSpy.mockRestore();
}
});
});

View file

@ -1,26 +1,161 @@
// Narrow session-store helpers for channel hot paths.
import {
listSessionEntries,
loadSessionEntry as getSessionEntry,
readSessionUpdatedAt,
listSessionEntries as listAccessorSessionEntries,
loadSessionEntry,
patchSessionEntry as patchAccessorSessionEntry,
readSessionUpdatedAt as readAccessorSessionUpdatedAt,
replaceSessionEntry,
type SessionAccessScope,
updateSessionEntry,
} from "../config/sessions/session-accessor.js";
import { loadSessionStore as loadSessionStoreImpl } from "../config/sessions/store-load.js";
import type { ResolvedSessionMaintenanceConfig } from "../config/sessions/store.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 SessionStoreEntryPatch = (
entry: SessionEntry,
context: { existingEntry?: SessionEntry },
) => Promise<Partial<SessionEntry> | null> | Partial<SessionEntry> | null;
type PatchSessionEntryParams = SessionStoreReadParams & {
fallbackEntry?: SessionEntry;
maintenanceConfig?: ResolvedSessionMaintenanceConfig;
preserveActivity?: boolean;
replaceEntry?: boolean;
update: SessionStoreEntryPatch;
};
type ReadSessionUpdatedAtParams = SessionStoreReadParams;
type UpdateSessionStoreEntryParams = {
storePath: string;
sessionKey: string;
update: SessionStoreEntryUpdate;
skipMaintenance?: boolean;
takeCacheOwnership?: boolean;
requireWriteSuccess?: boolean;
};
type UpsertSessionEntryParams = SessionStoreReadParams & {
entry: SessionEntry;
};
function toSessionAccessScope(params: SessionStoreReadParams): SessionAccessScope {
// Maintainer note: keep this adapter narrow so plugin callers retain the
// object-parameter API while internal accessor-only options stay private.
return {
sessionKey: params.sessionKey,
...(params.agentId !== undefined ? { agentId: params.agentId } : {}),
...(params.env !== undefined ? { env: params.env } : {}),
...(params.hydrateSkillPromptRefs !== undefined
? { hydrateSkillPromptRefs: params.hydrateSkillPromptRefs }
: {}),
...(params.storePath !== undefined ? { storePath: params.storePath } : {}),
};
}
/**
* @deprecated Use getSessionEntry/listSessionEntries for reads and
* patchSessionEntry/upsertSessionEntry for writes. loadSessionStore keeps the
* legacy mutable whole-store shape and will remain a compatibility escape hatch.
* patchSessionEntry/upsertSessionEntry for writes. This whole-store helper is
* kept only during the transition before SQLite migration. Callers must
* migrate away from reading sessions.json directly.
*/
export const loadSessionStore = loadSessionStoreImpl;
export { getSessionEntry, listSessionEntries, readSessionUpdatedAt };
/** Loads one session entry by agent/session identity. */
export function getSessionEntry(params: SessionStoreReadParams): SessionEntry | undefined {
return loadSessionEntry(toSessionAccessScope(params));
}
/** Lists session entries for one agent. */
export function listSessionEntries(
params: SessionStoreListParams = {},
): SessionStoreEntrySummary[] {
return listAccessorSessionEntries({
...(params.agentId !== undefined ? { agentId: params.agentId } : {}),
...(params.env !== undefined ? { env: params.env } : {}),
...(params.hydrateSkillPromptRefs !== undefined
? { hydrateSkillPromptRefs: params.hydrateSkillPromptRefs }
: {}),
...(params.storePath !== undefined ? { storePath: params.storePath } : {}),
});
}
/** Patches one session entry by agent/session identity. */
export async function patchSessionEntry(
params: PatchSessionEntryParams,
): Promise<SessionEntry | null> {
return await patchAccessorSessionEntry(toSessionAccessScope(params), params.update, {
fallbackEntry: params.fallbackEntry,
maintenanceConfig: params.maintenanceConfig,
preserveActivity: params.preserveActivity,
replaceEntry: params.replaceEntry,
});
}
/** Reads the last activity timestamp for one session entry. */
export function readSessionUpdatedAt(params: ReadSessionUpdatedAtParams): number | undefined {
return readAccessorSessionUpdatedAt(toSessionAccessScope(params));
}
/** Updates an existing session entry by store path and session key. */
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,
requireWriteSuccess: params.requireWriteSuccess,
},
);
}
/** Replaces or creates one session entry by agent/session identity. */
export async function upsertSessionEntry(params: UpsertSessionEntryParams): Promise<void> {
await replaceSessionEntry(toSessionAccessScope(params), params.entry);
}
export { resolveSessionStoreEntry } from "../config/sessions/store-entry.js";
export {
resolveSessionFilePath,
resolveSessionTranscriptPathInDir,
resolveStorePath,
} from "../config/sessions/paths.js";
export { resolveSessionTranscriptPathInDir, resolveStorePath } from "../config/sessions/paths.js";
/**
* @deprecated Use getSessionEntry to read session metadata by agent/session
* identity instead of resolving transcript file paths. This file-path helper
* is kept only during the transition before SQLite migration. Callers must
* migrate away from resolving transcript file paths directly.
*/
export { resolveSessionFilePath } from "../config/sessions/paths.js";
/**
* @deprecated Use patchSessionEntry/upsertSessionEntry to persist session
* metadata by agent/session identity. This file-path helper is kept only during
* the transition before SQLite migration. Callers must migrate away from
* persisting transcript file paths directly.
*/
export { resolveAndPersistSessionFile } from "../config/sessions/session-file.js";
export { readLatestAssistantTextFromSessionTranscript } from "../config/sessions/transcript.js";
export { resolveSessionKey } from "../config/sessions/session-key.js";
@ -28,14 +163,18 @@ export { resolveGroupSessionKey } from "../config/sessions/group.js";
export { canonicalizeMainSessionAlias } from "../config/sessions/main-session.js";
export {
clearSessionStoreCacheForTest,
patchSessionEntry,
recordSessionMetaFromInbound,
saveSessionStore,
updateLastRoute,
updateSessionStore,
updateSessionStoreEntry,
upsertSessionEntry,
} from "../config/sessions/store.js";
/**
* @deprecated Use patchSessionEntry/upsertSessionEntry for writes. These
* whole-store helpers are kept only during the transition before SQLite
* migration. Callers must migrate away from reading or writing sessions.json.
*/
export { saveSessionStore, updateSessionStore } from "../config/sessions/store.js";
// Maintainer note: keep saveSessionStore/updateSessionStore grouped as one
// compatibility operation. A SQLite bridge must diff before/after store shapes,
// apply changed/deleted rows in one write transaction, and publish after commit.
export {
evaluateSessionFreshness,
resolveChannelResetConfig,

View file

@ -1,4 +1,7 @@
// Plugin runtime index tests cover runtime entrypoint exports and registry setup.
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "../../agents/defaults.js";
import {
@ -6,13 +9,13 @@ import {
setRuntimeConfigSnapshot,
type OpenClawConfig,
} from "../../config/config.js";
import { listSessionEntries, loadSessionEntry } from "../../config/sessions/session-accessor.js";
import { onAgentEvent } from "../../infra/agent-events.js";
import {
requestHeartbeat,
resetHeartbeatWakeStateForTests,
setHeartbeatWakeHandler,
} from "../../infra/heartbeat-wake.js";
import * as jsonFiles from "../../infra/json-files.js";
import * as execModule from "../../process/exec.js";
import { onSessionTranscriptUpdate } from "../../sessions/transcript-events.js";
import { VERSION } from "../../version.js";
@ -314,16 +317,16 @@ describe("plugin runtime command execution", () => {
]);
expect(runtime.agent.runEmbeddedPiAgent).toBe(runtime.agent.runEmbeddedAgent);
expectFunctionKeys(runtime.agent.session as Record<string, unknown>, [
"loadSessionStore",
"getSessionEntry",
"listSessionEntries",
"patchSessionEntry",
"upsertSessionEntry",
"saveSessionStore",
"updateSessionStore",
"updateSessionStoreEntry",
"resolveSessionFilePath",
]);
expect(runtime.agent.session.getSessionEntry).toBe(loadSessionEntry);
expect(runtime.agent.session.listSessionEntries).toBe(listSessionEntries);
},
},
{
@ -340,6 +343,41 @@ describe("plugin runtime command execution", () => {
expectRuntimeShape(assert);
});
it("preserves requireWriteSuccess through runtime session entry updates", async () => {
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-runtime-session-store-"));
const storePath = path.join(tempDir, "sessions.json");
const sessionKey = "agent:main:main";
const runtime = createPluginRuntime();
try {
await runtime.agent.session.upsertSessionEntry({
sessionKey,
storePath,
entry: {
sessionId: "session-1",
updatedAt: 10,
},
});
const writeError = Object.assign(new Error("write failed"), { code: "ENOENT" });
const writeSpy = vi.spyOn(jsonFiles, "writeTextAtomic").mockRejectedValue(writeError);
try {
await expect(
runtime.agent.session.updateSessionStoreEntry({
sessionKey,
storePath,
requireWriteSuccess: true,
update: () => ({ model: "gpt-5.5" }),
}),
).rejects.toBe(writeError);
} finally {
writeSpy.mockRestore();
}
} finally {
fs.rmSync(tempDir, { recursive: true, force: true });
}
});
it("modelAuth wrappers strip agentDir and store to prevent credential steering", async () => {
// The wrappers should not forward agentDir or store from plugin callers.
// We verify this by checking the wrapper functions exist and are not the

View file

@ -12,21 +12,65 @@ import { normalizeThinkLevel, resolveThinkingProfile } from "../../auto-reply/th
import { getRuntimeConfig } from "../../config/config.js";
import { resolveSessionFilePath, resolveStorePath } from "../../config/sessions/paths.js";
import {
listSessionEntries,
loadSessionEntry as getSessionEntry,
listSessionEntries as listAccessorSessionEntries,
loadSessionEntry,
patchSessionEntry as patchAccessorSessionEntry,
replaceSessionEntry,
type SessionAccessScope,
updateSessionEntry,
} from "../../config/sessions/session-accessor.js";
import {
loadSessionStore,
patchSessionEntry,
saveSessionStore,
updateSessionStore,
updateSessionStoreEntry,
upsertSessionEntry,
type ResolvedSessionMaintenanceConfig,
} 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;
requireWriteSuccess?: boolean;
};
type RuntimeSessionStoreEntryPatchParams = RuntimeSessionStoreReadParams & {
fallbackEntry?: SessionEntry;
maintenanceConfig?: ResolvedSessionMaintenanceConfig;
preserveActivity?: boolean;
replaceEntry?: boolean;
update: (
entry: SessionEntry,
context: { existingEntry?: SessionEntry },
) => Promise<Partial<SessionEntry> | null> | Partial<SessionEntry> | null;
};
type RuntimeUpsertSessionEntryParams = RuntimeSessionStoreReadParams & {
entry: SessionEntry;
};
const loadEmbeddedAgentRuntime = createLazyRuntimeModule(
() => import("./runtime-embedded-agent.runtime.js"),
);
@ -41,6 +85,73 @@ function resolveRuntimeThinkingCatalog(
return configuredCatalog.length > 0 ? configuredCatalog : undefined;
}
function toSessionAccessScope(params: RuntimeSessionStoreReadParams): SessionAccessScope {
// Keep plugin runtime parameters aligned with the public SDK wrapper while
// avoiding direct exposure of internal accessor-only options.
return {
sessionKey: params.sessionKey,
...(params.agentId !== undefined ? { agentId: params.agentId } : {}),
...(params.env !== undefined ? { env: params.env } : {}),
...(params.hydrateSkillPromptRefs !== undefined
? { hydrateSkillPromptRefs: params.hydrateSkillPromptRefs }
: {}),
...(params.storePath !== undefined ? { storePath: params.storePath } : {}),
};
}
function getSessionEntry(params: RuntimeSessionStoreReadParams): SessionEntry | undefined {
return loadSessionEntry(toSessionAccessScope(params));
}
function listSessionEntries(
params: RuntimeSessionStoreListParams = {},
): RuntimeSessionStoreEntrySummary[] {
return listAccessorSessionEntries({
...(params.agentId !== undefined ? { agentId: params.agentId } : {}),
...(params.env !== undefined ? { env: params.env } : {}),
...(params.hydrateSkillPromptRefs !== undefined
? { hydrateSkillPromptRefs: params.hydrateSkillPromptRefs }
: {}),
...(params.storePath !== undefined ? { storePath: params.storePath } : {}),
});
}
async function patchSessionEntry(
params: RuntimeSessionStoreEntryPatchParams,
): Promise<SessionEntry | null> {
return await patchAccessorSessionEntry(toSessionAccessScope(params), params.update, {
fallbackEntry: params.fallbackEntry,
maintenanceConfig: params.maintenanceConfig,
preserveActivity: params.preserveActivity,
replaceEntry: params.replaceEntry,
});
}
async function updateSessionStoreEntry(
params: RuntimeSessionStoreEntryUpdateParams,
): Promise<SessionEntry | null> {
// Maintainer note: keep the legacy object-parameter API here, but route
// mutations through the session accessor boundary.
return await updateSessionEntry(
{
sessionKey: params.sessionKey,
storePath: params.storePath,
},
params.update,
{
skipMaintenance: params.skipMaintenance,
takeCacheOwnership: params.takeCacheOwnership,
requireWriteSuccess: params.requireWriteSuccess,
},
);
}
async function upsertSessionEntry(params: RuntimeUpsertSessionEntryParams): Promise<void> {
// Maintainer note: this compatibility helper has full-entry replacement
// semantics, so removed fields must not survive as merge leftovers.
await replaceSessionEntry(toSessionAccessScope(params), params.entry);
}
/** Creates the plugin runtime agent facade with lazy embedded-agent/session helpers. */
export function createRuntimeAgent(): PluginRuntime["agent"] {
const agentRuntime = {

View file

@ -59,6 +59,42 @@ type RuntimeReplaceConfigFileParams = {
afterWrite: RuntimeConfigAfterWrite;
writeOptions?: RuntimeWriteConfigOptions;
};
type RuntimeSessionEntry = import("../../config/sessions/types.js").SessionEntry;
type RuntimeSessionStoreReadParams = {
agentId?: string;
env?: NodeJS.ProcessEnv;
hydrateSkillPromptRefs?: boolean;
sessionKey: string;
storePath?: string;
};
type RuntimeSessionStoreListParams = Partial<Omit<RuntimeSessionStoreReadParams, "sessionKey">>;
type RuntimeSessionStoreEntrySummary = {
sessionKey: string;
entry: RuntimeSessionEntry;
};
type RuntimeSessionStoreEntryPatchParams = RuntimeSessionStoreReadParams & {
fallbackEntry?: RuntimeSessionEntry;
maintenanceConfig?: import("../../config/sessions/store.js").ResolvedSessionMaintenanceConfig;
preserveActivity?: boolean;
replaceEntry?: boolean;
update: (
entry: RuntimeSessionEntry,
context: { existingEntry?: RuntimeSessionEntry },
) => Promise<Partial<RuntimeSessionEntry> | null> | Partial<RuntimeSessionEntry> | null;
};
type RuntimeUpsertSessionEntryParams = RuntimeSessionStoreReadParams & {
entry: RuntimeSessionEntry;
};
type RuntimeSessionStoreEntryUpdateParams = {
storePath: string;
sessionKey: string;
update: (
entry: RuntimeSessionEntry,
) => Promise<Partial<RuntimeSessionEntry> | null> | Partial<RuntimeSessionEntry> | null;
skipMaintenance?: boolean;
takeCacheOwnership?: boolean;
requireWriteSuccess?: boolean;
};
export type PluginRuntimeThinkingPolicyRequest = {
provider?: string | null;
model?: string | null;
@ -205,19 +241,44 @@ export type PluginRuntimeCore = {
ensureAgentWorkspace: typeof import("../../agents/workspace.js").ensureAgentWorkspace;
session: {
resolveStorePath: typeof import("../../config/sessions/paths.js").resolveStorePath;
getSessionEntry: typeof import("../../config/sessions/session-accessor.js").loadSessionEntry;
listSessionEntries: typeof import("../../config/sessions/session-accessor.js").listSessionEntries;
patchSessionEntry: typeof import("../../config/sessions/store.js").patchSessionEntry;
upsertSessionEntry: typeof import("../../config/sessions/store.js").upsertSessionEntry;
getSessionEntry: (params: RuntimeSessionStoreReadParams) => RuntimeSessionEntry | undefined;
listSessionEntries: (
params?: RuntimeSessionStoreListParams,
) => RuntimeSessionStoreEntrySummary[];
patchSessionEntry: (
params: RuntimeSessionStoreEntryPatchParams,
) => Promise<RuntimeSessionEntry | null>;
upsertSessionEntry: (params: RuntimeUpsertSessionEntryParams) => Promise<void>;
/**
* @deprecated Use getSessionEntry/listSessionEntries for reads and
* patchSessionEntry/upsertSessionEntry for writes. This keeps the legacy
* mutable whole-store compatibility shape.
* patchSessionEntry/upsertSessionEntry for writes. This whole-store
* helper is kept only during the transition before SQLite migration.
* Callers must migrate away from reading sessions.json directly.
*/
loadSessionStore: typeof import("../../config/sessions/store-load.js").loadSessionStore;
/**
* @deprecated Use patchSessionEntry/upsertSessionEntry for writes. This
* whole-store helper is kept only during the transition before SQLite
* migration. Callers must migrate away from writing sessions.json
* directly.
*/
saveSessionStore: import("../../config/sessions/runtime-types.js").SaveSessionStore;
/**
* @deprecated Use patchSessionEntry/upsertSessionEntry for writes. This
* whole-store helper is kept only during the transition before SQLite
* migration. Callers must migrate away from updating sessions.json
* directly.
*/
updateSessionStore: typeof import("../../config/sessions/store.js").updateSessionStore;
updateSessionStoreEntry: typeof import("../../config/sessions/store.js").updateSessionStoreEntry;
updateSessionStoreEntry: (
params: RuntimeSessionStoreEntryUpdateParams,
) => Promise<RuntimeSessionEntry | null>;
/**
* @deprecated Use getSessionEntry to read session metadata by
* agent/session identity. This file-path helper is kept only during the
* transition before SQLite migration. Callers must migrate away from
* resolving transcript file paths directly.
*/
resolveSessionFilePath: typeof import("../../config/sessions/paths.js").resolveSessionFilePath;
};
};