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.
- package/dist/agent/assistant.js +12 -0
- package/dist/cli/dashboard.js +724 -106
- package/dist/config.d.ts +11 -0
- package/dist/config.js +16 -0
- package/dist/index.js +20 -0
- package/dist/memory/chunker.js +13 -2
- package/dist/memory/hot-cache.d.ts +38 -0
- package/dist/memory/hot-cache.js +73 -0
- package/dist/memory/integrity.d.ts +28 -0
- package/dist/memory/integrity.js +119 -0
- package/dist/memory/maintenance.d.ts +23 -2
- package/dist/memory/maintenance.js +140 -3
- package/dist/memory/seed-user-model.d.ts +3 -1
- package/dist/memory/seed-user-model.js +6 -5
- package/dist/memory/store.d.ts +259 -2
- package/dist/memory/store.js +751 -21
- package/dist/memory/write-queue.d.ts +96 -0
- package/dist/memory/write-queue.js +165 -0
- package/dist/tools/memory-tools.js +38 -1
- package/dist/types.d.ts +10 -2
- package/package.json +1 -1
package/dist/memory/store.d.ts
CHANGED
|
@@ -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
|
*/
|