Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions examples/claude-code-memory-plugin/scripts/auto-recall.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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
// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -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(
Expand Down
33 changes: 33 additions & 0 deletions examples/claude-code-memory-plugin/servers/memory-server.js
Original file line number Diff line number Diff line change
Expand Up @@ -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" });
}
Expand Down Expand Up @@ -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
// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -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) {
Expand Down
45 changes: 45 additions & 0 deletions examples/claude-code-memory-plugin/src/memory-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,14 @@ type FindResult = {
total?: number;
};

type CommitSessionResult = {
task_id?: string;
status?: string;
memories_extracted?: Record<string, number>;
active_count_updated?: number;
error?: unknown;
};

type ScopeName = "user" | "agent";

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -284,6 +292,21 @@ class OpenVikingClient {
);
}

async sessionUsed(sessionId: string, contexts: string[]): Promise<void> {
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<CommitSessionResult> {
return this.request<CommitSessionResult>(
`/api/v1/sessions/${encodeURIComponent(sessionId)}/commit`,
{ method: "POST", body: JSON.stringify({}) },
);
}

async deleteSession(sessionId: string): Promise<void> {
await this.request(`/api/v1/sessions/${encodeURIComponent(sessionId)}`, { method: "DELETE" });
}
Expand Down Expand Up @@ -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
// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -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) => {
Expand Down
13 changes: 13 additions & 0 deletions examples/openclaw-plugin/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -512,4 +512,17 @@ export class OpenVikingClient {
method: "DELETE",
}, agentId);
}

async sessionUsed(
sessionId: string,
contexts: string[],
agentId?: string,
): Promise<void> {
if (contexts.length === 0) return;
await this.request(
`/api/v1/sessions/${encodeURIComponent(sessionId)}/used`,
{ method: "POST", body: JSON.stringify({ contexts }) },
agentId,
);
}
}
15 changes: 13 additions & 2 deletions examples/openclaw-plugin/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -708,8 +708,8 @@ const contextEnginePlugin = {
async execute(_toolCallId: string, params: Record<string, unknown>) {
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`);
Expand Down Expand Up @@ -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),
Expand Down