diff --git a/examples/claude-code-memory-plugin/scripts/auto-recall.mjs b/examples/claude-code-memory-plugin/scripts/auto-recall.mjs index 2718a04ea..6df449b42 100644 --- a/examples/claude-code-memory-plugin/scripts/auto-recall.mjs +++ b/examples/claude-code-memory-plugin/scripts/auto-recall.mjs @@ -208,6 +208,38 @@ async function resolveTargetUri(targetUri) { return `viking://${scope}/${space}/${parts.join("/")}`; } +function markRecalledMemoriesUsed(contexts) { + const uniqueContexts = [...new Set(contexts.filter(uri => typeof uri === "string" && uri.length > 0))]; + if (uniqueContexts.length === 0) return; + + void (async () => { + const sessionResult = await fetchJSON("/api/v1/sessions", { + method: "POST", + body: JSON.stringify({}), + }); + if (!sessionResult?.session_id) return; + + const sessionId = sessionResult.session_id; + try { + await fetchJSON(`/api/v1/sessions/${encodeURIComponent(sessionId)}/used`, { + method: "POST", + body: JSON.stringify({ contexts: uniqueContexts }), + }); + await fetchJSON(`/api/v1/sessions/${encodeURIComponent(sessionId)}/commit`, { + method: "POST", + body: JSON.stringify({}), + }); + log("used_signal", { sessionId, count: uniqueContexts.length, uris: uniqueContexts }); + } catch (err) { + logError("used_signal_failed", err); + } finally { + await fetchJSON(`/api/v1/sessions/${encodeURIComponent(sessionId)}`, { + method: "DELETE", + }).catch(() => {}); + } + })(); +} + // --------------------------------------------------------------------------- // Search OpenViking // --------------------------------------------------------------------------- @@ -331,6 +363,7 @@ async function main() { } log("picked", { pickedCount: memories.length, uris: memories.map(m => m.uri) }); + markRecalledMemoriesUsed(memories.map(memory => memory.uri)); // Read full content for leaf memories const lines = await Promise.all( diff --git a/examples/claude-code-memory-plugin/servers/memory-server.js b/examples/claude-code-memory-plugin/servers/memory-server.js index 0f0e494dc..9b0f354a7 100644 --- a/examples/claude-code-memory-plugin/servers/memory-server.js +++ b/examples/claude-code-memory-plugin/servers/memory-server.js @@ -228,6 +228,17 @@ class OpenVikingClient { async extractSessionMemories(sessionId) { return this.request(`/api/v1/sessions/${encodeURIComponent(sessionId)}/extract`, { method: "POST", body: JSON.stringify({}) }); } + async sessionUsed(sessionId, contexts) { + if (contexts.length === 0) + return; + await this.request(`/api/v1/sessions/${encodeURIComponent(sessionId)}/used`, { + method: "POST", + body: JSON.stringify({ contexts }), + }); + } + async commitSession(sessionId) { + return this.request(`/api/v1/sessions/${encodeURIComponent(sessionId)}/commit`, { method: "POST", body: JSON.stringify({}) }); + } async deleteSession(sessionId) { await this.request(`/api/v1/sessions/${encodeURIComponent(sessionId)}`, { method: "DELETE" }); } @@ -366,6 +377,27 @@ async function searchBothScopes(client, query, limit) { const unique = all.filter((m, i, self) => i === self.findIndex((o) => o.uri === m.uri)); return unique.filter((m) => m.level === 2); } +function markRecalledMemoriesUsed(client, contexts) { + const uniqueContexts = [...new Set(contexts.filter((uri) => typeof uri === "string" && uri.length > 0))]; + if (uniqueContexts.length === 0) + return; + void (async () => { + let sessionId; + try { + sessionId = await client.createSession(); + await client.sessionUsed(sessionId, uniqueContexts); + await client.commitSession(sessionId); + } + catch { + // Fire-and-forget usage tracking must never block or fail the caller. + } + finally { + if (sessionId) { + await client.deleteSession(sessionId).catch(() => { }); + } + } + })(); +} // --------------------------------------------------------------------------- // MCP Server // --------------------------------------------------------------------------- @@ -397,6 +429,7 @@ server.tool("memory_recall", "Search long-term memories from OpenViking. Use whe if (memories.length === 0) { return { content: [{ type: "text", text: "No relevant memories found in OpenViking." }] }; } + markRecalledMemoriesUsed(client, memories.map((memory) => memory.uri)); // Read full content for leaf memories const lines = await Promise.all(memories.map(async (item) => { if (item.level === 2) { diff --git a/examples/claude-code-memory-plugin/src/memory-server.ts b/examples/claude-code-memory-plugin/src/memory-server.ts index 2dd0ac0ad..560380c0a 100644 --- a/examples/claude-code-memory-plugin/src/memory-server.ts +++ b/examples/claude-code-memory-plugin/src/memory-server.ts @@ -36,6 +36,14 @@ type FindResult = { total?: number; }; +type CommitSessionResult = { + task_id?: string; + status?: string; + memories_extracted?: Record; + active_count_updated?: number; + error?: unknown; +}; + type ScopeName = "user" | "agent"; // --------------------------------------------------------------------------- @@ -284,6 +292,21 @@ class OpenVikingClient { ); } + async sessionUsed(sessionId: string, contexts: string[]): Promise { + if (contexts.length === 0) return; + await this.request(`/api/v1/sessions/${encodeURIComponent(sessionId)}/used`, { + method: "POST", + body: JSON.stringify({ contexts }), + }); + } + + async commitSession(sessionId: string): Promise { + return this.request( + `/api/v1/sessions/${encodeURIComponent(sessionId)}/commit`, + { method: "POST", body: JSON.stringify({}) }, + ); + } + async deleteSession(sessionId: string): Promise { await this.request(`/api/v1/sessions/${encodeURIComponent(sessionId)}`, { method: "DELETE" }); } @@ -437,6 +460,26 @@ async function searchBothScopes( return unique.filter((m) => m.level === 2); } +function markRecalledMemoriesUsed(client: OpenVikingClient, contexts: string[]): void { + const uniqueContexts = [...new Set(contexts.filter((uri) => typeof uri === "string" && uri.length > 0))]; + if (uniqueContexts.length === 0) return; + + void (async () => { + let sessionId: string | undefined; + try { + sessionId = await client.createSession(); + await client.sessionUsed(sessionId, uniqueContexts); + await client.commitSession(sessionId); + } catch { + // Fire-and-forget usage tracking must never block or fail the caller. + } finally { + if (sessionId) { + await client.deleteSession(sessionId).catch(() => {}); + } + } + })(); +} + // --------------------------------------------------------------------------- // MCP Server // --------------------------------------------------------------------------- @@ -479,6 +522,8 @@ server.tool( return { content: [{ type: "text" as const, text: "No relevant memories found in OpenViking." }] }; } + markRecalledMemoriesUsed(client, memories.map((memory) => memory.uri)); + // Read full content for leaf memories const lines = await Promise.all( memories.map(async (item) => { diff --git a/examples/openclaw-plugin/client.ts b/examples/openclaw-plugin/client.ts index aba3e488b..a7d6c352c 100644 --- a/examples/openclaw-plugin/client.ts +++ b/examples/openclaw-plugin/client.ts @@ -512,4 +512,17 @@ export class OpenVikingClient { method: "DELETE", }, agentId); } + + async sessionUsed( + sessionId: string, + contexts: string[], + agentId?: string, + ): Promise { + if (contexts.length === 0) return; + await this.request( + `/api/v1/sessions/${encodeURIComponent(sessionId)}/used`, + { method: "POST", body: JSON.stringify({ contexts }) }, + agentId, + ); + } } diff --git a/examples/openclaw-plugin/index.ts b/examples/openclaw-plugin/index.ts index 7692d077a..50ee84114 100644 --- a/examples/openclaw-plugin/index.ts +++ b/examples/openclaw-plugin/index.ts @@ -708,8 +708,8 @@ const contextEnginePlugin = { async execute(_toolCallId: string, params: Record) { rememberSessionAgentId(ctx); const archiveId = String((params as { archiveId?: string }).archiveId ?? "").trim(); - const sessionId = ctx.sessionId ?? ""; - api.logger.info?.(`openviking: ov_archive_expand invoked (archiveId=${archiveId || "(empty)"}, sessionId=${sessionId || "(empty)"})`); + const activeSessionId = ctx.sessionId ?? ""; + api.logger.info?.(`openviking: ov_archive_expand invoked (archiveId=${archiveId || "(empty)"}, sessionId=${activeSessionId || "(empty)"})`); if (!archiveId) { api.logger.warn?.(`openviking: ov_archive_expand missing archiveId`); @@ -892,6 +892,17 @@ const contextEnginePlugin = { const memories = pickMemoriesForInjection(processed, cfg.recallLimit, queryText); if (memories.length > 0) { + const recalledUris = memories + .map((memory) => memory.uri) + .filter((uri): uri is string => typeof uri === "string" && uri.length > 0); + const ovSessionId = openClawSessionToOvStorageId( + ctx?.sessionId, + ctx?.sessionKey, + ); + void client.sessionUsed(ovSessionId, recalledUris, agentId).catch((err) => { + api.logger.warn(`openviking: sessionUsed failed: ${String(err)}`); + }); + const { lines: memoryLines, estimatedTokens } = await buildMemoryLinesWithBudget( memories, (uri) => client.read(uri, agentId),