diff --git a/examples/openclaw-memory-plugin/client.ts b/examples/openclaw-memory-plugin/client.ts index 56489999..c48b2b69 100644 --- a/examples/openclaw-memory-plugin/client.ts +++ b/examples/openclaw-memory-plugin/client.ts @@ -47,16 +47,36 @@ export function isMemoryUri(uri: string): boolean { } export class OpenVikingClient { - private readonly resolvedSpaceByScope: Partial> = {}; + private resolvedSpaceByScope: Partial> = {}; private runtimeIdentity: RuntimeIdentity | null = null; constructor( private readonly baseUrl: string, private readonly apiKey: string, - private readonly agentId: string, + private agentId: string, private readonly timeoutMs: number, ) {} + /** + * Dynamically switch the agent identity for multi-agent memory isolation. + * When a shared client serves multiple agents (e.g. in OpenClaw multi-agent + * gateway), call this before each agent's recall/capture to route memories + * to the correct agent_space = md5(user_id + agent_id)[:12]. + * Clears cached space resolution so the next request re-derives agent_space. + */ + setAgentId(newAgentId: string): void { + if (newAgentId && newAgentId !== this.agentId) { + this.agentId = newAgentId; + // Clear cached identity and spaces — they depend on agentId + this.runtimeIdentity = null; + this.resolvedSpaceByScope = {}; + } + } + + getAgentId(): string { + return this.agentId; + } + private async request(path: string, init: RequestInit = {}): Promise { const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), this.timeoutMs); diff --git a/examples/openclaw-memory-plugin/config.ts b/examples/openclaw-memory-plugin/config.ts index 3fccd12e..40c73f03 100644 --- a/examples/openclaw-memory-plugin/config.ts +++ b/examples/openclaw-memory-plugin/config.ts @@ -16,6 +16,8 @@ export type MemoryOpenVikingConfig = { timeoutMs?: number; autoCapture?: boolean; captureMode?: "semantic" | "keyword"; + /** Minimum sanitized text length (chars) required to trigger auto-capture. Default 50. */ + captureMinLength?: number; captureMaxLength?: number; autoRecall?: boolean; recallLimit?: number; @@ -30,6 +32,7 @@ const DEFAULT_PORT = 1933; const DEFAULT_TARGET_URI = "viking://user/memories"; const DEFAULT_TIMEOUT_MS = 15000; const DEFAULT_CAPTURE_MODE = "semantic"; +const DEFAULT_CAPTURE_MIN_LENGTH = 50; const DEFAULT_CAPTURE_MAX_LENGTH = 24000; const DEFAULT_RECALL_LIMIT = 6; const DEFAULT_RECALL_SCORE_THRESHOLD = 0.01; @@ -105,6 +108,7 @@ export const memoryOpenVikingConfigSchema = { "timeoutMs", "autoCapture", "captureMode", + "captureMinLength", "captureMaxLength", "autoRecall", "recallLimit", @@ -153,6 +157,10 @@ export const memoryOpenVikingConfigSchema = { timeoutMs: Math.max(1000, Math.floor(toNumber(cfg.timeoutMs, DEFAULT_TIMEOUT_MS))), autoCapture: cfg.autoCapture !== false, captureMode: captureMode ?? DEFAULT_CAPTURE_MODE, + captureMinLength: Math.max( + 1, + Math.min(1000, Math.floor(toNumber(cfg.captureMinLength, DEFAULT_CAPTURE_MIN_LENGTH))), + ), captureMaxLength: Math.max( 200, Math.min(200_000, Math.floor(toNumber(cfg.captureMaxLength, DEFAULT_CAPTURE_MAX_LENGTH))), @@ -237,6 +245,12 @@ export const memoryOpenVikingConfigSchema = { advanced: true, help: '"semantic" captures all eligible user text and relies on OpenViking extraction; "keyword" uses trigger regex first.', }, + captureMinLength: { + label: "Capture Min Length", + placeholder: String(DEFAULT_CAPTURE_MIN_LENGTH), + advanced: true, + help: "Minimum sanitized text length (chars) required to trigger auto-capture. Shorter messages are skipped to save VLM tokens.", + }, captureMaxLength: { label: "Capture Max Length", placeholder: String(DEFAULT_CAPTURE_MAX_LENGTH), diff --git a/examples/openclaw-memory-plugin/index.ts b/examples/openclaw-memory-plugin/index.ts index 4a2a4499..8eff483d 100644 --- a/examples/openclaw-memory-plugin/index.ts +++ b/examples/openclaw-memory-plugin/index.ts @@ -354,7 +354,16 @@ const memoryPlugin = { ); if (cfg.autoRecall || cfg.ingestReplyAssist) { - api.on("before_agent_start", async (event: { messages?: unknown[]; prompt: string }) => { + api.on("before_agent_start", async (event: { messages?: unknown[]; prompt: string }, ctx?: { agentId?: string }) => { + // Dynamically switch agent identity for multi-agent memory isolation. + // In multi-agent gateway deployments, the hook context carries the current + // agent's ID so we route memory operations to the correct agent_space. + const hookAgentId = ctx?.agentId; + if (hookAgentId) { + const client = await getClient(); + client.setAgentId(hookAgentId); + api.logger.info?.(`memory-openviking: switched to agentId=${hookAgentId} for recall`); + } const queryText = extractLatestUserText(event.messages) || event.prompt.trim(); if (!queryText) { return; @@ -474,7 +483,14 @@ const memoryPlugin = { if (cfg.autoCapture) { let lastProcessedMsgCount = 0; - api.on("agent_end", async (event: { success?: boolean; messages?: unknown[] }) => { + api.on("agent_end", async (event: { success?: boolean; messages?: unknown[] }, ctx?: { agentId?: string }) => { + // Dynamically switch agent identity for multi-agent memory isolation + const hookAgentId = ctx?.agentId; + if (hookAgentId) { + const client = await getClient(); + client.setAgentId(hookAgentId); + api.logger.info?.(`memory-openviking: switched to agentId=${hookAgentId} for capture`); + } if (!event.success || !event.messages || event.messages.length === 0) { api.logger.info( `memory-openviking: auto-capture skipped (success=${String(event.success)}, messages=${event.messages?.length ?? 0})`, @@ -495,7 +511,7 @@ const memoryPlugin = { const turnText = newTexts.join("\n"); // 对合并文本做 capture decision(主要检查长度和命令过滤) - const decision = getCaptureDecision(turnText, cfg.captureMode, cfg.captureMaxLength); + const decision = getCaptureDecision(turnText, cfg.captureMode, cfg.captureMinLength, cfg.captureMaxLength); const preview = turnText.length > 80 ? `${turnText.slice(0, 80)}...` : turnText; api.logger.info( `memory-openviking: capture-check shouldCapture=${String(decision.shouldCapture)} reason=${decision.reason} newMsgCount=${newCount} text="${preview}"`, diff --git a/examples/openclaw-memory-plugin/text-utils.ts b/examples/openclaw-memory-plugin/text-utils.ts index 40f224a4..6dd18d87 100644 --- a/examples/openclaw-memory-plugin/text-utils.ts +++ b/examples/openclaw-memory-plugin/text-utils.ts @@ -206,7 +206,7 @@ export function pickRecentUniqueTexts(texts: string[], limit: number): string[] return picked.reverse(); } -export function getCaptureDecision(text: string, mode: CaptureMode, captureMaxLength: number): { +export function getCaptureDecision(text: string, mode: CaptureMode, captureMinLength: number, captureMaxLength: number): { shouldCapture: boolean; reason: string; normalizedText: string; @@ -223,7 +223,7 @@ export function getCaptureDecision(text: string, mode: CaptureMode, captureMaxLe } const compactText = normalizedText.replace(/\s+/g, ""); - const minLength = resolveCaptureMinLength(compactText); + const minLength = Math.max(resolveCaptureMinLength(compactText), captureMinLength); if (compactText.length < minLength || normalizedText.length > captureMaxLength) { return { shouldCapture: false,