mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-03 07:32:43 +00:00
* docs(secrets): remove retired web credential paths * refactor(web): remove retired provider compatibility paths * refactor(providers): delete retired compatibility routes * refactor(secrets): remove retired credential aliases * refactor(plugin-sdk): delete retired compatibility surfaces * docs(plugin-sdk): remove retired migration guidance * chore(plugin-sdk): refresh rebased surface budgets * chore(plugin-sdk): refresh API removal baseline * refactor(compat): migrate retired internal callers * chore(plugin-sdk): refresh current-main baselines * test(config): migrate plugin-owned secret assertions * test(gateway): narrow plugin secret refs * fix(plugin-sdk): preserve private boundary type identity * chore(compat): remove stale sweep references * chore(lint): lower max-lines budget * refactor(secrets): remove unused web helper * build(plugin-sdk): drop removed compat entries * chore(plugin-sdk): refresh rebased API baseline * chore(plugin-sdk): use Linux API baseline hash * fix(plugin-sdk): preserve private bundled build entries * fix(plugin-sdk): package private runtime facades * fix(plugins): preserve external credential contracts
506 lines
16 KiB
TypeScript
506 lines
16 KiB
TypeScript
/** Registry state for plugin memory runtimes, prompt supplements, and flush planning. */
|
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
import type { MemoryCitationsMode } from "../config/types.memory.js";
|
|
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
|
import { createSubsystemLogger } from "../logging/subsystem.js";
|
|
import type { MemorySearchManager } from "../memory-host-sdk/host/types.js";
|
|
|
|
const log = createSubsystemLogger("plugins/memory-state");
|
|
|
|
export type MemoryPromptSectionParams = {
|
|
availableTools: Set<string>;
|
|
citationsMode?: MemoryCitationsMode;
|
|
agentId?: string;
|
|
agentSessionKey?: string;
|
|
sandboxed?: boolean;
|
|
};
|
|
|
|
export type MemoryPromptSectionBuilder = (params: MemoryPromptSectionParams) => string[];
|
|
|
|
/**
|
|
* Loads and renders prompt state before synchronous prompt assembly.
|
|
* Implementations may perform async state reads here, but must validate their
|
|
* owner instance before returning lines for the current run.
|
|
*/
|
|
type MemoryPromptSectionPreparer = (
|
|
params: MemoryPromptSectionParams,
|
|
) => Promise<readonly string[]>;
|
|
|
|
export type PreparedMemoryPromptSection = Readonly<{
|
|
context: Readonly<{
|
|
availableTools: readonly string[];
|
|
citationsMode?: MemoryCitationsMode;
|
|
agentId?: string;
|
|
agentSessionKey?: string;
|
|
sandboxed: boolean;
|
|
}>;
|
|
lines: readonly string[];
|
|
}>;
|
|
|
|
export type MemoryCorpusSearchResult = {
|
|
corpus: string;
|
|
path: string;
|
|
title?: string;
|
|
kind?: string;
|
|
score: number;
|
|
snippet: string;
|
|
id?: string;
|
|
startLine?: number;
|
|
endLine?: number;
|
|
citation?: string;
|
|
source?: string;
|
|
provenanceLabel?: string;
|
|
sourceType?: string;
|
|
sourcePath?: string;
|
|
updatedAt?: string;
|
|
};
|
|
|
|
type MemoryCorpusGetResult = {
|
|
corpus: string;
|
|
path: string;
|
|
title?: string;
|
|
kind?: string;
|
|
content: string;
|
|
fromLine: number;
|
|
lineCount: number;
|
|
id?: string;
|
|
provenanceLabel?: string;
|
|
sourceType?: string;
|
|
sourcePath?: string;
|
|
updatedAt?: string;
|
|
};
|
|
|
|
export type MemoryCorpusSupplement = {
|
|
search(params: {
|
|
query: string;
|
|
maxResults?: number;
|
|
agentId?: string;
|
|
agentSessionKey?: string;
|
|
sandboxed?: boolean;
|
|
}): Promise<MemoryCorpusSearchResult[]>;
|
|
get(params: {
|
|
lookup: string;
|
|
fromLine?: number;
|
|
lineCount?: number;
|
|
agentId?: string;
|
|
agentSessionKey?: string;
|
|
sandboxed?: boolean;
|
|
}): Promise<MemoryCorpusGetResult | null>;
|
|
};
|
|
|
|
type MemoryCorpusSupplementRegistration = {
|
|
pluginId: string;
|
|
supplement: MemoryCorpusSupplement;
|
|
};
|
|
|
|
type MemoryPromptSupplementRegistration = {
|
|
pluginId: string;
|
|
builder: MemoryPromptSectionBuilder;
|
|
};
|
|
|
|
type MemoryPromptPreparationRegistration = {
|
|
pluginId: string;
|
|
prepare: MemoryPromptSectionPreparer;
|
|
};
|
|
|
|
export type MemoryFlushPlan = {
|
|
softThresholdTokens: number;
|
|
forceFlushTranscriptBytes: number;
|
|
reserveTokensFloor: number;
|
|
model?: string;
|
|
prompt: string;
|
|
systemPrompt: string;
|
|
relativePath: string;
|
|
};
|
|
|
|
export type MemoryFlushPlanResolver = (params: {
|
|
cfg?: OpenClawConfig;
|
|
nowMs?: number;
|
|
}) => MemoryFlushPlan | null;
|
|
|
|
export type RegisteredMemorySearchManager = MemorySearchManager;
|
|
|
|
type MemoryRuntimeQmdConfig = {
|
|
command?: string;
|
|
};
|
|
|
|
type MemoryRuntimeBackendConfig =
|
|
| {
|
|
backend: "builtin";
|
|
}
|
|
| {
|
|
backend: "qmd";
|
|
qmd?: MemoryRuntimeQmdConfig;
|
|
};
|
|
|
|
export type MemoryPluginRuntime = {
|
|
getMemorySearchManager(params: {
|
|
cfg: OpenClawConfig;
|
|
agentId: string;
|
|
purpose?: "default" | "status" | "cli";
|
|
}): Promise<{
|
|
manager: RegisteredMemorySearchManager | null;
|
|
debug?: {
|
|
backend?: "builtin" | "qmd";
|
|
purpose?: "default" | "status" | "cli";
|
|
managerMs?: number;
|
|
managerCacheState?:
|
|
| "cached-full-hit"
|
|
| "cached-full-miss"
|
|
| "transient-cli"
|
|
| "transient-status"
|
|
| "pending-create-wait"
|
|
| "fallback-builtin"
|
|
| "recent-failure-cooldown";
|
|
qmdIdentityHash?: string;
|
|
failureCode?: "qmd-unavailable";
|
|
};
|
|
error?: string;
|
|
}>;
|
|
resolveMemoryBackendConfig(params: {
|
|
cfg: OpenClawConfig;
|
|
agentId: string;
|
|
}): MemoryRuntimeBackendConfig;
|
|
closeMemorySearchManager?(params: { cfg: OpenClawConfig; agentId: string }): Promise<void>;
|
|
closeAllMemorySearchManagers?(): Promise<void>;
|
|
};
|
|
|
|
type MemoryPluginPublicArtifactContentType = "markdown" | "json" | "text";
|
|
|
|
export type MemoryPluginPublicArtifact = {
|
|
kind: string;
|
|
workspaceDir: string;
|
|
relativePath: string;
|
|
absolutePath: string;
|
|
agentIds: string[];
|
|
contentType: MemoryPluginPublicArtifactContentType;
|
|
};
|
|
|
|
export type MemoryPluginPublicArtifactsProvider = {
|
|
listArtifacts(params: { cfg: OpenClawConfig }): Promise<MemoryPluginPublicArtifact[]>;
|
|
};
|
|
|
|
export type MemoryPluginCapability = {
|
|
promptBuilder?: MemoryPromptSectionBuilder;
|
|
flushPlanResolver?: MemoryFlushPlanResolver;
|
|
runtime?: MemoryPluginRuntime;
|
|
publicArtifacts?: MemoryPluginPublicArtifactsProvider;
|
|
};
|
|
|
|
type MemoryPluginCapabilityRegistration = {
|
|
pluginId: string;
|
|
capability: MemoryPluginCapability;
|
|
};
|
|
|
|
type MemoryPluginState = {
|
|
capability?: MemoryPluginCapabilityRegistration;
|
|
corpusSupplements: MemoryCorpusSupplementRegistration[];
|
|
promptPreparations: MemoryPromptPreparationRegistration[];
|
|
promptSupplements: MemoryPromptSupplementRegistration[];
|
|
};
|
|
|
|
const memoryPluginState: MemoryPluginState = {
|
|
corpusSupplements: [],
|
|
promptPreparations: [],
|
|
promptSupplements: [],
|
|
};
|
|
|
|
const preparedMemoryPromptSections = new WeakSet<PreparedMemoryPromptSection>();
|
|
const activePreparedMemoryPromptSection = new AsyncLocalStorage<PreparedMemoryPromptSection>();
|
|
|
|
export function registerMemoryCorpusSupplement(
|
|
pluginId: string,
|
|
supplement: MemoryCorpusSupplement,
|
|
): void {
|
|
const next = memoryPluginState.corpusSupplements.filter(
|
|
(registration) => registration.pluginId !== pluginId,
|
|
);
|
|
next.push({ pluginId, supplement });
|
|
memoryPluginState.corpusSupplements = next;
|
|
}
|
|
|
|
export function registerMemoryCapability(
|
|
pluginId: string,
|
|
capability: MemoryPluginCapability,
|
|
): void {
|
|
const existingCapability = memoryPluginState.capability?.capability;
|
|
// A selected memory plugin can add bridge artifacts while memory-core owns sidecar runtime hooks.
|
|
const shouldPreserveExisting =
|
|
existingCapability &&
|
|
Boolean(capability.publicArtifacts) &&
|
|
!capability.promptBuilder &&
|
|
!capability.flushPlanResolver &&
|
|
!capability.runtime;
|
|
memoryPluginState.capability = {
|
|
pluginId,
|
|
capability: {
|
|
...(shouldPreserveExisting ? existingCapability : {}),
|
|
...capability,
|
|
},
|
|
};
|
|
}
|
|
|
|
export function getMemoryCapabilityRegistration(): MemoryPluginCapabilityRegistration | undefined {
|
|
return memoryPluginState.capability
|
|
? {
|
|
pluginId: memoryPluginState.capability.pluginId,
|
|
capability: { ...memoryPluginState.capability.capability },
|
|
}
|
|
: undefined;
|
|
}
|
|
|
|
export function listMemoryCorpusSupplements(): MemoryCorpusSupplementRegistration[] {
|
|
return [...memoryPluginState.corpusSupplements];
|
|
}
|
|
export function registerMemoryPromptSupplement(
|
|
pluginId: string,
|
|
builder: MemoryPromptSectionBuilder,
|
|
): void {
|
|
const next = memoryPluginState.promptSupplements.filter(
|
|
(registration) => registration.pluginId !== pluginId,
|
|
);
|
|
next.push({ pluginId, builder });
|
|
memoryPluginState.promptSupplements = next;
|
|
}
|
|
|
|
export function registerMemoryPromptPreparation(
|
|
pluginId: string,
|
|
prepare: MemoryPromptSectionPreparer,
|
|
): void {
|
|
const next = memoryPluginState.promptPreparations.filter(
|
|
(registration) => registration.pluginId !== pluginId,
|
|
);
|
|
next.push({ pluginId, prepare });
|
|
memoryPluginState.promptPreparations = next;
|
|
}
|
|
|
|
function buildSynchronousMemoryPromptSection(params: MemoryPromptSectionParams): {
|
|
primary: string[];
|
|
supplements: Array<{ pluginId: string; lines: string[] }>;
|
|
} {
|
|
const primary = normalizeMemoryPromptLines(
|
|
memoryPluginState.capability?.capability.promptBuilder?.(params) ?? [],
|
|
);
|
|
const supplements = memoryPluginState.promptSupplements
|
|
// Keep supplement order stable even if plugin registration order changes.
|
|
.toSorted((left, right) => left.pluginId.localeCompare(right.pluginId))
|
|
.map((registration) => ({
|
|
pluginId: registration.pluginId,
|
|
lines: normalizeMemoryPromptLines(registration.builder(params)),
|
|
}));
|
|
return { primary, supplements };
|
|
}
|
|
|
|
function cloneMemoryPromptSectionParams(
|
|
params: MemoryPromptSectionParams,
|
|
): MemoryPromptSectionParams {
|
|
return {
|
|
availableTools: new Set(params.availableTools),
|
|
citationsMode: params.citationsMode,
|
|
agentId: params.agentId,
|
|
agentSessionKey: params.agentSessionKey,
|
|
sandboxed: params.sandboxed,
|
|
};
|
|
}
|
|
|
|
function snapshotMemoryPromptContext(
|
|
params: MemoryPromptSectionParams,
|
|
): PreparedMemoryPromptSection["context"] {
|
|
return Object.freeze({
|
|
availableTools: Object.freeze([...params.availableTools].toSorted()),
|
|
citationsMode: params.citationsMode,
|
|
agentId: params.agentId,
|
|
agentSessionKey: params.agentSessionKey,
|
|
sandboxed: params.sandboxed === true,
|
|
});
|
|
}
|
|
|
|
function preparedMemoryPromptContextMatches(
|
|
prepared: PreparedMemoryPromptSection,
|
|
params: MemoryPromptSectionParams,
|
|
): boolean {
|
|
const current = snapshotMemoryPromptContext(params);
|
|
return (
|
|
prepared.context.citationsMode === current.citationsMode &&
|
|
prepared.context.agentId === current.agentId &&
|
|
prepared.context.agentSessionKey === current.agentSessionKey &&
|
|
prepared.context.sandboxed === current.sandboxed &&
|
|
prepared.context.availableTools.length === current.availableTools.length &&
|
|
prepared.context.availableTools.every((tool, index) => tool === current.availableTools[index])
|
|
);
|
|
}
|
|
|
|
/** Prepare one immutable memory prompt snapshot for a run. */
|
|
export async function prepareMemoryPromptSection(
|
|
params: MemoryPromptSectionParams,
|
|
): Promise<PreparedMemoryPromptSection> {
|
|
const runParams = cloneMemoryPromptSectionParams(params);
|
|
const context = snapshotMemoryPromptContext(runParams);
|
|
const synchronous = buildSynchronousMemoryPromptSection(
|
|
cloneMemoryPromptSectionParams(runParams),
|
|
);
|
|
const preparationRegistrations = [...memoryPluginState.promptPreparations];
|
|
const preparedSupplements = await Promise.all(
|
|
preparationRegistrations.map(async (registration) => ({
|
|
pluginId: registration.pluginId,
|
|
lines: normalizeMemoryPromptLines(
|
|
await registration.prepare(cloneMemoryPromptSectionParams(runParams)),
|
|
),
|
|
})),
|
|
);
|
|
const lines = Object.freeze([
|
|
...synchronous.primary,
|
|
...[...synchronous.supplements, ...preparedSupplements]
|
|
.toSorted((left, right) => left.pluginId.localeCompare(right.pluginId))
|
|
.flatMap((registration) => registration.lines),
|
|
]);
|
|
const prepared = Object.freeze({
|
|
context,
|
|
lines,
|
|
});
|
|
preparedMemoryPromptSections.add(prepared);
|
|
return prepared;
|
|
}
|
|
|
|
/** Keep async preparation run-scoped while a context engine assembles synchronously. */
|
|
export async function runWithPreparedMemoryPromptSection<T>(
|
|
params: MemoryPromptSectionParams,
|
|
run: () => Promise<T>,
|
|
): Promise<T> {
|
|
const prepared = await prepareMemoryPromptSection(params);
|
|
return activePreparedMemoryPromptSection.run(prepared, run);
|
|
}
|
|
|
|
export function getActivePreparedMemoryPromptSection(): PreparedMemoryPromptSection | undefined {
|
|
return activePreparedMemoryPromptSection.getStore();
|
|
}
|
|
|
|
export function buildMemoryPromptSection(
|
|
params: MemoryPromptSectionParams,
|
|
prepared?: PreparedMemoryPromptSection,
|
|
): string[] {
|
|
if (prepared) {
|
|
// Run-scoped prompt state must never cross agent/session/tool boundaries.
|
|
if (
|
|
!preparedMemoryPromptSections.has(prepared) ||
|
|
!preparedMemoryPromptContextMatches(prepared, params)
|
|
) {
|
|
throw new Error("prepared memory prompt section does not match the current run");
|
|
}
|
|
return [...prepared.lines];
|
|
}
|
|
const synchronous = buildSynchronousMemoryPromptSection(params);
|
|
return [...synchronous.primary, ...synchronous.supplements.flatMap((entry) => entry.lines)];
|
|
}
|
|
|
|
function normalizeMemoryPromptLines(value: unknown): string[] {
|
|
if (!Array.isArray(value)) {
|
|
return [];
|
|
}
|
|
return value.filter((line): line is string => typeof line === "string");
|
|
}
|
|
|
|
export function listMemoryPromptSupplements(): MemoryPromptSupplementRegistration[] {
|
|
return [...memoryPluginState.promptSupplements];
|
|
}
|
|
export function listMemoryPromptPreparations(): MemoryPromptPreparationRegistration[] {
|
|
return [...memoryPluginState.promptPreparations];
|
|
}
|
|
export function resolveMemoryFlushPlan(params: {
|
|
cfg?: OpenClawConfig;
|
|
nowMs?: number;
|
|
}): MemoryFlushPlan | null {
|
|
return memoryPluginState.capability?.capability.flushPlanResolver?.(params) ?? null;
|
|
}
|
|
export function getMemoryRuntime(): MemoryPluginRuntime | undefined {
|
|
return memoryPluginState.capability?.capability.runtime;
|
|
}
|
|
|
|
export function hasMemoryRuntime(): boolean {
|
|
return getMemoryRuntime() !== undefined;
|
|
}
|
|
|
|
function cloneMemoryPublicArtifact(
|
|
artifact: MemoryPluginPublicArtifact,
|
|
): MemoryPluginPublicArtifact {
|
|
const agentIds = Array.isArray(artifact.agentIds) ? artifact.agentIds : [];
|
|
return {
|
|
...artifact,
|
|
agentIds: [...agentIds],
|
|
};
|
|
}
|
|
|
|
// The sort below dereferences these fields, so a plugin-supplied artifact
|
|
// missing any of them would crash every status/bridge consumer.
|
|
function isValidMemoryPublicArtifact(
|
|
artifact: MemoryPluginPublicArtifact | null | undefined,
|
|
): artifact is MemoryPluginPublicArtifact {
|
|
return (
|
|
typeof artifact?.kind === "string" &&
|
|
typeof artifact.workspaceDir === "string" &&
|
|
typeof artifact.relativePath === "string" &&
|
|
typeof artifact.absolutePath === "string" &&
|
|
typeof artifact.contentType === "string"
|
|
);
|
|
}
|
|
|
|
export async function listActiveMemoryPublicArtifacts(params: {
|
|
cfg: OpenClawConfig;
|
|
}): Promise<MemoryPluginPublicArtifact[]> {
|
|
const pluginId = memoryPluginState.capability?.pluginId;
|
|
const listed =
|
|
(await memoryPluginState.capability?.capability.publicArtifacts?.listArtifacts(params)) ?? [];
|
|
if (!Array.isArray(listed)) {
|
|
log.warn(`ignoring public memory artifacts from plugin "${pluginId}": not an array`);
|
|
return [];
|
|
}
|
|
const artifacts = listed.filter(isValidMemoryPublicArtifact);
|
|
if (artifacts.length < listed.length) {
|
|
log.warn(
|
|
`ignoring ${listed.length - artifacts.length} malformed public memory artifact(s) from plugin "${pluginId}": artifacts must include string kind, workspaceDir, relativePath, absolutePath, and contentType`,
|
|
);
|
|
}
|
|
return artifacts.map(cloneMemoryPublicArtifact).toSorted((left, right) => {
|
|
const workspaceOrder = left.workspaceDir.localeCompare(right.workspaceDir);
|
|
if (workspaceOrder !== 0) {
|
|
return workspaceOrder;
|
|
}
|
|
const relativePathOrder = left.relativePath.localeCompare(right.relativePath);
|
|
if (relativePathOrder !== 0) {
|
|
return relativePathOrder;
|
|
}
|
|
const kindOrder = left.kind.localeCompare(right.kind);
|
|
if (kindOrder !== 0) {
|
|
return kindOrder;
|
|
}
|
|
const contentTypeOrder = left.contentType.localeCompare(right.contentType);
|
|
if (contentTypeOrder !== 0) {
|
|
return contentTypeOrder;
|
|
}
|
|
const agentOrder = left.agentIds.join("\0").localeCompare(right.agentIds.join("\0"));
|
|
if (agentOrder !== 0) {
|
|
return agentOrder;
|
|
}
|
|
return left.absolutePath.localeCompare(right.absolutePath);
|
|
});
|
|
}
|
|
|
|
export function restoreMemoryPluginState(state: MemoryPluginState): void {
|
|
memoryPluginState.capability = state.capability
|
|
? {
|
|
pluginId: state.capability.pluginId,
|
|
capability: { ...state.capability.capability },
|
|
}
|
|
: undefined;
|
|
memoryPluginState.corpusSupplements = [...state.corpusSupplements];
|
|
memoryPluginState.promptPreparations = [...state.promptPreparations];
|
|
memoryPluginState.promptSupplements = [...state.promptSupplements];
|
|
}
|
|
|
|
export function clearMemoryPluginState(): void {
|
|
memoryPluginState.capability = undefined;
|
|
memoryPluginState.corpusSupplements = [];
|
|
memoryPluginState.promptPreparations = [];
|
|
memoryPluginState.promptSupplements = [];
|
|
}
|