mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 23:20:43 +00:00
* [EV-001] memory-core: filter memory_search session hits by visibility - Move session visibility + listSpawnedSessionKeys to plugin-sdk; sync test hook with sessions-resolution __testing.setDepsForTest - Extract loadCombinedSessionStoreForGateway to config/sessions; re-export from gateway session-utils - Add session-transcript-hit stem resolver for builtin + QMD paths - Post-filter memory_search results before citations/recall; fail closed when requester session key missing; optional corpus=sessions - Tests: stem extraction, visibility filter smoke, existing suites green * chore: sync plugin-sdk exports for session-transcript-hit and session-visibility Run pnpm plugin-sdk:sync-exports so package.json exports match scripts/lib/plugin-sdk-entrypoints.json. Fixes contract tests and lint:plugins:plugin-sdk-subpaths-exported for memory-core imports. * fix(EV-001): cross-agent session memory hits + hoist combined store load - resolveTranscriptStemToSessionKeys: stop filtering by requester agentId so keys from other agents reach createSessionVisibilityGuard (a2a + visibility=all). - Re-export loadCombinedSessionStoreForGateway from session-transcript-hit; filterMemorySearchHitsBySessionVisibility loads the combined store once per pass. - Drop unused agentId from filter params; extend tests (Greptile/Codex review). * fix(memory_search): honor corpus=sessions before maxResults cap Pass sources into MemoryIndexManager.search so FTS/vector queries add source IN (...) before ranking and top-N slice (Codex: non-session hits could fill the window). QMD path: oversample fetch limit for single-source recall, filter by source, then diversify/clamp to the requested maxResults. Wire corpus=sessions from tools; extend MemorySearchManager opts and wrappers. * fix(memory_search): apply corpus=memory source filter like sessions Pass sources: ["memory"] into manager.search so maxResults applies only within the memory index; post-filter for defense in depth. Document corpus=memory in the tool description. * fix: scope qmd session memory search * fix: enforce memory search session visibility (#70761) (thanks @nefainl) --------- Co-authored-by: NefAI <info@nefai.nl> Co-authored-by: Ayaan Zaidi <hi@obviy.us>
194 lines
5.3 KiB
TypeScript
194 lines
5.3 KiB
TypeScript
import {
|
|
listMemoryCorpusSupplements,
|
|
resolveMemorySearchConfig,
|
|
resolveSessionAgentId,
|
|
type MemoryCorpusSearchResult,
|
|
type AnyAgentTool,
|
|
type OpenClawConfig,
|
|
} from "openclaw/plugin-sdk/memory-core-host-runtime-core";
|
|
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
|
|
import { Type } from "typebox";
|
|
|
|
type MemoryToolRuntime = typeof import("./tools.runtime.js");
|
|
type MemorySearchManagerResult = Awaited<
|
|
ReturnType<(typeof import("./memory/index.js"))["getMemorySearchManager"]>
|
|
>;
|
|
|
|
let memoryToolRuntimePromise: Promise<MemoryToolRuntime> | null = null;
|
|
|
|
export async function loadMemoryToolRuntime(): Promise<MemoryToolRuntime> {
|
|
memoryToolRuntimePromise ??= import("./tools.runtime.js");
|
|
return await memoryToolRuntimePromise;
|
|
}
|
|
|
|
export const MemorySearchSchema = Type.Object({
|
|
query: Type.String(),
|
|
maxResults: Type.Optional(Type.Number()),
|
|
minScore: Type.Optional(Type.Number()),
|
|
corpus: Type.Optional(
|
|
Type.Union([
|
|
Type.Literal("memory"),
|
|
Type.Literal("wiki"),
|
|
Type.Literal("all"),
|
|
Type.Literal("sessions"),
|
|
]),
|
|
),
|
|
});
|
|
|
|
export const MemoryGetSchema = Type.Object({
|
|
path: Type.String(),
|
|
from: Type.Optional(Type.Number()),
|
|
lines: Type.Optional(Type.Number()),
|
|
corpus: Type.Optional(
|
|
Type.Union([Type.Literal("memory"), Type.Literal("wiki"), Type.Literal("all")]),
|
|
),
|
|
});
|
|
|
|
export function resolveMemoryToolContext(options: {
|
|
config?: OpenClawConfig;
|
|
agentSessionKey?: string;
|
|
}) {
|
|
const cfg = options.config;
|
|
if (!cfg) {
|
|
return null;
|
|
}
|
|
const agentId = resolveSessionAgentId({
|
|
sessionKey: options.agentSessionKey,
|
|
config: cfg,
|
|
});
|
|
if (!resolveMemorySearchConfig(cfg, agentId)) {
|
|
return null;
|
|
}
|
|
return { cfg, agentId };
|
|
}
|
|
|
|
export async function getMemoryManagerContext(params: {
|
|
cfg: OpenClawConfig;
|
|
agentId: string;
|
|
}): Promise<
|
|
| {
|
|
manager: NonNullable<MemorySearchManagerResult["manager"]>;
|
|
}
|
|
| {
|
|
error: string | undefined;
|
|
}
|
|
> {
|
|
return await getMemoryManagerContextWithPurpose({ ...params, purpose: undefined });
|
|
}
|
|
|
|
export async function getMemoryManagerContextWithPurpose(params: {
|
|
cfg: OpenClawConfig;
|
|
agentId: string;
|
|
purpose?: "default" | "status";
|
|
}): Promise<
|
|
| {
|
|
manager: NonNullable<MemorySearchManagerResult["manager"]>;
|
|
}
|
|
| {
|
|
error: string | undefined;
|
|
}
|
|
> {
|
|
const { getMemorySearchManager } = await loadMemoryToolRuntime();
|
|
const { manager, error } = await getMemorySearchManager({
|
|
cfg: params.cfg,
|
|
agentId: params.agentId,
|
|
purpose: params.purpose,
|
|
});
|
|
return manager ? { manager } : { error };
|
|
}
|
|
|
|
export function createMemoryTool(params: {
|
|
options: {
|
|
config?: OpenClawConfig;
|
|
agentSessionKey?: string;
|
|
};
|
|
label: string;
|
|
name: string;
|
|
description: string;
|
|
parameters: typeof MemorySearchSchema | typeof MemoryGetSchema;
|
|
execute: (ctx: { cfg: OpenClawConfig; agentId: string }) => AnyAgentTool["execute"];
|
|
}): AnyAgentTool | null {
|
|
const ctx = resolveMemoryToolContext(params.options);
|
|
if (!ctx) {
|
|
return null;
|
|
}
|
|
return {
|
|
label: params.label,
|
|
name: params.name,
|
|
description: params.description,
|
|
parameters: params.parameters,
|
|
execute: params.execute(ctx),
|
|
};
|
|
}
|
|
|
|
export function buildMemorySearchUnavailableResult(error: string | undefined) {
|
|
const reason = (error ?? "memory search unavailable").trim() || "memory search unavailable";
|
|
const isQuotaError = /insufficient_quota|quota|429/.test(normalizeLowercaseStringOrEmpty(reason));
|
|
const warning = isQuotaError
|
|
? "Memory search is unavailable because the embedding provider quota is exhausted."
|
|
: "Memory search is unavailable due to an embedding/provider error.";
|
|
const action = isQuotaError
|
|
? "Top up or switch embedding provider, then retry memory_search."
|
|
: "Check embedding provider configuration and retry memory_search.";
|
|
return {
|
|
results: [],
|
|
disabled: true,
|
|
unavailable: true,
|
|
error: reason,
|
|
warning,
|
|
action,
|
|
debug: {
|
|
warning,
|
|
action,
|
|
error: reason,
|
|
},
|
|
};
|
|
}
|
|
|
|
export async function searchMemoryCorpusSupplements(params: {
|
|
query: string;
|
|
maxResults?: number;
|
|
agentSessionKey?: string;
|
|
corpus?: "memory" | "wiki" | "all" | "sessions";
|
|
}): Promise<MemoryCorpusSearchResult[]> {
|
|
if (params.corpus === "memory" || params.corpus === "sessions") {
|
|
return [];
|
|
}
|
|
const supplements = listMemoryCorpusSupplements();
|
|
if (supplements.length === 0) {
|
|
return [];
|
|
}
|
|
const results = (
|
|
await Promise.all(
|
|
supplements.map(async (registration) => await registration.supplement.search(params)),
|
|
)
|
|
).flat();
|
|
return results
|
|
.toSorted((left, right) => {
|
|
if (left.score !== right.score) {
|
|
return right.score - left.score;
|
|
}
|
|
return left.path.localeCompare(right.path);
|
|
})
|
|
.slice(0, Math.max(1, params.maxResults ?? 10));
|
|
}
|
|
|
|
export async function getMemoryCorpusSupplementResult(params: {
|
|
lookup: string;
|
|
fromLine?: number;
|
|
lineCount?: number;
|
|
agentSessionKey?: string;
|
|
corpus?: "memory" | "wiki" | "all" | "sessions";
|
|
}) {
|
|
if (params.corpus === "memory" || params.corpus === "sessions") {
|
|
return null;
|
|
}
|
|
for (const registration of listMemoryCorpusSupplements()) {
|
|
const result = await registration.supplement.get(params);
|
|
if (result) {
|
|
return result;
|
|
}
|
|
}
|
|
return null;
|
|
}
|