@cleocode/core 2026.4.35 → 2026.4.37
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/config.d.ts.map +1 -1
- package/dist/config.js +7 -0
- package/dist/config.js.map +1 -1
- package/dist/hooks/handlers/conduit-hooks.d.ts +72 -0
- package/dist/hooks/handlers/conduit-hooks.d.ts.map +1 -0
- package/dist/hooks/handlers/conduit-hooks.js +229 -0
- package/dist/hooks/handlers/conduit-hooks.js.map +1 -0
- package/dist/hooks/handlers/index.d.ts +2 -0
- package/dist/hooks/handlers/index.d.ts.map +1 -1
- package/dist/hooks/handlers/index.js +3 -0
- package/dist/hooks/handlers/index.js.map +1 -1
- package/dist/hooks/handlers/session-hooks.d.ts +14 -0
- package/dist/hooks/handlers/session-hooks.d.ts.map +1 -1
- package/dist/hooks/handlers/session-hooks.js +33 -0
- package/dist/hooks/handlers/session-hooks.js.map +1 -1
- package/dist/hooks/handlers/task-hooks.d.ts +2 -0
- package/dist/hooks/handlers/task-hooks.d.ts.map +1 -1
- package/dist/hooks/handlers/task-hooks.js +14 -0
- package/dist/hooks/handlers/task-hooks.js.map +1 -1
- package/dist/index.js +54928 -46853
- package/dist/index.js.map +4 -4
- package/dist/internal.d.ts +2 -0
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.js +1 -0
- package/dist/internal.js.map +1 -1
- package/dist/memory/anthropic-key-resolver.d.ts +35 -0
- package/dist/memory/anthropic-key-resolver.d.ts.map +1 -0
- package/dist/memory/anthropic-key-resolver.js +105 -0
- package/dist/memory/anthropic-key-resolver.js.map +1 -0
- package/dist/memory/auto-extract.d.ts +38 -42
- package/dist/memory/auto-extract.d.ts.map +1 -1
- package/dist/memory/auto-extract.js +38 -57
- package/dist/memory/auto-extract.js.map +1 -1
- package/dist/memory/brain-retrieval.d.ts +6 -0
- package/dist/memory/brain-retrieval.d.ts.map +1 -1
- package/dist/memory/brain-retrieval.js +145 -13
- package/dist/memory/brain-retrieval.js.map +1 -1
- package/dist/memory/brain-search.d.ts +82 -15
- package/dist/memory/brain-search.d.ts.map +1 -1
- package/dist/memory/brain-search.js +178 -93
- package/dist/memory/brain-search.js.map +1 -1
- package/dist/memory/engine-compat.d.ts +16 -1
- package/dist/memory/engine-compat.d.ts.map +1 -1
- package/dist/memory/engine-compat.js +0 -3
- package/dist/memory/engine-compat.js.map +1 -1
- package/dist/memory/learnings.d.ts.map +1 -1
- package/dist/memory/learnings.js +4 -3
- package/dist/memory/learnings.js.map +1 -1
- package/dist/memory/llm-extraction.d.ts +107 -0
- package/dist/memory/llm-extraction.d.ts.map +1 -0
- package/dist/memory/llm-extraction.js +425 -0
- package/dist/memory/llm-extraction.js.map +1 -0
- package/dist/memory/memory-bridge.js +23 -11
- package/dist/memory/memory-bridge.js.map +1 -1
- package/dist/memory/observer-reflector.d.ts +157 -0
- package/dist/memory/observer-reflector.d.ts.map +1 -0
- package/dist/memory/observer-reflector.js +626 -0
- package/dist/memory/observer-reflector.js.map +1 -0
- package/dist/store/brain-schema.d.ts +131 -0
- package/dist/store/brain-schema.d.ts.map +1 -1
- package/dist/store/brain-schema.js +30 -0
- package/dist/store/brain-schema.js.map +1 -1
- package/dist/store/brain-sqlite.js +41 -1
- package/dist/store/brain-sqlite.js.map +1 -1
- package/dist/tasks/complete.d.ts.map +1 -1
- package/dist/tasks/complete.js +7 -8
- package/dist/tasks/complete.js.map +1 -1
- package/package.json +13 -12
- package/src/config.ts +7 -0
- package/src/hooks/handlers/__tests__/conduit-hooks.test.ts +356 -0
- package/src/hooks/handlers/conduit-hooks.ts +258 -0
- package/src/hooks/handlers/index.ts +7 -0
- package/src/hooks/handlers/session-hooks.ts +37 -0
- package/src/hooks/handlers/task-hooks.ts +14 -0
- package/src/internal.ts +8 -0
- package/src/memory/__tests__/auto-extract.test.ts +43 -114
- package/src/memory/__tests__/brain-automation.test.ts +16 -39
- package/src/memory/__tests__/brain-rrf.test.ts +431 -0
- package/src/memory/__tests__/llm-extraction.test.ts +342 -0
- package/src/memory/__tests__/observer-reflector.test.ts +475 -0
- package/src/memory/anthropic-key-resolver.ts +113 -0
- package/src/memory/auto-extract.ts +40 -72
- package/src/memory/brain-retrieval.ts +187 -18
- package/src/memory/brain-search.ts +196 -128
- package/src/memory/engine-compat.ts +16 -4
- package/src/memory/learnings.ts +4 -3
- package/src/memory/llm-extraction.ts +524 -0
- package/src/memory/memory-bridge.ts +29 -12
- package/src/memory/observer-reflector.ts +829 -0
- package/src/store/brain-schema.ts +44 -0
- package/src/tasks/complete.ts +7 -10
|
@@ -1,64 +1,37 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Transcript and task-completion memory extraction.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* Historical context:
|
|
5
|
+
* - Previously contained a keyword-regex `ACTION_PATTERNS` that produced
|
|
6
|
+
* 88% noise in brain.db (T543).
|
|
7
|
+
* - Previously contained `extractTaskCompletionMemory` and
|
|
8
|
+
* `extractSessionEndMemory` — both disabled per T523 CA1 spec.
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
*
|
|
10
|
+
* Current design (research-backed, replaces keyword gate):
|
|
11
|
+
* - `extractFromTranscript` forwards to the LLM-driven extraction gate in
|
|
12
|
+
* `llm-extraction.ts`. The LLM returns typed structured memories
|
|
13
|
+
* (decision / pattern / learning / constraint / correction) with
|
|
14
|
+
* importance scores and justifications.
|
|
15
|
+
* - Only memories above the configured minimum importance are stored.
|
|
16
|
+
* - Each stored memory is tagged `agent-llm-extracted:<sessionId>` so
|
|
17
|
+
* downstream dedup, quality scoring, and consolidation can distinguish
|
|
18
|
+
* it from other write paths.
|
|
19
19
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* task completion. This created O(tasks x labels) noise with no
|
|
23
|
-
* deduplication, resulting in 2,466 duplicate patterns and 327
|
|
24
|
-
* duplicate learnings in brain.db (96.7% noise ratio).
|
|
20
|
+
* All extraction is best-effort: any error is swallowed so session end
|
|
21
|
+
* cannot be blocked by a failed LLM call.
|
|
25
22
|
*
|
|
26
|
-
*
|
|
27
|
-
* which runs deduplication-aware analysis on a schedule.
|
|
28
|
-
*
|
|
29
|
-
* @see .cleo/agent-outputs/T523-CA1-brain-integrity-spec.md
|
|
23
|
+
* Research: `.cleo/agent-outputs/R-llm-memory-systems-research.md`
|
|
30
24
|
*/
|
|
31
|
-
export async function extractTaskCompletionMemory(
|
|
32
|
-
_projectRoot: string,
|
|
33
|
-
_task: Task,
|
|
34
|
-
_parentTask?: Task,
|
|
35
|
-
): Promise<void> {
|
|
36
|
-
// No-op: noise generation disabled
|
|
37
|
-
return;
|
|
38
|
-
}
|
|
39
25
|
|
|
40
|
-
|
|
41
|
-
* Intentionally disabled per T523 CA1 specification.
|
|
42
|
-
*
|
|
43
|
-
* Previously auto-generated session summary decisions, duplicate
|
|
44
|
-
* "Completed:" learnings, and workflow patterns on session end.
|
|
45
|
-
* These duplicated data already stored in the sessions table and
|
|
46
|
-
* task records, adding no signal to brain.db.
|
|
47
|
-
*
|
|
48
|
-
* @see .cleo/agent-outputs/T523-CA1-brain-integrity-spec.md
|
|
49
|
-
*/
|
|
50
|
-
export async function extractSessionEndMemory(
|
|
51
|
-
_projectRoot: string,
|
|
52
|
-
_sessionData: SessionBridgeData,
|
|
53
|
-
_taskDetails: Task[],
|
|
54
|
-
): Promise<void> {
|
|
55
|
-
// No-op: noise generation disabled
|
|
56
|
-
return;
|
|
57
|
-
}
|
|
26
|
+
import type { Task } from '@cleocode/contracts';
|
|
58
27
|
|
|
59
28
|
/**
|
|
60
29
|
* Resolve an array of task IDs to their full Task objects.
|
|
61
30
|
* Tasks that cannot be found are silently excluded.
|
|
31
|
+
*
|
|
32
|
+
* Retained from the previous implementation because it is still used by
|
|
33
|
+
* callers that need hydrated task details without coupling to the disabled
|
|
34
|
+
* extraction stubs.
|
|
62
35
|
*/
|
|
63
36
|
export async function resolveTaskDetails(projectRoot: string, taskIds: string[]): Promise<Task[]> {
|
|
64
37
|
if (taskIds.length === 0) {
|
|
@@ -74,21 +47,24 @@ export async function resolveTaskDetails(projectRoot: string, taskIds: string[])
|
|
|
74
47
|
}
|
|
75
48
|
}
|
|
76
49
|
|
|
77
|
-
/** Action words that indicate a meaningful assistant turn worth storing. */
|
|
78
|
-
const ACTION_PATTERNS =
|
|
79
|
-
/\b(implement|fix|add|create|update|remove|refactor|extract|migrate|resolve|complete|found|learned|discovered)\b/i;
|
|
80
|
-
|
|
81
50
|
/**
|
|
82
|
-
* Extract
|
|
83
|
-
*
|
|
51
|
+
* Extract durable knowledge from a provider session transcript and store it
|
|
52
|
+
* in brain.db via the LLM extraction gate.
|
|
84
53
|
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
54
|
+
* Replaces the legacy `ACTION_PATTERNS` keyword-regex extractor. The LLM
|
|
55
|
+
* returns typed, structured memories with justification and importance
|
|
56
|
+
* scoring; only high-value items are persisted.
|
|
57
|
+
*
|
|
58
|
+
* Behaviour:
|
|
59
|
+
* - Returns silently when transcript is empty/non-string.
|
|
60
|
+
* - Returns silently when `brain.llmExtraction.enabled` is false OR when
|
|
61
|
+
* `ANTHROPIC_API_KEY` is not set (best-effort degradation).
|
|
62
|
+
* - Never throws — all errors are swallowed so session end cannot be
|
|
63
|
+
* blocked by a failed extraction.
|
|
87
64
|
*
|
|
88
65
|
* @param projectRoot - Absolute path to project root.
|
|
89
66
|
* @param sessionId - The CLEO session ID being processed.
|
|
90
67
|
* @param transcript - Plain-text provider transcript (user/assistant turns).
|
|
91
|
-
* @task T144 @epic T134
|
|
92
68
|
*/
|
|
93
69
|
export async function extractFromTranscript(
|
|
94
70
|
projectRoot: string,
|
|
@@ -96,20 +72,12 @@ export async function extractFromTranscript(
|
|
|
96
72
|
transcript: string,
|
|
97
73
|
): Promise<void> {
|
|
98
74
|
try {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
if (actionLines.length === 0) return;
|
|
102
|
-
|
|
103
|
-
const { storeLearning } = await import('./learnings.js');
|
|
104
|
-
for (const line of actionLines) {
|
|
105
|
-
await storeLearning(projectRoot, {
|
|
106
|
-
insight: line.trim().slice(0, 250),
|
|
107
|
-
source: `transcript:${sessionId}`,
|
|
108
|
-
confidence: 0.6,
|
|
109
|
-
actionable: false,
|
|
110
|
-
});
|
|
75
|
+
if (typeof transcript !== 'string' || transcript.trim().length === 0) {
|
|
76
|
+
return;
|
|
111
77
|
}
|
|
78
|
+
const { extractFromTranscript: runLlmExtraction } = await import('./llm-extraction.js');
|
|
79
|
+
await runLlmExtraction({ projectRoot, sessionId, transcript });
|
|
112
80
|
} catch {
|
|
113
|
-
// Best-effort: must never throw
|
|
81
|
+
// Best-effort: extraction must never throw during session end.
|
|
114
82
|
}
|
|
115
83
|
}
|
|
@@ -36,7 +36,7 @@ import type {
|
|
|
36
36
|
BrainNarrativeRow,
|
|
37
37
|
BrainTimelineNeighborRow,
|
|
38
38
|
} from './brain-row-types.js';
|
|
39
|
-
import { searchBrain } from './brain-search.js';
|
|
39
|
+
import { hybridSearch, searchBrain } from './brain-search.js';
|
|
40
40
|
import { searchSimilar } from './brain-similarity.js';
|
|
41
41
|
import { addGraphEdge, upsertGraphNode } from './graph-auto-populate.js';
|
|
42
42
|
import { computeObservationQuality } from './quality-scoring.js';
|
|
@@ -65,6 +65,12 @@ export interface SearchBrainCompactParams {
|
|
|
65
65
|
dateEnd?: string;
|
|
66
66
|
/** T418: filter results to observations produced by a specific agent (Wave 8 mental models). */
|
|
67
67
|
agent?: string;
|
|
68
|
+
/**
|
|
69
|
+
* When true (default), use Reciprocal Rank Fusion to combine FTS5 and
|
|
70
|
+
* vector search results for higher recall and better ranking.
|
|
71
|
+
* When false, fall back to FTS5-only search (faster, no embeddings needed).
|
|
72
|
+
*/
|
|
73
|
+
useRRF?: boolean;
|
|
68
74
|
}
|
|
69
75
|
|
|
70
76
|
/** Result from searchBrainCompact. */
|
|
@@ -166,20 +172,109 @@ export async function searchBrainCompact(
|
|
|
166
172
|
projectRoot: string,
|
|
167
173
|
params: SearchBrainCompactParams,
|
|
168
174
|
): Promise<SearchBrainCompactResult> {
|
|
169
|
-
const { query, limit, tables, dateStart, dateEnd, agent } = params;
|
|
175
|
+
const { query, limit, tables, dateStart, dateEnd, agent, useRRF = true } = params;
|
|
170
176
|
|
|
171
177
|
if (!query?.trim()) {
|
|
172
178
|
return { results: [], total: 0, tokensEstimated: 0 };
|
|
173
179
|
}
|
|
174
180
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
181
|
+
const effectiveLimit = limit ?? 10;
|
|
182
|
+
|
|
183
|
+
// T418: agent filter always forces FTS-only on observations table
|
|
184
|
+
const agentFilter = agent !== undefined && agent !== null;
|
|
185
|
+
|
|
186
|
+
// ----- RRF path (default) -----
|
|
187
|
+
if (useRRF && !agentFilter) {
|
|
188
|
+
// Run FTS (for dates + table-level data) and RRF fusion in parallel.
|
|
189
|
+
// FTS gives us row-level dates; RRF gives us the fused ranking order.
|
|
190
|
+
const [ftsResult, rrfResults] = await Promise.all([
|
|
191
|
+
searchBrain(projectRoot, query, { limit: effectiveLimit * 3, tables }).catch(() => ({
|
|
192
|
+
decisions: [],
|
|
193
|
+
patterns: [],
|
|
194
|
+
learnings: [],
|
|
195
|
+
observations: [],
|
|
196
|
+
})),
|
|
197
|
+
hybridSearch(query, projectRoot, { limit: effectiveLimit * 2 }),
|
|
198
|
+
]);
|
|
199
|
+
|
|
200
|
+
// Build a date map from FTS rows (id -> date string)
|
|
201
|
+
const dateMap = new Map<string, string>();
|
|
202
|
+
for (const d of ftsResult.decisions) {
|
|
203
|
+
const raw = d as Record<string, unknown>;
|
|
204
|
+
dateMap.set(d.id, (d.createdAt ?? (raw['created_at'] as string)) || '');
|
|
205
|
+
}
|
|
206
|
+
for (const p of ftsResult.patterns) {
|
|
207
|
+
const raw = p as Record<string, unknown>;
|
|
208
|
+
dateMap.set(p.id, (p.extractedAt ?? (raw['extracted_at'] as string)) || '');
|
|
209
|
+
}
|
|
210
|
+
for (const l of ftsResult.learnings) {
|
|
211
|
+
const raw = l as Record<string, unknown>;
|
|
212
|
+
dateMap.set(l.id, (l.createdAt ?? (raw['created_at'] as string)) || '');
|
|
213
|
+
}
|
|
214
|
+
for (const o of ftsResult.observations) {
|
|
215
|
+
const raw = o as Record<string, unknown>;
|
|
216
|
+
dateMap.set(o.id, (o.createdAt ?? (raw['created_at'] as string)) || '');
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Apply table filter when specified (map singular type names to plural table names)
|
|
220
|
+
const singularToTable: Record<string, string> = {
|
|
221
|
+
decision: 'decisions',
|
|
222
|
+
pattern: 'patterns',
|
|
223
|
+
learning: 'learnings',
|
|
224
|
+
observation: 'observations',
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
let results: BrainCompactHit[] = rrfResults
|
|
228
|
+
.map((r) => ({
|
|
229
|
+
id: r.id,
|
|
230
|
+
type: r.type as 'decision' | 'pattern' | 'learning' | 'observation',
|
|
231
|
+
title: r.title.slice(0, 80),
|
|
232
|
+
date: dateMap.get(r.id) ?? '',
|
|
233
|
+
relevance: r.score,
|
|
234
|
+
}))
|
|
235
|
+
.filter((r) => {
|
|
236
|
+
// Only include items that the FTS scan returned (ensures quality gating is respected)
|
|
237
|
+
return dateMap.has(r.id);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
if (tables && tables.length > 0) {
|
|
241
|
+
results = results.filter((r) =>
|
|
242
|
+
tables.includes(
|
|
243
|
+
singularToTable[r.type] as 'decisions' | 'patterns' | 'learnings' | 'observations',
|
|
244
|
+
),
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Apply date filters client-side
|
|
249
|
+
if (dateStart) results = results.filter((r) => !r.date || r.date >= dateStart);
|
|
250
|
+
if (dateEnd) results = results.filter((r) => !r.date || r.date <= dateEnd);
|
|
251
|
+
|
|
252
|
+
results = results.slice(0, effectiveLimit);
|
|
253
|
+
|
|
254
|
+
for (const hit of results) {
|
|
255
|
+
hit._next = memoryFindHitNext(hit.id);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (results.length > 0) {
|
|
259
|
+
const returnedIds = results.map((r) => r.id);
|
|
260
|
+
setImmediate(() => {
|
|
261
|
+
incrementCitationCounts(projectRoot, returnedIds).catch(() => {});
|
|
262
|
+
logRetrieval(projectRoot, query, returnedIds, 'find-rrf', results.length * 50).catch(
|
|
263
|
+
() => {},
|
|
264
|
+
);
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return { results, total: results.length, tokensEstimated: results.length * 50 };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// ----- FTS-only path (useRRF=false or agent filter) -----
|
|
272
|
+
const effectiveTables = agentFilter
|
|
273
|
+
? (['observations'] as Array<'decisions' | 'patterns' | 'learnings' | 'observations'>)
|
|
274
|
+
: tables;
|
|
180
275
|
|
|
181
276
|
const searchResult = await searchBrain(projectRoot, query, {
|
|
182
|
-
limit:
|
|
277
|
+
limit: effectiveLimit,
|
|
183
278
|
tables: effectiveTables,
|
|
184
279
|
});
|
|
185
280
|
|
|
@@ -189,7 +284,7 @@ export async function searchBrainCompact(
|
|
|
189
284
|
// We handle both naming conventions for robustness.
|
|
190
285
|
let results: BrainCompactHit[] = [];
|
|
191
286
|
|
|
192
|
-
if (!
|
|
287
|
+
if (!agentFilter) {
|
|
193
288
|
for (const d of searchResult.decisions) {
|
|
194
289
|
const raw = d as Record<string, unknown>;
|
|
195
290
|
results.push({
|
|
@@ -224,7 +319,7 @@ export async function searchBrainCompact(
|
|
|
224
319
|
for (const o of searchResult.observations) {
|
|
225
320
|
const raw = o as Record<string, unknown>;
|
|
226
321
|
// T418: apply agent post-filter when specified
|
|
227
|
-
if (
|
|
322
|
+
if (agentFilter) {
|
|
228
323
|
const rowAgent = o.agent ?? (raw['agent'] as string | null) ?? null;
|
|
229
324
|
if (rowAgent !== agent) continue;
|
|
230
325
|
}
|
|
@@ -237,18 +332,23 @@ export async function searchBrainCompact(
|
|
|
237
332
|
}
|
|
238
333
|
|
|
239
334
|
// Apply date filters client-side if provided
|
|
240
|
-
if (dateStart)
|
|
241
|
-
|
|
242
|
-
}
|
|
243
|
-
if (dateEnd) {
|
|
244
|
-
results = results.filter((r) => r.date <= dateEnd);
|
|
245
|
-
}
|
|
335
|
+
if (dateStart) results = results.filter((r) => r.date >= dateStart);
|
|
336
|
+
if (dateEnd) results = results.filter((r) => r.date <= dateEnd);
|
|
246
337
|
|
|
247
338
|
// Enrich each hit with _next progressive disclosure directives
|
|
248
339
|
for (const hit of results) {
|
|
249
340
|
hit._next = memoryFindHitNext(hit.id);
|
|
250
341
|
}
|
|
251
342
|
|
|
343
|
+
// Citation tracking + retrieval logging (non-blocking)
|
|
344
|
+
if (results.length > 0) {
|
|
345
|
+
const returnedIds = results.map((r) => r.id);
|
|
346
|
+
setImmediate(() => {
|
|
347
|
+
incrementCitationCounts(projectRoot, returnedIds).catch(() => {});
|
|
348
|
+
logRetrieval(projectRoot, query, returnedIds, 'find', results.length * 50).catch(() => {});
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
|
|
252
352
|
return {
|
|
253
353
|
results,
|
|
254
354
|
total: results.length,
|
|
@@ -499,6 +599,21 @@ export async function fetchBrainEntries(
|
|
|
499
599
|
}
|
|
500
600
|
}
|
|
501
601
|
|
|
602
|
+
// Citation tracking + retrieval logging (non-blocking)
|
|
603
|
+
if (results.length > 0) {
|
|
604
|
+
const fetchedIds = results.map((r) => r.id);
|
|
605
|
+
setImmediate(() => {
|
|
606
|
+
incrementCitationCounts(projectRoot, fetchedIds).catch(() => {});
|
|
607
|
+
logRetrieval(
|
|
608
|
+
projectRoot,
|
|
609
|
+
fetchedIds.join(','),
|
|
610
|
+
fetchedIds,
|
|
611
|
+
'fetch',
|
|
612
|
+
results.length * 500,
|
|
613
|
+
).catch(() => {});
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
|
|
502
617
|
return {
|
|
503
618
|
results,
|
|
504
619
|
notFound,
|
|
@@ -579,7 +694,12 @@ export async function observeBrain(
|
|
|
579
694
|
// - sourceType 'manual' → 'owner' (owner-stated facts skip short-term in consolidator)
|
|
580
695
|
// - sourceType 'session-debrief' → 'task-outcome' (synthesized summaries)
|
|
581
696
|
// - otherwise → 'agent' (default for all hook/agent writes)
|
|
582
|
-
//
|
|
697
|
+
// Source confidence routing (spec §4.1 Decision Tree):
|
|
698
|
+
// - sourceType 'manual' → 'owner' (owner-stated facts are ground truth)
|
|
699
|
+
// - sourceType 'session-debrief' → 'task-outcome' (verified by completion)
|
|
700
|
+
// - otherwise → 'agent' (default for all hook/agent writes)
|
|
701
|
+
// Owner and task-outcome sources are auto-verified as ground truth.
|
|
702
|
+
// Agent-inferred entries start unverified — consolidator promotes via corroboration.
|
|
583
703
|
const resolvedSourceConfidence: BrainSourceConfidence =
|
|
584
704
|
sourceConfidenceParam ??
|
|
585
705
|
(sourceType === 'manual'
|
|
@@ -589,7 +709,8 @@ export async function observeBrain(
|
|
|
589
709
|
: 'agent');
|
|
590
710
|
const memoryTier: BrainMemoryTier = 'short';
|
|
591
711
|
const memoryType = 'episodic' as const;
|
|
592
|
-
const verified =
|
|
712
|
+
const verified =
|
|
713
|
+
resolvedSourceConfidence === 'owner' || resolvedSourceConfidence === 'task-outcome';
|
|
593
714
|
|
|
594
715
|
// Content-hash dedup: SHA-256 prefix of title+text
|
|
595
716
|
const contentHash = createHash('sha256')
|
|
@@ -1289,3 +1410,51 @@ async function incrementCitationCounts(projectRoot: string, ids: string[]): Prom
|
|
|
1289
1410
|
}
|
|
1290
1411
|
}
|
|
1291
1412
|
}
|
|
1413
|
+
|
|
1414
|
+
/**
|
|
1415
|
+
* Log a retrieval event to brain_retrieval_log for co-retrieval analysis.
|
|
1416
|
+
*
|
|
1417
|
+
* Creates the table on first use if it doesn't exist (self-healing).
|
|
1418
|
+
* Best-effort: errors are silently swallowed.
|
|
1419
|
+
*/
|
|
1420
|
+
async function logRetrieval(
|
|
1421
|
+
projectRoot: string,
|
|
1422
|
+
query: string,
|
|
1423
|
+
entryIds: string[],
|
|
1424
|
+
source: string,
|
|
1425
|
+
tokensUsed?: number,
|
|
1426
|
+
): Promise<void> {
|
|
1427
|
+
if (entryIds.length === 0) return;
|
|
1428
|
+
|
|
1429
|
+
const { getBrainDb, getBrainNativeDb } = await import('../store/brain-sqlite.js');
|
|
1430
|
+
await getBrainDb(projectRoot);
|
|
1431
|
+
const nativeDb = getBrainNativeDb();
|
|
1432
|
+
if (!nativeDb) return;
|
|
1433
|
+
|
|
1434
|
+
// Self-healing: create table if not exists
|
|
1435
|
+
const createSql =
|
|
1436
|
+
'CREATE TABLE IF NOT EXISTS brain_retrieval_log (' +
|
|
1437
|
+
'id INTEGER PRIMARY KEY AUTOINCREMENT,' +
|
|
1438
|
+
'query TEXT NOT NULL,' +
|
|
1439
|
+
'entry_ids TEXT NOT NULL,' +
|
|
1440
|
+
'entry_count INTEGER NOT NULL,' +
|
|
1441
|
+
'source TEXT NOT NULL,' +
|
|
1442
|
+
'tokens_used INTEGER,' +
|
|
1443
|
+
"created_at TEXT NOT NULL DEFAULT (datetime('now'))" +
|
|
1444
|
+
')';
|
|
1445
|
+
try {
|
|
1446
|
+
nativeDb.prepare(createSql).run();
|
|
1447
|
+
} catch {
|
|
1448
|
+
return;
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
try {
|
|
1452
|
+
nativeDb
|
|
1453
|
+
.prepare(
|
|
1454
|
+
'INSERT INTO brain_retrieval_log (query, entry_ids, entry_count, source, tokens_used) VALUES (?, ?, ?, ?, ?)',
|
|
1455
|
+
)
|
|
1456
|
+
.run(query, entryIds.join(','), entryIds.length, source, tokensUsed ?? null);
|
|
1457
|
+
} catch {
|
|
1458
|
+
/* best-effort */
|
|
1459
|
+
}
|
|
1460
|
+
}
|