Skip to content
Open
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
28 changes: 24 additions & 4 deletions cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import * as readline from "node:readline";
import JSON5 from "json5";
import { loadLanceDB, type MemoryEntry, type MemoryStore } from "./src/store.js";
import { createRetriever, type MemoryRetriever } from "./src/retriever.js";
import type { MemoryScopeManager } from "./src/scopes.js";
import { summarizeScopesByType, type MemoryScopeManager } from "./src/scopes.js";
import type { MemoryMigrator } from "./src/migrate.js";
import { createMemoryUpgrader } from "./src/memory-upgrader.js";
import type { LlmClient } from "./src/llm-client.js";
Expand Down Expand Up @@ -749,12 +749,19 @@ export function registerMemoryCLI(program: Command, context: CLIContext): void {
}

const stats = await context.store.stats(scopeFilter);
const scopeStats = context.scopeManager.getStats();
const configuredScopeStats = context.scopeManager.getStats();
const observedScopeStats = summarizeScopesByType(Object.keys(stats.scopeCounts));
const retrievalConfig = context.retriever.getConfig();

const summary = {
memory: stats,
scopes: scopeStats,
scopes: {
totalScopes: configuredScopeStats.totalScopes,
agentsWithCustomAccess: configuredScopeStats.agentsWithCustomAccess,
scopesByType: configuredScopeStats.scopesByType,
configured: configuredScopeStats,
observed: observedScopeStats,
},
retrieval: {
mode: retrievalConfig.mode,
hasFtsSupport: context.store.hasFtsSupport,
Expand All @@ -766,11 +773,24 @@ export function registerMemoryCLI(program: Command, context: CLIContext): void {
} else {
console.log(`Memory Statistics:`);
console.log(`• Total memories: ${stats.totalCount}`);
console.log(`• Available scopes: ${scopeStats.totalScopes}`);
console.log(`• Configured scopes: ${configuredScopeStats.totalScopes}`);
console.log(`• Observed scopes in DB: ${observedScopeStats.totalScopes}`);
console.log(`• Retrieval mode: ${retrievalConfig.mode}`);
console.log(`• FTS support: ${context.store.hasFtsSupport ? 'Yes' : 'No'}`);
console.log();

console.log("Observed scopes by type:");
Object.entries(observedScopeStats.scopesByType).forEach(([type, count]) => {
console.log(` • ${type}: ${count}`);
});
console.log();

console.log("Configured scopes by type:");
Object.entries(configuredScopeStats.scopesByType).forEach(([type, count]) => {
console.log(` • ${type}: ${count}`);
});
console.log();

console.log("Memories by scope:");
Object.entries(stats.scopeCounts).forEach(([scope, count]) => {
console.log(` • ${scope}: ${count}`);
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
]
},
"scripts": {
"test": "node test/embedder-error-hints.test.mjs && node test/cjk-recursion-regression.test.mjs && node test/migrate-legacy-schema.test.mjs && node --test test/config-session-strategy-migration.test.mjs && node --test test/scope-access-undefined.test.mjs && node --test test/reflection-bypass-hook.test.mjs && node --test test/smart-extractor-scope-filter.test.mjs && node --test test/store-empty-scope-filter.test.mjs && node --test test/recall-text-cleanup.test.mjs && node test/update-consistency-lancedb.test.mjs && node --test test/strip-envelope-metadata.test.mjs && node test/cli-smoke.mjs && node test/functional-e2e.mjs && node test/retriever-rerank-regression.mjs && node test/smart-memory-lifecycle.mjs && node test/smart-extractor-branches.mjs && node test/plugin-manifest-regression.mjs && node --test test/session-summary-before-reset.test.mjs && node --test test/sync-plugin-version.test.mjs && node test/smart-metadata-v2.mjs && node test/vector-search-cosine.test.mjs && node test/context-support-e2e.mjs && node test/temporal-facts.test.mjs && node test/memory-update-supersede.test.mjs && node test/memory-upgrader-diagnostics.test.mjs && node --test test/llm-api-key-client.test.mjs && node --test test/llm-oauth-client.test.mjs && node --test test/cli-oauth-login.test.mjs && node --test test/workflow-fork-guards.test.mjs && node --test test/clawteam-scope.test.mjs && node --test test/cross-process-lock.test.mjs && node --test test/preference-slots.test.mjs",
"test": "node test/embedder-error-hints.test.mjs && node test/cjk-recursion-regression.test.mjs && node test/migrate-legacy-schema.test.mjs && node --test test/config-session-strategy-migration.test.mjs && node --test test/scope-access-undefined.test.mjs && node --test test/scope-stats-regression.test.mjs && node --test test/reflection-bypass-hook.test.mjs && node --test test/smart-extractor-scope-filter.test.mjs && node --test test/store-empty-scope-filter.test.mjs && node --test test/recall-text-cleanup.test.mjs && node test/update-consistency-lancedb.test.mjs && node --test test/strip-envelope-metadata.test.mjs && node test/cli-smoke.mjs && node test/functional-e2e.mjs && node test/retriever-rerank-regression.mjs && node test/smart-memory-lifecycle.mjs && node test/smart-extractor-branches.mjs && node test/plugin-manifest-regression.mjs && node --test test/session-summary-before-reset.test.mjs && node --test test/sync-plugin-version.test.mjs && node test/smart-metadata-v2.mjs && node test/vector-search-cosine.test.mjs && node test/context-support-e2e.mjs && node test/temporal-facts.test.mjs && node test/memory-update-supersede.test.mjs && node test/memory-upgrader-diagnostics.test.mjs && node --test test/llm-api-key-client.test.mjs && node --test test/llm-oauth-client.test.mjs && node --test test/cli-oauth-login.test.mjs && node --test test/workflow-fork-guards.test.mjs && node --test test/clawteam-scope.test.mjs && node --test test/cross-process-lock.test.mjs && node --test test/preference-slots.test.mjs",
"test:openclaw-host": "node test/openclaw-host-functional.mjs",
"version": "node scripts/sync-plugin-version.mjs openclaw.plugin.json package.json && git add openclaw.plugin.json"
},
Expand Down
68 changes: 40 additions & 28 deletions src/scopes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,43 @@ export function isSystemBypassId(agentId?: string): boolean {
return typeof agentId === "string" && SYSTEM_BYPASS_IDS.has(agentId);
}

export function summarizeScopesByType(scopes: string[]): {
totalScopes: number;
scopesByType: Record<string, number>;
} {
const scopesByType: Record<string, number> = {
global: 0,
agent: 0,
custom: 0,
project: 0,
user: 0,
other: 0,
};

for (const scope of scopes) {
if (scope === "global") {
scopesByType.global++;
} else if (scope.startsWith("agent:")) {
scopesByType.agent++;
} else if (scope.startsWith("custom:")) {
scopesByType.custom++;
} else if (scope.startsWith("project:")) {
scopesByType.project++;
} else if (scope.startsWith("user:") || scope.startsWith("reflection:")) {
// TODO: add a dedicated `reflection` bucket once downstream dashboards accept it.
// For now, reflection scopes are counted under `user` for schema compatibility.
scopesByType.user++;
} else {
scopesByType.other++;
}
}

return {
totalScopes: scopes.length,
scopesByType,
};
}

/** @internal Exported for testing only — resets the legacy warning throttle. */
export function _resetLegacyFallbackWarningState(): void {
warnedLegacyFallbackBypassIds.clear();
Expand Down Expand Up @@ -412,37 +449,12 @@ export class MemoryScopeManager implements ScopeManager {
scopesByType: Record<string, number>;
} {
const scopes = this.getAllScopes();
const scopesByType: Record<string, number> = {
global: 0,
agent: 0,
custom: 0,
project: 0,
user: 0,
other: 0,
};

for (const scope of scopes) {
if (scope === "global") {
scopesByType.global++;
} else if (scope.startsWith("agent:")) {
scopesByType.agent++;
} else if (scope.startsWith("custom:")) {
scopesByType.custom++;
} else if (scope.startsWith("project:")) {
scopesByType.project++;
} else if (scope.startsWith("user:") || scope.startsWith("reflection:")) {
// TODO: add a dedicated `reflection` bucket once downstream dashboards accept it.
// For now, reflection scopes are counted under `user` for schema compatibility.
scopesByType.user++;
} else {
scopesByType.other++;
}
}
const summary = summarizeScopesByType(scopes);

return {
totalScopes: scopes.length,
totalScopes: summary.totalScopes,
agentsWithCustomAccess: Object.keys(this.config.agentAccess).length,
scopesByType,
scopesByType: summary.scopesByType,
};
}
}
Expand Down
26 changes: 22 additions & 4 deletions src/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { join } from "node:path";
import type { MemoryRetriever, RetrievalResult } from "./retriever.js";
import type { MemoryStore } from "./store.js";
import { isNoise } from "./noise-filter.js";
import { isSystemBypassId, resolveScopeFilter, parseAgentIdFromSessionKey, type MemoryScopeManager } from "./scopes.js";
import { isSystemBypassId, resolveScopeFilter, parseAgentIdFromSessionKey, summarizeScopesByType, type MemoryScopeManager } from "./scopes.js";
import type { Embedder } from "./embedder.js";
import {
appendRelation,
Expand Down Expand Up @@ -1335,16 +1335,28 @@ export function registerMemoryStatsTool(
}

const stats = await context.store.stats(scopeFilter);
const scopeManagerStats = context.scopeManager.getStats();
const configuredScopeStats = context.scopeManager.getStats();
const observedScopeStats = summarizeScopesByType(Object.keys(stats.scopeCounts));
const retrievalConfig = context.retriever.getConfig();

const textLines = [
`Memory Statistics:`,
`\u2022 Total memories: ${stats.totalCount}`,
`\u2022 Available scopes: ${scopeManagerStats.totalScopes}`,
`\u2022 Configured scopes: ${configuredScopeStats.totalScopes}`,
`\u2022 Observed scopes in DB: ${observedScopeStats.totalScopes}`,
`\u2022 Retrieval mode: ${retrievalConfig.mode}`,
`\u2022 FTS support: ${context.store.hasFtsSupport ? "Yes" : "No"}`,
``,
`Observed scopes by type:`,
...Object.entries(observedScopeStats.scopesByType).map(
([s, count]) => ` \u2022 ${s}: ${count}`,
),
``,
`Configured scopes by type:`,
...Object.entries(configuredScopeStats.scopesByType).map(
([s, count]) => ` \u2022 ${s}: ${count}`,
),
``,
`Memories by scope:`,
...Object.entries(stats.scopeCounts).map(
([s, count]) => ` \u2022 ${s}: ${count}`,
Expand Down Expand Up @@ -1385,7 +1397,13 @@ export function registerMemoryStatsTool(
content: [{ type: "text", text }],
details: {
stats,
scopeManagerStats,
scopeManagerStats: {
totalScopes: configuredScopeStats.totalScopes,
agentsWithCustomAccess: configuredScopeStats.agentsWithCustomAccess,
scopesByType: configuredScopeStats.scopesByType,
configured: configuredScopeStats,
observed: observedScopeStats,
},
retrievalConfig: {
...retrievalConfig,
rerankApiKey: retrievalConfig.rerankApiKey ? "***" : undefined,
Expand Down
55 changes: 53 additions & 2 deletions test/cli-smoke.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,21 @@ async function runCliSmoke() {
const context = {
store,
retriever: { retrieve: async () => [] },
scopeManager: { getDefaultScope: () => "global" },
scopeManager: {
getDefaultScope: () => "global",
getStats: () => ({
totalScopes: 1,
agentsWithCustomAccess: 0,
scopesByType: {
global: 1,
agent: 0,
custom: 0,
project: 0,
user: 0,
other: 0,
},
}),
},
migrator: {},
embedder: {
embedPassage: async () => [0, 0, 0, 0],
Expand Down Expand Up @@ -206,6 +220,13 @@ async function runCliSmoke() {
async hasId(id) {
return id === entry.id;
},
async stats() {
return {
totalCount: 1,
scopeCounts: { global: 1, "agent:main": 2 },
categoryCounts: { fact: 1 },
};
},
},
retriever: {
async retrieve() {
Expand All @@ -228,7 +249,22 @@ async function runCliSmoke() {
};
},
},
scopeManager: {},
scopeManager: {
getStats() {
return {
totalScopes: 1,
agentsWithCustomAccess: 0,
scopesByType: {
global: 1,
agent: 0,
custom: 0,
project: 0,
user: 0,
other: 0,
},
};
},
},
migrator: {},
embedder: {
async embedQuery() {
Expand Down Expand Up @@ -256,6 +292,21 @@ async function runCliSmoke() {

assert.match(searchOutput, /search_regression_1/);

const statsOutput = await captureStdout(async () => {
await searchProgram.parseAsync([
"node",
"openclaw",
"memory-pro",
"stats",
"--json",
]);
});
const statsJson = JSON.parse(statsOutput);
assert.ok(statsJson.scopes?.configured);
assert.ok(statsJson.scopes?.observed);
assert.equal(typeof statsJson.scopes.configured.totalScopes, "number");
assert.equal(typeof statsJson.scopes.observed.totalScopes, "number");

const lexicalStore = new MemoryStore({
dbPath: path.join(workDir, "lexical-db"),
vectorDim: 4,
Expand Down
33 changes: 33 additions & 0 deletions test/scope-stats-regression.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import jitiFactory from "jiti";

const jiti = jitiFactory(import.meta.url, { interopDefault: true });
const { summarizeScopesByType, createScopeManager } = jiti("../src/scopes.ts");

test("configured scopes and observed scopes are counted separately", () => {
const scopeManager = createScopeManager({
default: "global",
definitions: {
global: { description: "Shared scope" },
},
});

const configured = scopeManager.getStats();
const observed = summarizeScopesByType(["global", "agent:main", "agent:junshi"]);

assert.equal(configured.totalScopes, 1);
assert.equal(configured.scopesByType.global, 1);
assert.equal(configured.scopesByType.agent, 0);

assert.equal(observed.totalScopes, 3);
assert.equal(observed.scopesByType.global, 1);
assert.equal(observed.scopesByType.agent, 2);
});

test("observed scope summary handles empty scope list", () => {
const observed = summarizeScopesByType([]);
assert.equal(observed.totalScopes, 0);
assert.equal(observed.scopesByType.global, 0);
assert.equal(observed.scopesByType.agent, 0);
});
Loading