@cleocode/core 2026.4.36 → 2026.4.38

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.
Files changed (62) hide show
  1. package/dist/hooks/handlers/task-hooks.d.ts.map +1 -1
  2. package/dist/hooks/handlers/task-hooks.js +11 -0
  3. package/dist/hooks/handlers/task-hooks.js.map +1 -1
  4. package/dist/index.js +647 -34
  5. package/dist/index.js.map +4 -4
  6. package/dist/internal.d.ts +3 -1
  7. package/dist/internal.d.ts.map +1 -1
  8. package/dist/internal.js +3 -1
  9. package/dist/internal.js.map +1 -1
  10. package/dist/memory/decisions.d.ts.map +1 -1
  11. package/dist/memory/decisions.js +18 -0
  12. package/dist/memory/decisions.js.map +1 -1
  13. package/dist/memory/engine-compat.d.ts +17 -0
  14. package/dist/memory/engine-compat.d.ts.map +1 -1
  15. package/dist/memory/engine-compat.js +36 -0
  16. package/dist/memory/engine-compat.js.map +1 -1
  17. package/dist/memory/graph-memory-bridge.d.ts +158 -0
  18. package/dist/memory/graph-memory-bridge.d.ts.map +1 -0
  19. package/dist/memory/graph-memory-bridge.js +519 -0
  20. package/dist/memory/graph-memory-bridge.js.map +1 -0
  21. package/dist/memory/index.d.ts +1 -0
  22. package/dist/memory/index.d.ts.map +1 -1
  23. package/dist/memory/index.js +2 -0
  24. package/dist/memory/index.js.map +1 -1
  25. package/dist/memory/learnings.d.ts.map +1 -1
  26. package/dist/memory/learnings.js +18 -0
  27. package/dist/memory/learnings.js.map +1 -1
  28. package/dist/memory/llm-extraction.d.ts.map +1 -1
  29. package/dist/memory/llm-extraction.js.map +1 -1
  30. package/dist/memory/patterns.d.ts.map +1 -1
  31. package/dist/memory/patterns.js +18 -0
  32. package/dist/memory/patterns.js.map +1 -1
  33. package/dist/memory/quality-feedback.d.ts +129 -0
  34. package/dist/memory/quality-feedback.d.ts.map +1 -0
  35. package/dist/memory/quality-feedback.js +449 -0
  36. package/dist/memory/quality-feedback.js.map +1 -0
  37. package/dist/memory/sleep-consolidation.d.ts +98 -0
  38. package/dist/memory/sleep-consolidation.d.ts.map +1 -0
  39. package/dist/memory/sleep-consolidation.js +706 -0
  40. package/dist/memory/sleep-consolidation.js.map +1 -0
  41. package/dist/memory/temporal-supersession.d.ts +155 -0
  42. package/dist/memory/temporal-supersession.d.ts.map +1 -0
  43. package/dist/memory/temporal-supersession.js +406 -0
  44. package/dist/memory/temporal-supersession.js.map +1 -0
  45. package/package.json +6 -6
  46. package/src/hooks/handlers/task-hooks.ts +11 -0
  47. package/src/internal.ts +12 -0
  48. package/src/memory/__tests__/graph-memory-bridge.test.ts +357 -0
  49. package/src/memory/__tests__/llm-extraction.test.ts +17 -0
  50. package/src/memory/__tests__/quality-feedback.test.ts +418 -0
  51. package/src/memory/__tests__/sleep-consolidation.test.ts +790 -0
  52. package/src/memory/__tests__/temporal-supersession.test.ts +534 -0
  53. package/src/memory/decisions.ts +24 -0
  54. package/src/memory/engine-compat.ts +37 -0
  55. package/src/memory/graph-memory-bridge.ts +751 -0
  56. package/src/memory/index.ts +2 -0
  57. package/src/memory/learnings.ts +24 -0
  58. package/src/memory/patterns.ts +24 -0
  59. package/src/memory/quality-feedback.ts +640 -0
  60. package/src/memory/sleep-consolidation.ts +932 -0
  61. package/src/memory/temporal-supersession.ts +568 -0
  62. package/src/store/__tests__/performance-safety.test.ts +4 -4
@@ -69,6 +69,17 @@ export async function handleToolComplete(
69
69
  }
70
70
  });
71
71
 
72
+ // T555: Correlate retrieval outcomes against this task completion.
73
+ // Fire-and-forget: quality score adjustments must never block the response.
74
+ setImmediate(async () => {
75
+ try {
76
+ const { correlateOutcomes } = await import('../../memory/quality-feedback.js');
77
+ await correlateOutcomes(projectRoot);
78
+ } catch {
79
+ // Quality correlation errors must never surface to the task complete flow
80
+ }
81
+ });
82
+
72
83
  // T138: Refresh memory bridge after task completes (best-effort)
73
84
  await maybeRefreshMemoryBridge(projectRoot);
74
85
  }
package/src/internal.ts CHANGED
@@ -218,6 +218,7 @@ export {
218
218
  memoryPatternFind,
219
219
  memoryPatternStats,
220
220
  memoryPatternStore,
221
+ memoryQualityReport,
221
222
  memoryReasonSimilar,
222
223
  memoryReasonWhy,
223
224
  memorySearchHybrid,
@@ -244,6 +245,17 @@ export {
244
245
  pipelineManifestStats,
245
246
  readManifestEntries,
246
247
  } from './memory/pipeline-manifest-sqlite.js';
248
+ export type {
249
+ CorrelateOutcomesResult,
250
+ MemoryOutcome,
251
+ MemoryQualityReport,
252
+ } from './memory/quality-feedback.js';
253
+ // Memory — quality feedback loop (T555)
254
+ export {
255
+ correlateOutcomes,
256
+ getMemoryQualityReport,
257
+ trackMemoryUsage,
258
+ } from './memory/quality-feedback.js';
247
259
  // Metrics
248
260
  export {
249
261
  autoRecordDispatchTokenUsage,
@@ -0,0 +1,357 @@
1
+ /**
2
+ * Tests for graph-memory-bridge.ts
3
+ *
4
+ * Validates the four public functions:
5
+ * - linkMemoryToCode (manual edge creation)
6
+ * - autoLinkMemories (entity extraction + auto-linking)
7
+ * - queryMemoriesForCode (traverse code→memory edges)
8
+ * - queryCodeForMemory (traverse memory→code edges)
9
+ * - listCodeLinks (list all code_reference edges)
10
+ */
11
+
12
+ import { mkdir, mkdtemp, rm } from 'node:fs/promises';
13
+ import { tmpdir } from 'node:os';
14
+ import { join } from 'node:path';
15
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
16
+
17
+ let tempDir: string;
18
+
19
+ describe('graph-memory-bridge', () => {
20
+ beforeEach(async () => {
21
+ tempDir = await mkdtemp(join(tmpdir(), 'cleo-gmb-'));
22
+ await mkdir(join(tempDir, '.cleo'), { recursive: true });
23
+ process.env['CLEO_DIR'] = join(tempDir, '.cleo');
24
+ });
25
+
26
+ afterEach(async () => {
27
+ const { closeBrainDb } = await import('../../store/brain-sqlite.js');
28
+ const { resetNexusDbState } = await import('../../store/nexus-sqlite.js');
29
+ closeBrainDb();
30
+ resetNexusDbState();
31
+ delete process.env['CLEO_DIR'];
32
+ await rm(tempDir, { recursive: true, force: true });
33
+ });
34
+
35
+ // ---------------------------------------------------------------------------
36
+ // Helpers
37
+ // ---------------------------------------------------------------------------
38
+
39
+ /** Seed a brain page node directly. */
40
+ async function seedBrainNode(
41
+ id: string,
42
+ label: string,
43
+ nodeType = 'observation',
44
+ qualityScore = 0.7,
45
+ ): Promise<void> {
46
+ const { getBrainDb } = await import('../../store/brain-sqlite.js');
47
+ const { brainPageNodes } = await import('../../store/brain-schema.js');
48
+ const db = await getBrainDb(tempDir);
49
+ const now = new Date().toISOString().replace('T', ' ').slice(0, 19);
50
+ await db
51
+ .insert(brainPageNodes)
52
+ .values({
53
+ id,
54
+ nodeType: nodeType as import('../../store/brain-schema.js').BrainNodeType,
55
+ label,
56
+ qualityScore,
57
+ contentHash: null,
58
+ lastActivityAt: now,
59
+ createdAt: now,
60
+ updatedAt: now,
61
+ })
62
+ .onConflictDoNothing();
63
+ }
64
+
65
+ /** Seed a nexus node directly. */
66
+ async function seedNexusNode(
67
+ id: string,
68
+ label: string,
69
+ name: string | null,
70
+ filePath: string | null,
71
+ kind = 'function',
72
+ ): Promise<void> {
73
+ const { getNexusDb } = await import('../../store/nexus-sqlite.js');
74
+ const { nexusNodes } = await import('../../store/nexus-schema.js');
75
+
76
+ const db = await getNexusDb();
77
+ await db
78
+ .insert(nexusNodes)
79
+ .values({
80
+ id,
81
+ projectId: 'test-project',
82
+ kind: kind as import('../../store/nexus-schema.js').NexusNodeKind,
83
+ label,
84
+ name,
85
+ filePath,
86
+ })
87
+ .onConflictDoNothing();
88
+ }
89
+
90
+ // ---------------------------------------------------------------------------
91
+ // linkMemoryToCode
92
+ // ---------------------------------------------------------------------------
93
+
94
+ describe('linkMemoryToCode', () => {
95
+ it('returns false when nexus node does not exist', async () => {
96
+ const { linkMemoryToCode } = await import('../graph-memory-bridge.js');
97
+ const result = await linkMemoryToCode(tempDir, 'observation:O-test', 'nonexistent::symbol');
98
+ expect(result).toBe(false);
99
+ });
100
+
101
+ it('creates a code_reference edge when both nodes exist', async () => {
102
+ const { linkMemoryToCode } = await import('../graph-memory-bridge.js');
103
+ const { getBrainNativeDb } = await import('../../store/brain-sqlite.js');
104
+
105
+ await seedBrainNode('observation:O-abc', 'Test observation');
106
+ await seedNexusNode(
107
+ 'src/store/brain-sqlite.ts::getBrainDb',
108
+ 'getBrainDb',
109
+ 'getBrainDb',
110
+ 'src/store/brain-sqlite.ts',
111
+ );
112
+
113
+ const result = await linkMemoryToCode(
114
+ tempDir,
115
+ 'observation:O-abc',
116
+ 'src/store/brain-sqlite.ts::getBrainDb',
117
+ );
118
+
119
+ expect(result).toBe(true);
120
+
121
+ // Verify the edge was written to brain.db
122
+ const brainNative = getBrainNativeDb();
123
+ const edge = brainNative
124
+ ?.prepare(
125
+ `SELECT from_id, to_id, edge_type, weight FROM brain_page_edges
126
+ WHERE from_id = ? AND to_id = ? AND edge_type = 'code_reference'`,
127
+ )
128
+ .get('observation:O-abc', 'src/store/brain-sqlite.ts::getBrainDb') as
129
+ | { from_id: string; to_id: string; edge_type: string; weight: number }
130
+ | undefined;
131
+
132
+ expect(edge).toBeDefined();
133
+ expect(edge?.edge_type).toBe('code_reference');
134
+ expect(edge?.weight).toBe(1.0);
135
+ });
136
+
137
+ it('is idempotent — calling twice does not duplicate the edge', async () => {
138
+ const { linkMemoryToCode } = await import('../graph-memory-bridge.js');
139
+ const { getBrainNativeDb } = await import('../../store/brain-sqlite.js');
140
+
141
+ await seedBrainNode('observation:O-dup', 'Dup test');
142
+ await seedNexusNode('src/file.ts::myFunc', 'myFunc', 'myFunc', 'src/file.ts');
143
+
144
+ await linkMemoryToCode(tempDir, 'observation:O-dup', 'src/file.ts::myFunc');
145
+ await linkMemoryToCode(tempDir, 'observation:O-dup', 'src/file.ts::myFunc');
146
+
147
+ const brainNative = getBrainNativeDb();
148
+ const rows = brainNative
149
+ ?.prepare(
150
+ `SELECT COUNT(*) as cnt FROM brain_page_edges
151
+ WHERE from_id = 'observation:O-dup'
152
+ AND to_id = 'src/file.ts::myFunc'
153
+ AND edge_type = 'code_reference'`,
154
+ )
155
+ .get() as { cnt: number } | undefined;
156
+
157
+ expect(rows?.cnt).toBe(1);
158
+ });
159
+ });
160
+
161
+ // ---------------------------------------------------------------------------
162
+ // autoLinkMemories
163
+ // ---------------------------------------------------------------------------
164
+
165
+ describe('autoLinkMemories', () => {
166
+ it('returns zero counts when brain and nexus are empty', async () => {
167
+ const { autoLinkMemories } = await import('../graph-memory-bridge.js');
168
+ const result = await autoLinkMemories(tempDir);
169
+ expect(result.scanned).toBe(0);
170
+ expect(result.linked).toBe(0);
171
+ expect(result.links).toHaveLength(0);
172
+ });
173
+
174
+ it('matches brain nodes to nexus by exact symbol name', async () => {
175
+ const { autoLinkMemories } = await import('../graph-memory-bridge.js');
176
+
177
+ // Brain node label contains the symbol name
178
+ await seedBrainNode(
179
+ 'observation:O-sym1',
180
+ 'Used getBrainDb to initialize the database connection',
181
+ 'observation',
182
+ 0.8,
183
+ );
184
+
185
+ await seedNexusNode(
186
+ 'src/store/brain-sqlite.ts::getBrainDb',
187
+ 'getBrainDb',
188
+ 'getBrainDb',
189
+ 'src/store/brain-sqlite.ts',
190
+ 'function',
191
+ );
192
+
193
+ const result = await autoLinkMemories(tempDir);
194
+
195
+ expect(result.scanned).toBeGreaterThan(0);
196
+ expect(result.linked).toBeGreaterThanOrEqual(1);
197
+
198
+ const link = result.links.find(
199
+ (l) => l.brainNodeId === 'observation:O-sym1' && l.nexusNodeId.includes('getBrainDb'),
200
+ );
201
+ expect(link).toBeDefined();
202
+ expect(['exact-symbol', 'fuzzy-symbol']).toContain(link?.matchStrategy);
203
+ });
204
+
205
+ it('matches brain nodes to nexus by file path in label', async () => {
206
+ const { autoLinkMemories } = await import('../graph-memory-bridge.js');
207
+
208
+ await seedBrainNode(
209
+ 'decision:D-fp1',
210
+ 'Modified src/store/brain-sqlite.ts to add WAL mode support',
211
+ 'decision',
212
+ 0.9,
213
+ );
214
+
215
+ await seedNexusNode(
216
+ 'src/store/brain-sqlite.ts',
217
+ 'brain-sqlite.ts',
218
+ null,
219
+ 'src/store/brain-sqlite.ts',
220
+ 'file',
221
+ );
222
+
223
+ const result = await autoLinkMemories(tempDir);
224
+
225
+ expect(result.linked).toBeGreaterThanOrEqual(1);
226
+ const link = result.links.find(
227
+ (l) => l.brainNodeId === 'decision:D-fp1' && l.nexusNodeId === 'src/store/brain-sqlite.ts',
228
+ );
229
+ expect(link).toBeDefined();
230
+ expect(link?.matchStrategy).toBe('exact-file');
231
+ });
232
+
233
+ it('marks already-linked edges as alreadyLinked', async () => {
234
+ const { autoLinkMemories, linkMemoryToCode } = await import('../graph-memory-bridge.js');
235
+
236
+ await seedBrainNode('observation:O-pre', 'calls getBrainDb directly', 'observation', 0.8);
237
+ await seedNexusNode(
238
+ 'src/store/brain-sqlite.ts::getBrainDb',
239
+ 'getBrainDb',
240
+ 'getBrainDb',
241
+ 'src/store/brain-sqlite.ts',
242
+ );
243
+
244
+ // Pre-link manually
245
+ await linkMemoryToCode(tempDir, 'observation:O-pre', 'src/store/brain-sqlite.ts::getBrainDb');
246
+
247
+ // Auto-link should count it as already linked
248
+ const result = await autoLinkMemories(tempDir);
249
+ expect(result.alreadyLinked).toBeGreaterThanOrEqual(1);
250
+ });
251
+ });
252
+
253
+ // ---------------------------------------------------------------------------
254
+ // queryMemoriesForCode
255
+ // ---------------------------------------------------------------------------
256
+
257
+ describe('queryMemoriesForCode', () => {
258
+ it('returns empty memories for unknown symbol', async () => {
259
+ const { queryMemoriesForCode } = await import('../graph-memory-bridge.js');
260
+ const result = await queryMemoriesForCode(tempDir, 'unknown::symbol');
261
+ expect(result.nexusNodeId).toBe('unknown::symbol');
262
+ expect(result.memories).toHaveLength(0);
263
+ });
264
+
265
+ it('returns memories linked to a given nexus node', async () => {
266
+ const { linkMemoryToCode, queryMemoriesForCode } = await import('../graph-memory-bridge.js');
267
+
268
+ await seedBrainNode('observation:O-q1', 'Query test observation');
269
+ await seedNexusNode('src/file.ts::myFunc', 'myFunc', 'myFunc', 'src/file.ts');
270
+
271
+ await linkMemoryToCode(tempDir, 'observation:O-q1', 'src/file.ts::myFunc');
272
+
273
+ const result = await queryMemoriesForCode(tempDir, 'src/file.ts::myFunc');
274
+ expect(result.memories).toHaveLength(1);
275
+ expect(result.memories[0]?.nodeId).toBe('observation:O-q1');
276
+ expect(result.memories[0]?.edgeWeight).toBe(1.0);
277
+ });
278
+ });
279
+
280
+ // ---------------------------------------------------------------------------
281
+ // queryCodeForMemory
282
+ // ---------------------------------------------------------------------------
283
+
284
+ describe('queryCodeForMemory', () => {
285
+ it('returns empty codeNodes for unknown memory ID', async () => {
286
+ const { queryCodeForMemory } = await import('../graph-memory-bridge.js');
287
+ const result = await queryCodeForMemory(tempDir, 'observation:O-unknown');
288
+ expect(result.brainNodeId).toBe('observation:O-unknown');
289
+ expect(result.codeNodes).toHaveLength(0);
290
+ });
291
+
292
+ it('returns code nodes linked from a given memory node', async () => {
293
+ const { linkMemoryToCode, queryCodeForMemory } = await import('../graph-memory-bridge.js');
294
+
295
+ await seedBrainNode('decision:D-c1', 'Code for memory test');
296
+ await seedNexusNode(
297
+ 'src/store/schema.ts::brainDecisions',
298
+ 'brainDecisions',
299
+ 'brainDecisions',
300
+ 'src/store/schema.ts',
301
+ );
302
+
303
+ await linkMemoryToCode(tempDir, 'decision:D-c1', 'src/store/schema.ts::brainDecisions');
304
+
305
+ const result = await queryCodeForMemory(tempDir, 'decision:D-c1');
306
+ expect(result.codeNodes).toHaveLength(1);
307
+ expect(result.codeNodes[0]?.nexusNodeId).toBe('src/store/schema.ts::brainDecisions');
308
+ expect(result.codeNodes[0]?.kind).toBe('function');
309
+ expect(result.codeNodes[0]?.edgeWeight).toBe(1.0);
310
+ });
311
+ });
312
+
313
+ // ---------------------------------------------------------------------------
314
+ // listCodeLinks
315
+ // ---------------------------------------------------------------------------
316
+
317
+ describe('listCodeLinks', () => {
318
+ it('returns empty array when no code_reference edges exist', async () => {
319
+ const { listCodeLinks } = await import('../graph-memory-bridge.js');
320
+ const result = await listCodeLinks(tempDir);
321
+ expect(result).toHaveLength(0);
322
+ });
323
+
324
+ it('returns all code_reference edges with enriched metadata', async () => {
325
+ const { linkMemoryToCode, listCodeLinks } = await import('../graph-memory-bridge.js');
326
+
327
+ await seedBrainNode('observation:O-list1', 'List test obs 1');
328
+ await seedBrainNode('observation:O-list2', 'List test obs 2');
329
+ await seedNexusNode('src/a.ts::funcA', 'funcA', 'funcA', 'src/a.ts');
330
+ await seedNexusNode('src/b.ts::funcB', 'funcB', 'funcB', 'src/b.ts');
331
+
332
+ await linkMemoryToCode(tempDir, 'observation:O-list1', 'src/a.ts::funcA');
333
+ await linkMemoryToCode(tempDir, 'observation:O-list2', 'src/b.ts::funcB');
334
+
335
+ const result = await listCodeLinks(tempDir);
336
+ expect(result).toHaveLength(2);
337
+
338
+ const link1 = result.find((l) => l.brainNodeId === 'observation:O-list1');
339
+ expect(link1?.nexusNodeLabel).toBe('funcA');
340
+ expect(link1?.filePath).toBe('src/a.ts');
341
+ expect(link1?.weight).toBe(1.0);
342
+ });
343
+
344
+ it('respects the limit parameter', async () => {
345
+ const { linkMemoryToCode, listCodeLinks } = await import('../graph-memory-bridge.js');
346
+
347
+ for (let i = 0; i < 5; i++) {
348
+ await seedBrainNode(`observation:O-lim${i}`, `Limit test ${i}`);
349
+ await seedNexusNode(`src/x${i}.ts::fn${i}`, `fn${i}`, `fn${i}`, `src/x${i}.ts`);
350
+ await linkMemoryToCode(tempDir, `observation:O-lim${i}`, `src/x${i}.ts::fn${i}`);
351
+ }
352
+
353
+ const result = await listCodeLinks(tempDir, 3);
354
+ expect(result).toHaveLength(3);
355
+ });
356
+ });
357
+ });
@@ -43,6 +43,15 @@ vi.mock('@anthropic-ai/sdk/helpers/zod', () => ({
43
43
  zodOutputFormat: vi.fn().mockReturnValue({ _mock: 'zodOutputFormat' }),
44
44
  }));
45
45
 
46
+ // Mock the key resolver so tests don't depend on filesystem state
47
+ // (~/.claude/.credentials.json, ~/.local/share/cleo/anthropic-key).
48
+ // Default: no key. Tests that inject a client bypass this anyway.
49
+ const mockResolveKey = vi.fn().mockReturnValue(null);
50
+ vi.mock('../anthropic-key-resolver.js', () => ({
51
+ resolveAnthropicApiKey: (...args: unknown[]) => mockResolveKey(...args),
52
+ clearAnthropicKeyCache: vi.fn(),
53
+ }));
54
+
46
55
  // Mock the SDK entry point so buildAnthropicClient doesn't touch the network.
47
56
  // Tests that need this will inject a custom client via options.client instead.
48
57
  vi.mock('@anthropic-ai/sdk', () => {
@@ -52,6 +61,14 @@ vi.mock('@anthropic-ai/sdk', () => {
52
61
  return { default: MockAnthropic };
53
62
  });
54
63
 
64
+ // Mock the key resolver so tests don't depend on filesystem state
65
+ // (~/.claude/.credentials.json, ~/.local/share/cleo/anthropic-key).
66
+ // Default: no key. Tests that inject a client via options.client bypass this.
67
+ vi.mock('../anthropic-key-resolver.js', () => ({
68
+ resolveAnthropicApiKey: vi.fn().mockReturnValue(null),
69
+ clearAnthropicKeyCache: vi.fn(),
70
+ }));
71
+
55
72
  // ---- imports after mocks --------------------------------------------------
56
73
 
57
74
  import { storeDecision } from '../decisions.js';