perf(memory): defer session backfill runtime (#119372)

Punchcard-Session: coral-workshop-workshop-3f
This commit is contained in:
Vincent Koc
2026-08-05 08:16:20 +08:00
committed by GitHub
parent 9ff9aac71a
commit cbde3c1945
8 changed files with 149 additions and 60 deletions
+1 -1
View File
@@ -1,8 +1,8 @@
import { listAgentIds } from "openclaw/plugin-sdk/agent-runtime";
import { createLazyRuntimeModule } from "openclaw/plugin-sdk/lazy-runtime";
// Memory Core plugin entrypoint registers its OpenClaw integration.
import {
jsonResult,
listAgentIds,
resolveMemorySearchConfig,
resolveSessionAgentIds,
type MemoryPluginRuntime,
@@ -0,0 +1,81 @@
import type { GatewayRequestHandlerOptions } from "openclaw/plugin-sdk/gateway-runtime";
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-entry";
import { expect, it, vi } from "vitest";
import { registerSessionBackfillGatewayMethods } from "./session-backfill-gateway.js";
const backfillModule = vi.hoisted(() => ({
loadCount: 0,
executeSessionBackfill: vi.fn(),
executeSessionBackfillBatch: vi.fn(),
}));
vi.mock("./session-backfill.js", () => {
backfillModule.loadCount += 1;
return {
executeSessionBackfill: backfillModule.executeSessionBackfill,
executeSessionBackfillBatch: backfillModule.executeSessionBackfillBatch,
};
});
it("loads session backfill execution only for the first valid request", async () => {
const methods = new Map<string, (options: GatewayRequestHandlerOptions) => Promise<void>>();
const api = {
runtime: {
config: {
current: () => ({
agents: {
entries: { main: { default: true, workspace: "/tmp/main-workspace" } },
},
}),
},
agent: {
resolveAgentWorkspaceDir: () => "/tmp/main-workspace",
},
},
registerGatewayMethod(
method: string,
handler: (options: GatewayRequestHandlerOptions) => Promise<void>,
) {
methods.set(method, handler);
},
} as unknown as OpenClawPluginApi;
registerSessionBackfillGatewayMethods(api);
expect(backfillModule.loadCount).toBe(0);
const preview = methods.get("memory.sessionBackfill.preview");
expect(preview).toBeDefined();
const invalidRespond = vi.fn();
await preview!({
params: { agentId: "main", from: "invalid" },
respond: invalidRespond,
} as unknown as GatewayRequestHandlerOptions);
expect(backfillModule.loadCount).toBe(0);
expect(invalidRespond.mock.calls[0]?.[2]).toMatchObject({ code: "INVALID_REQUEST" });
backfillModule.executeSessionBackfillBatch.mockResolvedValue({
result: {
agentId: "main",
workspaceDir: "/tmp/main-workspace",
applied: false,
rem: false,
days: [],
candidateCount: 0,
stagedEntries: 0,
writtenDiaryEntries: 0,
replacedDiaryEntries: 0,
},
continuation: { advanced: false, hasMore: false },
});
await preview!({
params: { agentId: "main" },
respond: vi.fn(),
} as unknown as GatewayRequestHandlerOptions);
await preview!({
params: { agentId: "main" },
respond: vi.fn(),
} as unknown as GatewayRequestHandlerOptions);
expect(backfillModule.loadCount).toBe(1);
expect(backfillModule.executeSessionBackfillBatch).toHaveBeenCalledTimes(2);
});
@@ -0,0 +1,3 @@
// Keep the gateway's dynamic boundary separate from the CLI runtime, which
// also owns a static session-backfill import for command execution.
export { executeSessionBackfill, executeSessionBackfillBatch } from "./session-backfill.js";
@@ -1,4 +1,3 @@
import { listAgentIds } from "openclaw/plugin-sdk/agent-runtime";
import { readPositiveIntegerParam, readStringParam } from "openclaw/plugin-sdk/channel-actions";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
import {
@@ -6,17 +5,14 @@ import {
errorShape,
type GatewayRequestHandlerOptions,
} from "openclaw/plugin-sdk/gateway-runtime";
import { createLazyRuntimeModule } from "openclaw/plugin-sdk/lazy-runtime";
import { listAgentIds } from "openclaw/plugin-sdk/memory-core-host-runtime-core";
import { resolveMemoryRemDreamingConfig } from "openclaw/plugin-sdk/memory-core-host-status";
import { resolvePluginConfigObject } from "openclaw/plugin-sdk/plugin-config-runtime";
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-entry";
import { normalizeAgentId } from "openclaw/plugin-sdk/routing";
import {
executeSessionBackfill,
executeSessionBackfillBatch,
normalizeSessionBackfillSelection,
type RunSessionBackfillParams,
type SessionBackfillResult,
} from "./session-backfill.js";
import type { SessionBackfillResult } from "./session-backfill-contract.js";
import { normalizeSessionBackfillSelection } from "./session-backfill-selection.js";
const SESSION_BACKFILL_GATEWAY_METHODS = {
preview: "memory.sessionBackfill.preview",
@@ -24,10 +20,12 @@ const SESSION_BACKFILL_GATEWAY_METHODS = {
rollback: "memory.sessionBackfill.rollback",
} as const;
type SessionBackfillGatewayParams = Pick<
RunSessionBackfillParams,
"agentId" | "from" | "to" | "limitDays"
>;
type SessionBackfillGatewayParams = {
agentId: string;
from?: string;
to?: string;
limitDays: number;
};
type SessionBackfillGatewayResult = {
days: number;
@@ -44,6 +42,10 @@ type SessionBackfillGatewayResult = {
class InvalidSessionBackfillRequestError extends Error {}
const loadSessionBackfillGatewayRuntime = createLazyRuntimeModule(
() => import("./session-backfill-gateway.runtime.js"),
);
function paramsRecord(value: unknown): Record<string, unknown> {
if (!value || typeof value !== "object" || Array.isArray(value)) {
throw new Error("params must be an object.");
@@ -165,6 +167,7 @@ export function registerSessionBackfillGatewayMethods(api: OpenClawPluginApi): v
}
try {
const context = resolveExecutionContext(api, request.agentId);
const { executeSessionBackfillBatch } = await loadSessionBackfillGatewayRuntime();
const execution = await executeSessionBackfillBatch({
...request,
...context,
@@ -203,6 +206,7 @@ export function registerSessionBackfillGatewayMethods(api: OpenClawPluginApi): v
}
try {
const context = resolveExecutionContext(api, request.agentId);
const { executeSessionBackfill } = await loadSessionBackfillGatewayRuntime();
const result = await executeSessionBackfill({ ...request, ...context, rollback: true });
respond(true, {
removedDiaryEntries: result.rollback?.removedDiaryEntries ?? 0,
@@ -11,8 +11,6 @@ import type {
} from "./session-backfill-contract.js";
import { readSessionIngestionState, writeSessionIngestionState } from "./session-ingestion.js";
const DEFAULT_SESSION_BACKFILL_LIMIT_DAYS = 92;
const MEMORY_DAY_RE = /^\d{4}-\d{2}-\d{2}$/;
// Batch keys are SHA-256 hex digests, so this colon-delimited marker cannot collide.
const SESSION_BACKFILL_BASELINE_KEY_PREFIX = "complete-baseline:";
@@ -34,45 +32,6 @@ type SessionBackfillBaseline = {
agentId: string;
};
function normalizeMemoryDay(value: string | undefined, flag: string): string | undefined {
if (value === undefined) {
return undefined;
}
const day = value.trim();
if (!MEMORY_DAY_RE.test(day)) {
throw new Error(`${flag} must use YYYY-MM-DD.`);
}
const parsed = new Date(`${day}T00:00:00.000Z`);
if (!Number.isFinite(parsed.getTime()) || parsed.toISOString().slice(0, 10) !== day) {
throw new Error(`${flag} must be a valid calendar day.`);
}
return day;
}
export function normalizeSessionBackfillSelection(
params: { from?: string; to?: string; limitDays?: number },
labels: { from: string; to: string; limitDays: string } = {
from: "--from",
to: "--to",
limitDays: "--limit-days",
},
): { from?: string; to?: string; limitDays: number } {
const from = normalizeMemoryDay(params.from, labels.from);
const to = normalizeMemoryDay(params.to, labels.to);
if (from !== undefined && to !== undefined && from > to) {
throw new Error(`${labels.from} must not be after ${labels.to}.`);
}
const limitDays = params.limitDays ?? DEFAULT_SESSION_BACKFILL_LIMIT_DAYS;
if (!Number.isInteger(limitDays) || limitDays <= 0) {
throw new Error(`${labels.limitDays} must be a positive integer.`);
}
return {
...(from !== undefined ? { from } : {}),
...(to !== undefined ? { to } : {}),
limitDays,
};
}
export async function recordSessionBackfillRewindBatch(params: {
workspaceDir: string;
candidates: SessionBackfillRewindCandidate[];
@@ -0,0 +1,41 @@
const DEFAULT_SESSION_BACKFILL_LIMIT_DAYS = 92;
const MEMORY_DAY_RE = /^\d{4}-\d{2}-\d{2}$/;
function normalizeMemoryDay(value: string | undefined, flag: string): string | undefined {
if (value === undefined) {
return undefined;
}
const day = value.trim();
if (!MEMORY_DAY_RE.test(day)) {
throw new Error(`${flag} must use YYYY-MM-DD.`);
}
const parsed = new Date(`${day}T00:00:00.000Z`);
if (!Number.isFinite(parsed.getTime()) || parsed.toISOString().slice(0, 10) !== day) {
throw new Error(`${flag} must be a valid calendar day.`);
}
return day;
}
export function normalizeSessionBackfillSelection(
params: { from?: string; to?: string; limitDays?: number },
labels: { from: string; to: string; limitDays: string } = {
from: "--from",
to: "--to",
limitDays: "--limit-days",
},
): { from?: string; to?: string; limitDays: number } {
const from = normalizeMemoryDay(params.from, labels.from);
const to = normalizeMemoryDay(params.to, labels.to);
if (from !== undefined && to !== undefined && from > to) {
throw new Error(`${labels.from} must not be after ${labels.to}.`);
}
const limitDays = params.limitDays ?? DEFAULT_SESSION_BACKFILL_LIMIT_DAYS;
if (!Number.isInteger(limitDays) || limitDays <= 0) {
throw new Error(`${labels.limitDays} must be a positive integer.`);
}
return {
...(from !== undefined ? { from } : {}),
...(to !== undefined ? { to } : {}),
limitDays,
};
}
@@ -14,11 +14,11 @@ import type {
import {
drainSessionBackfill,
markSessionBackfillRewindBaseline,
normalizeSessionBackfillSelection,
recordSessionBackfillRewindBatch,
resetSessionBackfillIngestionState,
rewindSessionBackfillIngestionState,
} from "./session-backfill-lifecycle.js";
import { normalizeSessionBackfillSelection } from "./session-backfill-selection.js";
import {
SESSION_INGESTION_MAX_MESSAGES_PER_FILE,
SESSION_INGESTION_MAX_MESSAGES_PER_SWEEP,
@@ -46,9 +46,6 @@ const SESSION_BACKFILL_QUERY_PREFIX = "__dreaming_session_backfill__";
const TOP_CANDIDATE_LIMIT = 5;
const MAX_SESSION_BACKFILL_APPLY_BATCHES = 10_000;
export { normalizeSessionBackfillSelection } from "./session-backfill-lifecycle.js";
export type { SessionBackfillResult } from "./session-backfill-contract.js";
export type MemorySessionBackfillOptions = {
agent?: string;
from?: string;
@@ -74,7 +71,7 @@ type SessionBackfillScan = {
stateKey: string;
};
export type RunSessionBackfillParams = {
type RunSessionBackfillParams = {
agentId: string;
workspaceDir: string;
from?: string;
@@ -11,7 +11,11 @@ export {
} from "../agents/tools/common.js";
export type { AnyAgentTool } from "../agents/tools/common.js";
export { resolveCronStyleNow } from "../agents/current-time.js";
export { resolveDefaultAgentId, resolveSessionAgentIds } from "../agents/agent-scope.js";
export {
listAgentIds,
resolveDefaultAgentId,
resolveSessionAgentIds,
} from "../agents/agent-scope.js";
export { resolveMemorySearchConfig } from "../agents/memory-search.js";
export { resolveMemoryDreamingPluginConfig } from "../memory-host-sdk/dreaming.js";
export { parseNonNegativeByteSize } from "../config/byte-size.js";