clementine-agent 1.2.1 → 1.2.3

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.
@@ -10,6 +10,8 @@
10
10
  * (single-user, one MCP subprocess handles all writes).
11
11
  */
12
12
  import type { Feedback, MemoryExtraction, SearchResult, SessionSummary, SyncStats, TranscriptTurn, WikilinkConnection } from '../types.js';
13
+ import { HotCache } from './hot-cache.js';
14
+ import { type WriteQueueOpts } from './write-queue.js';
13
15
  export declare class MemoryStore {
14
16
  private dbPath;
15
17
  private vaultDir;
@@ -17,6 +19,8 @@ export declare class MemoryStore {
17
19
  private _stmtChunkCount;
18
20
  private _stmtInsertTranscript;
19
21
  private _stmtInsertUsage;
22
+ private chunkRowCache;
23
+ private writeQueue;
20
24
  constructor(dbPath: string, vaultDir: string);
21
25
  /**
22
26
  * Create the database and schema if needed.
@@ -215,6 +219,25 @@ export declare class MemoryStore {
215
219
  category?: string;
216
220
  topic?: string;
217
221
  }, strict?: boolean): SearchResult[];
222
+ /**
223
+ * 1-hop wikilink expansion: for each seed chunk's source_file, find files
224
+ * that link to it or that it links to, and pull their top chunks. Returns
225
+ * SearchResult-shaped rows with a fractional boost so they enter the
226
+ * candidate pool below the seed scores but above pure noise.
227
+ *
228
+ * Pattern: 2026-frontier agent memory uses graph expansion (Mem0g, Zep
229
+ * Graphiti) to surface chunks that share an entity but miss the lexical
230
+ * match. Wikilinks are the cheapest available edge — Clementine already
231
+ * extracts them on every vault sync. The richer FalkorDB graph adds
232
+ * temporal validity and entity types but isn't required for this lift.
233
+ */
234
+ expandViaWikilinks(seeds: SearchResult[], opts?: {
235
+ boost?: number;
236
+ limitPerFile?: number;
237
+ maxNeighbors?: number;
238
+ agentSlug?: string;
239
+ strict?: boolean;
240
+ }): SearchResult[];
218
241
  /**
219
242
  * Combined FTS5 relevance + recency search for context injection.
220
243
  *
@@ -223,6 +246,7 @@ export declare class MemoryStore {
223
246
  * 2. Recency fetch -> N most recent chunks
224
247
  * 3. Deduplicate by (source_file, section)
225
248
  * 4. Apply salience boost to FTS results
249
+ * 5. Wikilink graph expansion -> 1-hop neighbors of top seeds (boost 0.7×)
226
250
  */
227
251
  searchContext(query: string, limitOrOpts?: number | {
228
252
  limit?: number;
@@ -252,6 +276,15 @@ export declare class MemoryStore {
252
276
  scores: number[];
253
277
  agentSlug?: string | null;
254
278
  }): void;
279
+ /** Internal sync recall_trace insert. Called by the WriteQueue. */
280
+ _logRecallTraceSync(opts: {
281
+ sessionKey: string;
282
+ messageId?: string | null;
283
+ query: string;
284
+ chunkIds: number[];
285
+ scores: number[];
286
+ agentSlug?: string | null;
287
+ }): void;
255
288
  /**
256
289
  * Fetch recent recall traces for a session, newest first.
257
290
  * Used by the dashboard chat panel to show "what memory powered this answer".
@@ -303,6 +336,25 @@ export declare class MemoryStore {
303
336
  salience: number;
304
337
  updatedAt: string;
305
338
  }>;
339
+ /** Cache stats for the dashboard / debugging. */
340
+ getChunkCacheStats(): ReturnType<HotCache<number, unknown>['stats']>;
341
+ /**
342
+ * Enable the write-behind queue. After this call, saveTurn / recordAccess /
343
+ * recordOutcome / logRecallTrace enqueue instead of running SQL on the
344
+ * caller's thread. Idempotent. Tests leave this off and rely on the sync path.
345
+ */
346
+ enableWriteQueue(opts?: WriteQueueOpts): void;
347
+ /** Drain and stop the write queue. Call on graceful shutdown. */
348
+ flushWrites(): Promise<void>;
349
+ /** Stats for the dashboard / debugging. Returns null when queue disabled. */
350
+ getWriteQueueStats(): {
351
+ size: number;
352
+ dropped: number;
353
+ } | null;
354
+ /** Drop a single cache entry — called from mutations that touch a chunk. */
355
+ invalidateChunkCache(chunkId: number): void;
356
+ /** Drop the whole cache — fullSync and similar bulk operations call this. */
357
+ clearChunkCache(): void;
306
358
  private _parseJsonArray;
307
359
  /** The fixed slot vocabulary. Adding new slots is a code change so the
308
360
  * agent doesn't sprawl into ad-hoc namespaces. */
@@ -366,6 +418,19 @@ export declare class MemoryStore {
366
418
  * async, which propagates up the entire searchContext chain.
367
419
  */
368
420
  private searchByDenseEmbedding;
421
+ /**
422
+ * Pre-embed the top N most-cited chunks at startup. Eliminates cold-start
423
+ * latency for the chunks the agent is most likely to retrieve next. Skips
424
+ * chunks that already have a current-model dense embedding.
425
+ *
426
+ * Ranking: by outcome citation count in the last 30d (chunks the agent
427
+ * actually used), tiebroken by recency. Soft-deleted excluded.
428
+ */
429
+ warmDenseEmbeddings(topN?: number): Promise<{
430
+ warmed: number;
431
+ skipped: number;
432
+ failed: number;
433
+ }>;
369
434
  /**
370
435
  * Backfill dense embeddings on chunks that don't yet have one (or that
371
436
  * were embedded by an older model). Async because the dense model itself
@@ -396,9 +461,12 @@ export declare class MemoryStore {
396
461
  */
397
462
  getConnections(noteName: string): WikilinkConnection[];
398
463
  /**
399
- * Save a conversation turn to the transcripts table.
464
+ * Save a conversation turn to the transcripts table. Routes through the
465
+ * write queue when enabled so the request thread doesn't block on SQL.
400
466
  */
401
467
  saveTurn(sessionKey: string, role: string, content: string, model?: string): void;
468
+ /** Internal sync transcript insert. Called directly by the WriteQueue. */
469
+ _saveTurnSync(sessionKey: string, role: string, content: string, model: string): void;
402
470
  /**
403
471
  * Get all turns for a given session, ordered chronologically.
404
472
  */
@@ -426,9 +494,12 @@ export declare class MemoryStore {
426
494
  */
427
495
  getRecentSummaries(limit?: number): SessionSummary[];
428
496
  /**
429
- * Record that chunks were accessed (retrieved/displayed).
497
+ * Record that chunks were accessed (retrieved/displayed). Routes through
498
+ * the write queue when enabled.
430
499
  */
431
500
  recordAccess(chunkIds: number[], accessType?: string): void;
501
+ /** Internal sync access log insert. Called directly by the WriteQueue. */
502
+ _recordAccessSync(chunkIds: number[], accessType: string): void;
432
503
  /**
433
504
  * Recompute salience score for a chunk based on access patterns.
434
505
  *
@@ -451,6 +522,11 @@ export declare class MemoryStore {
451
522
  chunkId: number;
452
523
  referenced: boolean;
453
524
  }>, sessionKey?: string | null): void;
525
+ /** Internal sync outcome insert + EMA update. Called by the WriteQueue. */
526
+ _recordOutcomeSync(outcomes: Array<{
527
+ chunkId: number;
528
+ referenced: boolean;
529
+ }>, sessionKey?: string | null): void;
454
530
  /**
455
531
  * Idempotent append for a batch of SDK session transcript entries.
456
532
  * Entries with a uuid are upserted on (session_id, subpath, uuid);
@@ -663,6 +739,127 @@ export declare class MemoryStore {
663
739
  usageLogPruned: number;
664
740
  recallTracesPruned: number;
665
741
  };
742
+ /**
743
+ * User-model slots whose `updated_at` is older than maxAgeDays. These are
744
+ * candidates for the "verify or refresh" nudge — high-relevance memories
745
+ * that may have become silently wrong (Mem0 2026 calls this out as an
746
+ * open problem; we surface it via observability rather than auto-decay).
747
+ *
748
+ * Empty content is skipped (an empty slot has no claim to verify).
749
+ */
750
+ findStaleUserModelSlots(opts?: {
751
+ maxAgeDays?: number;
752
+ agentSlug?: string | null;
753
+ }): Array<{
754
+ slot: string;
755
+ ageDays: number;
756
+ agentSlug: string | null;
757
+ }>;
758
+ /**
759
+ * High-salience chunks whose outcome EMA has drifted negative — i.e., we
760
+ * keep ranking them high but the agent stopped citing them. Strong signal
761
+ * that the chunk is stale or wrong even though salience hasn't decayed.
762
+ *
763
+ * Conservative threshold: salience > 0.8 AND last_outcome_score < 0.
764
+ * Soft-deleted excluded.
765
+ */
766
+ findStaleHighSalienceChunks(opts?: {
767
+ salienceFloor?: number;
768
+ outcomeCeiling?: number;
769
+ limit?: number;
770
+ }): Array<{
771
+ chunkId: number;
772
+ sourceFile: string;
773
+ section: string;
774
+ salience: number;
775
+ lastOutcomeScore: number;
776
+ }>;
777
+ /**
778
+ * Format staleness findings into ready-to-inject prompt text. Heartbeat
779
+ * builders can drop this into the system prompt verbatim. Returns null
780
+ * if there's nothing to nudge about — caller should not inject empty text.
781
+ */
782
+ getStalenessNudges(opts?: {
783
+ agentSlug?: string | null;
784
+ maxSlotAgeDays?: number;
785
+ }): string | null;
786
+ /**
787
+ * Find procedure chunks whose frontmatter `triggers` overlap with words
788
+ * in the query. Used to surface learned workflows ("how Nate ships a
789
+ * release", "how to handle inbound replies") above generic facts when
790
+ * the user's intent matches.
791
+ *
792
+ * Match rule: case-insensitive substring of any trigger phrase appears
793
+ * in the query. Empty result if no procedure chunks exist or no triggers
794
+ * match — caller should treat this as additive context, not the whole
795
+ * answer.
796
+ */
797
+ findRelevantProcedures(query: string, opts?: {
798
+ limit?: number;
799
+ agentSlug?: string | null;
800
+ }): Array<{
801
+ id: number;
802
+ sourceFile: string;
803
+ section: string;
804
+ content: string;
805
+ triggers: string[];
806
+ matched: string[];
807
+ }>;
808
+ /** Persistent key/value for janitor state (last vacuum, etc.). */
809
+ getMaintenanceMeta(key: string): string | null;
810
+ setMaintenanceMeta(key: string, value: string): void;
811
+ /**
812
+ * Two-phase delete for consolidated, low-salience, unused chunks.
813
+ *
814
+ * Phase 1: soft-delete chunks where consolidated=1, not pinned, salience
815
+ * below floor, and never accessed (or last access older than
816
+ * expireDays).
817
+ * Phase 2: physically delete chunks that have been in chunk_soft_deletes
818
+ * for graceDays. Cascades to access_log, outcomes, chunk_history
819
+ * for the same chunk_id.
820
+ *
821
+ * Summary chunks whose `derived_from` references the deleted IDs are
822
+ * intentionally NOT propagate-deleted — the summary still encodes signal.
823
+ */
824
+ expireConsolidated(opts?: {
825
+ expireDays?: number;
826
+ salienceFloor?: number;
827
+ graceDays?: number;
828
+ }): {
829
+ softDeleted: number;
830
+ physicallyDeleted: number;
831
+ };
832
+ /** Trim outcomes table to a rolling window. Append-only, can grow fast. */
833
+ pruneOutcomes(retentionDays?: number): number;
834
+ /**
835
+ * Cap memory_extractions to maxRows. Deletes oldest non-active rows first;
836
+ * 'active' extractions are preserved regardless of count to protect the
837
+ * audit trail for in-flight work.
838
+ */
839
+ capExtractions(maxRows?: number): number;
840
+ /** Approximate SQLite database file size on disk, in bytes. */
841
+ dbSizeBytes(): number;
842
+ /**
843
+ * VACUUM the database. Reclaims space from deleted rows. Holds an
844
+ * exclusive lock for the duration — caller is expected to gate on
845
+ * idleness (see lastActivityAt).
846
+ */
847
+ vacuum(): {
848
+ sizeBeforeBytes: number;
849
+ sizeAfterBytes: number;
850
+ durationMs: number;
851
+ };
852
+ /**
853
+ * Most recent timestamp across the high-write activity tables, as a Unix
854
+ * milliseconds value. Returns null if all tables are empty. Used by the
855
+ * janitor's idle gate.
856
+ *
857
+ * Implementation note: SQLite's datetime() returns "YYYY-MM-DD HH:MM:SS"
858
+ * in UTC with no timezone marker — JS Date.parse interprets that as local
859
+ * time and skews by the offset. We compute the unix epoch in SQL to avoid
860
+ * the bug entirely.
861
+ */
862
+ lastActivityAt(): number | null;
666
863
  /**
667
864
  * Get chunks within a date range, ordered chronologically.
668
865
  * Useful for "what happened last week" type queries.
@@ -904,6 +1101,66 @@ export declare class MemoryStore {
904
1101
  * Reduces salience so they appear lower in search results (but aren't deleted).
905
1102
  */
906
1103
  markConsolidated(chunkIds: number[]): void;
1104
+ /**
1105
+ * Aggregate memory-health snapshot for the dashboard.
1106
+ *
1107
+ * Single-pass queries over each table; cheap enough to call on every
1108
+ * dashboard tab visit without caching. Adds graph stats only if a
1109
+ * graphStore is supplied and reachable.
1110
+ */
1111
+ getMemoryHealth(opts?: {
1112
+ graphStore?: {
1113
+ isAvailable(): boolean;
1114
+ nodeCount?(): Promise<number>;
1115
+ edgeCount?(): Promise<number>;
1116
+ };
1117
+ topCitedLimit?: number;
1118
+ }): {
1119
+ chunks: {
1120
+ total: number;
1121
+ consolidated: number;
1122
+ pinned: number;
1123
+ softDeleted: number;
1124
+ zombieCount: number;
1125
+ };
1126
+ chunksByCategory: Array<{
1127
+ category: string | null;
1128
+ count: number;
1129
+ }>;
1130
+ tableRowCounts: Record<string, number>;
1131
+ topCitedLast30d: Array<{
1132
+ chunkId: number;
1133
+ sourceFile: string;
1134
+ section: string;
1135
+ refCount: number;
1136
+ }>;
1137
+ staleUserModelSlots: Array<{
1138
+ slot: string;
1139
+ ageDays: number;
1140
+ agentSlug: string | null;
1141
+ }>;
1142
+ staleHighSalienceChunks: Array<{
1143
+ chunkId: number;
1144
+ sourceFile: string;
1145
+ section: string;
1146
+ salience: number;
1147
+ lastOutcomeScore: number;
1148
+ }>;
1149
+ chunkCacheStats: ReturnType<HotCache<number, unknown>['stats']>;
1150
+ writeQueue: {
1151
+ size: number;
1152
+ dropped: number;
1153
+ } | null;
1154
+ lastIntegrityReport: {
1155
+ ftsOk: boolean;
1156
+ ftsRebuilt: boolean;
1157
+ orphanRefsNulled: number;
1158
+ missingEmbeddings: number;
1159
+ ranAt: string;
1160
+ } | null;
1161
+ dbSizeBytes: number;
1162
+ lastVacuumAt: string | null;
1163
+ };
907
1164
  /**
908
1165
  * Get consolidation stats for monitoring.
909
1166
  */