mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
perf(memory): defer session backfill runtime (#119372)
Punchcard-Session: coral-workshop-workshop-3f
This commit is contained in:
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user