@vellumai/assistant 0.10.9-dev.202607161337.f69e16e → 0.10.9-dev.202607161445.5757c2f

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/Dockerfile CHANGED
@@ -129,6 +129,14 @@ COPY packages/twilio-client /app/packages/twilio-client
129
129
 
130
130
  COPY assistant /app/assistant
131
131
 
132
+ # Materialize the git-ignored offline plugin manifest from its canonical source.
133
+ # `assistant/src/cli/lib/plugin-catalog-local.ts` statically imports this JSON
134
+ # (loaded even when platform features are enabled), so it must exist in the image
135
+ # — the build installs with --ignore-scripts, so the postinstall sync that
136
+ # regenerates it in dev never runs here. Mirrors how CI materializes the
137
+ # feature-flag registry before building.
138
+ COPY plugins/marketplace.json /app/assistant/src/cli/lib/bundled-marketplace.json
139
+
132
140
  # Install assistant CLI launcher backed by the bundled assistant package
133
141
  RUN printf '#!/usr/bin/env sh\nexec bun run /app/assistant/src/index.ts "$@"\n' > /usr/local/bin/assistant && \
134
142
  chmod +x /usr/local/bin/assistant
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/assistant",
3
- "version": "0.10.9-dev.202607161337.f69e16e",
3
+ "version": "0.10.9-dev.202607161445.5757c2f",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "exports": {
@@ -81,6 +81,7 @@ const BASELINE: Record<string, readonly string[]> = {
81
81
  "src/cli/commands/memory/memory-v2-compare-render.ts",
82
82
  "src/cli/commands/memory/memory-v2.ts",
83
83
  "src/cli/commands/memory/memory-v3.ts",
84
+ "src/cli/commands/memory/nodes.ts",
84
85
  "src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts",
85
86
  "src/config/bundled-skills/playbooks/tools/playbook-create.ts",
86
87
  "src/config/bundled-skills/playbooks/tools/playbook-delete.ts",
@@ -26,6 +26,7 @@ model (section-grain lanes cached as live shadow state). Each subgroup exposes
26
26
  operator-facing maintenance verbs — reindexing, backfills, validation, and evals.
27
27
 
28
28
  Examples:
29
+ $ assistant memory nodes stats
29
30
  $ assistant memory nodes list --search "coffee"
30
31
  $ assistant memory nodes delete "User prefers TypeScript"
31
32
  $ assistant memory nodes update "User prefers TypeScript" "User prefers TypeScript and Bun"
@@ -48,11 +49,33 @@ a remembered fact without first looking up its ID.
48
49
  All subcommands require memory v2 to be enabled and the assistant to be running.
49
50
 
50
51
  Examples:
52
+ $ assistant memory nodes stats
51
53
  $ assistant memory nodes list
52
54
  $ assistant memory nodes list --search "TypeScript" --limit 20
53
55
  $ assistant memory nodes delete "User prefers TypeScript"
54
56
  $ assistant memory nodes update "User prefers TypeScript" "User prefers TypeScript and Bun"`,
55
57
  subcommands: [
58
+ {
59
+ name: "stats",
60
+ description: "Show a health overview of the memory v2 graph",
61
+ options: [
62
+ {
63
+ flags: "--json",
64
+ description: "Machine-readable compact JSON output",
65
+ },
66
+ ],
67
+ helpText: `
68
+ Behavior:
69
+ Scans all active memory graph nodes in a single pass and reports:
70
+ total count, breakdown by type and fidelity (with an ASCII bar chart),
71
+ nodes at risk of decay (significance < 15%), average significance,
72
+ oldest/newest node timestamps, last reinforcement, and the top 5 nodes
73
+ by significance. Does not require the daemon to be running.
74
+
75
+ Examples:
76
+ $ assistant memory nodes stats
77
+ $ assistant memory nodes stats --json`,
78
+ },
56
79
  {
57
80
  name: "list",
58
81
  description: "List active memory graph nodes",
@@ -203,4 +203,150 @@ export function registerMemoryNodesCommand(memory: Command): void {
203
203
  log.info(result.message);
204
204
  },
205
205
  );
206
+
207
+ // ── stats ─────────────────────────────────────────────────────────────
208
+
209
+ subcommand(nodes, "stats").action(
210
+ async (opts: { json?: boolean }, cmd: Command) => {
211
+ // Deferred: loads config and the graph stats handler in-process.
212
+ // No daemon needed — reads directly from the workspace SQLite DB.
213
+ const [{ handleStatsMemory }, { getConfig }] = await Promise.all([
214
+ import("../../../plugins/defaults/memory/graph/tool-handlers.js") as Promise<
215
+ typeof import("../../../plugins/defaults/memory/graph/tool-handlers.js")
216
+ >,
217
+ import("../../../config/loader.js") as Promise<
218
+ typeof import("../../../config/loader.js")
219
+ >,
220
+ ]);
221
+
222
+ const result = handleStatsMemory(getConfig());
223
+
224
+ if (!result.success) {
225
+ log.error(result.message);
226
+ process.exitCode = 1;
227
+ return;
228
+ }
229
+
230
+ if (opts.json) {
231
+ writeOutput(cmd, result.stats);
232
+ return;
233
+ }
234
+
235
+ printStats(result.stats!);
236
+ },
237
+ );
238
+ }
239
+
240
+ const MEMORY_TYPES = [
241
+ "episodic",
242
+ "semantic",
243
+ "procedural",
244
+ "emotional",
245
+ "prospective",
246
+ "behavioral",
247
+ "narrative",
248
+ "shared",
249
+ ] as const;
250
+
251
+ interface StatsData {
252
+ total: number;
253
+ byType: { [key: string]: number | undefined };
254
+ byFidelity: { vivid: number; clear: number; faded: number; gist: number };
255
+ atRisk: number;
256
+ edgeCount: number;
257
+ oldestCreated: number | null;
258
+ newestCreated: number | null;
259
+ lastReinforced: number | null;
260
+ avgSignificance: number;
261
+ topNodes: ReadonlyArray<{ content: string; significance: number }>;
262
+ }
263
+
264
+ function printStats(s: StatsData): void {
265
+ const n = s.total;
266
+ const e = s.edgeCount;
267
+
268
+ console.log(
269
+ `\nMemory graph ${n} node${n === 1 ? "" : "s"} · ${e} edge${e === 1 ? "" : "s"}\n`,
270
+ );
271
+
272
+ if (n === 0) {
273
+ console.log(" The memory graph is empty.");
274
+ console.log("");
275
+ return;
276
+ }
277
+
278
+ // ── by type ─────────────────────────────────────────────────────────────
279
+
280
+ console.log("BY TYPE");
281
+ for (const t of MEMORY_TYPES) {
282
+ const count = s.byType[t] ?? 0;
283
+ if (count > 0) {
284
+ console.log(` ${t.padEnd(12)} ${count}`);
285
+ }
286
+ }
287
+
288
+ // ── by fidelity (with ASCII bar) ─────────────────────────────────────────
289
+
290
+ console.log("\nBY FIDELITY");
291
+ const fidelities = ["vivid", "clear", "faded", "gist"] as const;
292
+ for (const f of fidelities) {
293
+ const count = s.byFidelity[f];
294
+ const barLen = n > 0 ? Math.round((count / n) * 20) : 0;
295
+ const bar = "█".repeat(barLen);
296
+ console.log(` ${f.padEnd(6)} ${String(count).padStart(4)} ${bar}`);
297
+ }
298
+
299
+ // ── significance ─────────────────────────────────────────────────────────
300
+
301
+ console.log("\nSIGNIFICANCE");
302
+ console.log(` average ${(s.avgSignificance * 100).toFixed(1)}%`);
303
+ if (s.atRisk > 0) {
304
+ console.log(
305
+ ` at risk ${s.atRisk} node${s.atRisk === 1 ? "" : "s"} fading (significance < 15%)`,
306
+ );
307
+ }
308
+
309
+ // ── timeline ─────────────────────────────────────────────────────────────
310
+
311
+ console.log("\nTIMELINE");
312
+ const dateOpts: Intl.DateTimeFormatOptions = {
313
+ month: "short",
314
+ day: "numeric",
315
+ year: "numeric",
316
+ };
317
+ if (s.oldestCreated) {
318
+ console.log(
319
+ ` oldest ${new Date(s.oldestCreated).toLocaleString("en-US", dateOpts)}`,
320
+ );
321
+ }
322
+ if (s.newestCreated) {
323
+ console.log(
324
+ ` newest ${new Date(s.newestCreated).toLocaleString("en-US", dateOpts)}`,
325
+ );
326
+ }
327
+ if (s.lastReinforced) {
328
+ console.log(
329
+ ` reinforced ${new Date(s.lastReinforced).toLocaleString("en-US", {
330
+ ...dateOpts,
331
+ hour: "numeric",
332
+ minute: "2-digit",
333
+ })}`,
334
+ );
335
+ }
336
+
337
+ // ── top nodes ────────────────────────────────────────────────────────────
338
+
339
+ if (s.topNodes.length > 0) {
340
+ console.log("\nTOP NODES BY SIGNIFICANCE");
341
+ const WIDTH = 60;
342
+ const trunc = (str: string) =>
343
+ str.length > WIDTH ? str.slice(0, WIDTH - 1) + "…" : str;
344
+ for (const node of s.topNodes) {
345
+ console.log(
346
+ ` ${(node.significance * 100).toFixed(0).padStart(3)}% ${trunc(node.content)}`,
347
+ );
348
+ }
349
+ }
350
+
351
+ console.log("");
206
352
  }
@@ -1,10 +1,14 @@
1
1
  /**
2
2
  * Offline plugin catalog reader backed by the manifest bundled into the
3
- * package at build time (see meta/sync-bundled-copies.ts).
3
+ * package at build time. `bundled-marketplace.json` is a generated artifact
4
+ * (git-ignored) that meta/sync-bundled-copies.ts copies from the canonical
5
+ * plugins/marketplace.json before every build, test, and install — the same
6
+ * treatment as the feature-flag registry.
4
7
  *
5
8
  * `getPluginCatalog` selects this reader only when platform features are
6
9
  * disabled (air-gapped / self-hosted, `VELLUM_DISABLE_PLATFORM=true`). Importing
7
- * the JSON directly (resolveJsonModule) means no filesystem path resolution, so
10
+ * the JSON directly (resolveJsonModule) inlines it into the bundle, so there is
11
+ * no filesystem path resolution and the manifest is always present at runtime —
8
12
  * it works identically in dev, Docker, and an npm-packed macOS install.
9
13
  */
10
14
 
@@ -883,3 +883,117 @@ export function getNodeEditHistory(
883
883
  .limit(limit)
884
884
  .all();
885
885
  }
886
+
887
+ // ---------------------------------------------------------------------------
888
+ // Graph stats
889
+ // ---------------------------------------------------------------------------
890
+
891
+ /** Fidelity counts for active (non-gone) nodes. */
892
+ export interface FidelityBreakdown {
893
+ vivid: number;
894
+ clear: number;
895
+ faded: number;
896
+ gist: number;
897
+ }
898
+
899
+ /** Aggregate health snapshot of the memory v2 graph. */
900
+ export interface GraphStats {
901
+ /** Total active (non-gone) node count. */
902
+ total: number;
903
+ /** Node count by MemoryType. */
904
+ byType: Partial<Record<MemoryType, number>>;
905
+ /** Node count by fidelity, excluding gone. */
906
+ byFidelity: FidelityBreakdown;
907
+ /** Nodes with significance < 0.15 (at risk of decaying away). */
908
+ atRisk: number;
909
+ /** Live edge count — both endpoints are non-gone. */
910
+ edgeCount: number;
911
+ /** Epoch ms of the oldest active node, or null if the graph is empty. */
912
+ oldestCreated: number | null;
913
+ /** Epoch ms of the newest active node, or null if the graph is empty. */
914
+ newestCreated: number | null;
915
+ /** Epoch ms of the most recent reinforcement event, or null if none. */
916
+ lastReinforced: number | null;
917
+ /** Mean significance across all active nodes (0–1). */
918
+ avgSignificance: number;
919
+ /** Up to 5 highest-significance nodes (content, significance, fidelity, type). */
920
+ topNodes: Array<{
921
+ content: string;
922
+ significance: number;
923
+ fidelity: string;
924
+ type: string;
925
+ }>;
926
+ }
927
+
928
+ /**
929
+ * Compute a health snapshot of the memory v2 graph in two queries:
930
+ * 1. A full scan of active nodes (significance DESC) to derive all
931
+ * per-node aggregates in one JS pass.
932
+ * 2. A SQL COUNT of live edges (both endpoints non-gone).
933
+ *
934
+ * Callers that only need the summary number can use `countNodes()` instead.
935
+ */
936
+ export function computeGraphStats(): GraphStats {
937
+ // Full scan — significance DESC so topNodes is just the first slice.
938
+ const nodes = queryNodes({ fidelityNot: ["gone"] });
939
+
940
+ const byType: Partial<Record<MemoryType, number>> = {};
941
+ const byFidelity: FidelityBreakdown = {
942
+ vivid: 0,
943
+ clear: 0,
944
+ faded: 0,
945
+ gist: 0,
946
+ };
947
+ let atRisk = 0;
948
+ let sigSum = 0;
949
+ let oldestCreated: number | null = null;
950
+ let newestCreated: number | null = null;
951
+ let lastReinforced: number | null = null;
952
+
953
+ for (const n of nodes) {
954
+ byType[n.type] = (byType[n.type] ?? 0) + 1;
955
+ if (n.fidelity !== "gone") {
956
+ byFidelity[n.fidelity as keyof FidelityBreakdown] += 1;
957
+ }
958
+ if (n.significance < 0.15) atRisk++;
959
+ sigSum += n.significance;
960
+ if (oldestCreated === null || n.created < oldestCreated)
961
+ oldestCreated = n.created;
962
+ if (newestCreated === null || n.created > newestCreated)
963
+ newestCreated = n.created;
964
+ if (lastReinforced === null || n.lastReinforced > lastReinforced)
965
+ lastReinforced = n.lastReinforced;
966
+ }
967
+
968
+ // Live edge count: both endpoints must be non-gone.
969
+ const db = getDb();
970
+ const edgeResult = db
971
+ .select({ count: sql<number>`count(*)` })
972
+ .from(memoryGraphEdges)
973
+ .where(
974
+ and(
975
+ sql`NOT EXISTS (SELECT 1 FROM ${memoryGraphNodes} WHERE ${memoryGraphNodes.id} = ${memoryGraphEdges.sourceNodeId} AND ${memoryGraphNodes.fidelity} = 'gone')`,
976
+ sql`NOT EXISTS (SELECT 1 FROM ${memoryGraphNodes} WHERE ${memoryGraphNodes.id} = ${memoryGraphEdges.targetNodeId} AND ${memoryGraphNodes.fidelity} = 'gone')`,
977
+ ),
978
+ )
979
+ .get();
980
+
981
+ return {
982
+ total: nodes.length,
983
+ byType,
984
+ byFidelity,
985
+ atRisk,
986
+ edgeCount: edgeResult?.count ?? 0,
987
+ oldestCreated,
988
+ newestCreated,
989
+ lastReinforced,
990
+ avgSignificance: nodes.length > 0 ? sigSum / nodes.length : 0,
991
+ // nodes is already sorted significance DESC — slice gives the top N.
992
+ topNodes: nodes.slice(0, 5).map((n) => ({
993
+ content: n.content,
994
+ significance: n.significance,
995
+ fidelity: n.fidelity,
996
+ type: n.type,
997
+ })),
998
+ };
999
+ }
@@ -14,7 +14,14 @@ import { enqueueMemoryJob } from "../../../../persistence/jobs-store.js";
14
14
  import { enqueuePkbIndexJob } from "../jobs/embed-pkb-file.js";
15
15
  import { getLogger } from "../logging.js";
16
16
  import { getWorkspaceDir } from "../paths.js";
17
- import { deleteNode, queryNodes, recordNodeEdit, updateNode } from "./store.js";
17
+ import type { GraphStats } from "./store.js";
18
+ import {
19
+ computeGraphStats,
20
+ deleteNode,
21
+ queryNodes,
22
+ recordNodeEdit,
23
+ updateNode,
24
+ } from "./store.js";
18
25
 
19
26
  const log = getLogger("graph-tool-handlers");
20
27
 
@@ -429,3 +436,31 @@ export function handleListMemory(
429
436
  total: filtered.length,
430
437
  };
431
438
  }
439
+
440
+ // ---------------------------------------------------------------------------
441
+ // handleStatsMemory
442
+ // ---------------------------------------------------------------------------
443
+
444
+ export interface StatsMemoryResult {
445
+ success: boolean;
446
+ message: string;
447
+ stats: GraphStats | null;
448
+ }
449
+
450
+ export function handleStatsMemory(config: AssistantConfig): StatsMemoryResult {
451
+ if (!config.memory.v2.enabled) {
452
+ return {
453
+ success: false,
454
+ message: "stats requires memory v2.",
455
+ stats: null,
456
+ };
457
+ }
458
+ const stats = computeGraphStats();
459
+ const n = stats.total;
460
+ const e = stats.edgeCount;
461
+ return {
462
+ success: true,
463
+ message: `${n} active node${n === 1 ? "" : "s"}, ${e} edge${e === 1 ? "" : "s"}.`,
464
+ stats,
465
+ };
466
+ }