claude-mem-lite 3.96.0 → 3.97.0
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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/adopt-cli.mjs +125 -51
- package/bash-utils.mjs +105 -29
- package/claudemd.mjs +63 -19
- package/cli/activity.mjs +36 -18
- package/cli/common.mjs +101 -18
- package/cli/doctor.mjs +29 -9
- package/cli.mjs +69 -10
- package/deep-search.mjs +63 -20
- package/format-utils.mjs +15 -7
- package/haiku-client.mjs +189 -111
- package/hash-utils.mjs +15 -6
- package/hook-context.mjs +135 -46
- package/hook-episode.mjs +76 -23
- package/hook-handoff.mjs +216 -84
- package/hook-llm.mjs +476 -216
- package/hook-memory.mjs +106 -43
- package/hook-optimize.mjs +272 -109
- package/hook-precompact.mjs +1 -1
- package/hook-semaphore.mjs +46 -12
- package/hook-shared.mjs +101 -131
- package/hook-update.mjs +286 -88
- package/hook.mjs +1207 -617
- package/install-metadata.mjs +246 -125
- package/install.mjs +1327 -917
- package/lib/activity.mjs +52 -29
- package/lib/atomic-write.mjs +23 -4
- package/lib/binding-probe.mjs +40 -38
- package/lib/browse-core.mjs +22 -8
- package/lib/citation-tracker.mjs +189 -79
- package/lib/cite-back-hint.mjs +37 -19
- package/lib/cite-recall-path.mjs +3 -1
- package/lib/cli-flags.mjs +4 -2
- package/lib/cli-project.mjs +8 -5
- package/lib/compress-core.mjs +34 -9
- package/lib/cooldown-path.mjs +3 -1
- package/lib/db-backup.mjs +33 -11
- package/lib/deferred-work.mjs +73 -32
- package/lib/delete-core.mjs +25 -9
- package/lib/doctor-benchmark.mjs +5 -1
- package/lib/doctor-drift.mjs +25 -8
- package/lib/edge-attribution.mjs +23 -9
- package/lib/env-number.mjs +7 -8
- package/lib/err-sampler.mjs +22 -11
- package/lib/error-recall-core.mjs +15 -17
- package/lib/events-injection.mjs +25 -8
- package/lib/export-columns.mjs +49 -10
- package/lib/fast-summary.mjs +22 -9
- package/lib/file-edge-match.mjs +4 -2
- package/lib/file-intel.mjs +38 -13
- package/lib/frontmatter.mjs +22 -5
- package/lib/get-core.mjs +78 -11
- package/lib/handoff-constants.mjs +18 -0
- package/lib/hook-stdin.mjs +10 -3
- package/lib/hook-stdout.mjs +21 -7
- package/lib/hook-telemetry.mjs +44 -17
- package/lib/id-routing.mjs +11 -6
- package/lib/import-jsonl.mjs +92 -41
- package/lib/inject-search-core.mjs +5 -2
- package/lib/injected-ids.mjs +19 -12
- package/lib/install-shape.mjs +47 -23
- package/lib/keyctx-marker.mjs +3 -1
- package/lib/lesson-bridge.mjs +8 -5
- package/lib/llm-call.mjs +76 -0
- package/lib/llm-provider-probe.mjs +24 -12
- package/lib/low-signal-patterns.mjs +25 -22
- package/lib/maintain-core.mjs +221 -92
- package/lib/mem-override.mjs +4 -2
- package/lib/metrics.mjs +30 -25
- package/lib/native-binding-hint.mjs +29 -8
- package/lib/observation-write.mjs +74 -19
- package/lib/patha-exclude-meter.mjs +10 -6
- package/lib/persist-reminder.mjs +22 -10
- package/lib/plan-reader.mjs +2 -2
- package/lib/private-strip.mjs +3 -3
- package/lib/proc-lock.mjs +15 -3
- package/lib/proxy-fetch.mjs +49 -13
- package/lib/quiet-scope.mjs +42 -0
- package/lib/recall-core.mjs +19 -7
- package/lib/recent-core.mjs +18 -5
- package/lib/registry-core.mjs +12 -6
- package/lib/release-digest.mjs +4 -1
- package/lib/relevance-floor.mjs +5 -2
- package/lib/reread-guard.mjs +16 -8
- package/lib/resolve-data-dir.mjs +1 -1
- package/lib/rrf.mjs +4 -1
- package/lib/save-enrich.mjs +36 -18
- package/lib/save-observation.mjs +78 -35
- package/lib/scrub-record.mjs +20 -6
- package/lib/search-core.mjs +456 -123
- package/lib/shard-gc.mjs +50 -0
- package/lib/startup-dashboard.mjs +20 -9
- package/lib/stats-core.mjs +101 -35
- package/lib/stats-quality.mjs +56 -20
- package/lib/summary-extractor.mjs +2 -2
- package/lib/task-imperative.mjs +12 -4
- package/lib/timeline-core.mjs +88 -28
- package/lib/tmp-fixture-sweep.mjs +32 -9
- package/lib/transcript-scan.mjs +20 -4
- package/lib/upgrade-banner.mjs +11 -7
- package/mem-cli.mjs +1314 -506
- package/memdir.mjs +53 -21
- package/nlp.mjs +68 -38
- package/npm-shrinkwrap.json +2 -2
- package/package.json +9 -2
- package/plugin-cache-guard.mjs +31 -15
- package/project-utils.mjs +44 -20
- package/registry-enricher.mjs +26 -6
- package/registry-github.mjs +4 -2
- package/registry-importer.mjs +151 -40
- package/registry-recommend.mjs +189 -96
- package/registry-retriever.mjs +229 -70
- package/registry-scanner.mjs +11 -6
- package/registry.mjs +163 -66
- package/rerank.mjs +18 -4
- package/resource-discovery.mjs +14 -4
- package/schema.mjs +258 -99
- package/scoring-sql.mjs +21 -10
- package/scripts/binding-probe-cli.mjs +17 -12
- package/scripts/hook-launcher.mjs +148 -31
- package/scripts/launch-preflight.mjs +5 -7
- package/scripts/launch.mjs +21 -9
- package/scripts/post-tool-recall.js +21 -5
- package/scripts/pre-agent-inject.js +51 -13
- package/scripts/pre-skill-bridge.js +22 -7
- package/scripts/pre-tool-recall.js +157 -77
- package/scripts/prompt-search-utils.mjs +70 -25
- package/scripts/setup.sh +16 -1
- package/scripts/user-prompt-search.js +238 -97
- package/search-engine.mjs +329 -96
- package/search-scoring.mjs +75 -33
- package/secret-scrub.mjs +48 -12
- package/server/fts-check.mjs +12 -9
- package/server.mjs +674 -243
- package/skip-tools.mjs +19 -8
- package/source-files.mjs +54 -21
- package/stop-words.mjs +152 -19
- package/synonyms.mjs +579 -164
- package/tfidf.mjs +130 -40
- package/tier.mjs +1 -1
- package/tool-schemas.mjs +381 -131
- package/utils.mjs +184 -36
package/hook-optimize.mjs
CHANGED
|
@@ -9,9 +9,17 @@
|
|
|
9
9
|
import { readFileSync, writeFileSync } from 'fs';
|
|
10
10
|
import { join } from 'path';
|
|
11
11
|
import {
|
|
12
|
-
truncate,
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
truncate,
|
|
13
|
+
debugLog,
|
|
14
|
+
debugCatch,
|
|
15
|
+
COMPRESSED_AUTO,
|
|
16
|
+
computeMinHash,
|
|
17
|
+
estimateJaccardFromMinHash,
|
|
18
|
+
jaccardSimilarity,
|
|
19
|
+
clampImportance,
|
|
20
|
+
cjkBigrams,
|
|
21
|
+
notLowSignalTitleClause,
|
|
22
|
+
scrubSecrets,
|
|
15
23
|
} from './utils.mjs';
|
|
16
24
|
import { callModelJSONAsync, BG_LLM_TIMEOUT_MS } from './haiku-client.mjs';
|
|
17
25
|
import { acquireLLMSlot, releaseLLMSlot } from './hook-semaphore.mjs';
|
|
@@ -201,12 +209,16 @@ export async function executeReenrich(db, limit = 10, { scope = 'narrow', projec
|
|
|
201
209
|
const candidates = findReenrichCandidates(db, limit, { scope, project });
|
|
202
210
|
if (candidates.length === 0) return { processed: 0, skipped: 0 };
|
|
203
211
|
|
|
204
|
-
let processed = 0,
|
|
212
|
+
let processed = 0,
|
|
213
|
+
skipped = 0;
|
|
205
214
|
const validTypes = OBS_TYPE_SET;
|
|
206
215
|
|
|
207
216
|
for (const cand of candidates) {
|
|
208
217
|
const gotSlot = await acquireLLMSlot();
|
|
209
|
-
if (!gotSlot) {
|
|
218
|
+
if (!gotSlot) {
|
|
219
|
+
skipped++;
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
210
222
|
|
|
211
223
|
try {
|
|
212
224
|
if (scope === 'scopes') {
|
|
@@ -223,15 +235,25 @@ Lesson: ${truncate(cand.lesson_learned || '(none)', 300)}
|
|
|
223
235
|
|
|
224
236
|
JSON: {"scope":"file|module|project|environment"}
|
|
225
237
|
scope: ${SCOPE_PROMPT_LEGEND}`;
|
|
226
|
-
const parsed = await callModelJSONAsync(scopePrompt, 'haiku', {
|
|
238
|
+
const parsed = await callModelJSONAsync(scopePrompt, 'haiku', {
|
|
239
|
+
timeout: BG_LLM_TIMEOUT_MS,
|
|
240
|
+
maxTokens: 60,
|
|
241
|
+
});
|
|
227
242
|
const scopeValue = normalizeScope(parsed && parsed.scope);
|
|
228
|
-
if (!scopeValue) {
|
|
243
|
+
if (!scopeValue) {
|
|
244
|
+
skipped++;
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
229
247
|
// `AND scope IS NULL` is the fill-only-empty guard: a save-enrich worker or
|
|
230
248
|
// an episode upgrade can land between candidate selection and this write,
|
|
231
249
|
// and a classifier round-trip is long enough for that to be real.
|
|
232
|
-
const res = db
|
|
250
|
+
const res = db
|
|
251
|
+
.prepare('UPDATE observations SET scope = ? WHERE id = ? AND scope IS NULL')
|
|
233
252
|
.run(scopeValue, cand.id);
|
|
234
|
-
if (res.changes === 0) {
|
|
253
|
+
if (res.changes === 0) {
|
|
254
|
+
skipped++;
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
235
257
|
// No rebuildVector: scope is a filter column, absent from the FTS text
|
|
236
258
|
// field and from vecTextForRow — a rebuild here would be a no-op write.
|
|
237
259
|
processed++;
|
|
@@ -251,11 +273,18 @@ Narrative: ${truncate(cand.narrative || '(no narrative)', 500)}
|
|
|
251
273
|
JSON: {"search_aliases":["alt phrasing","synonym","spelled-out jargon","CJK term if the domain word has one"],"scope":"file|module|project|environment"}
|
|
252
274
|
Give 3-6 aliases: words a user might search for the SAME concept but that are NOT already in the title (synonyms, the spelled-out form of an acronym, the jargon term for a described symptom, a CJK translation of a key domain term).
|
|
253
275
|
scope: ${SCOPE_PROMPT_LEGEND}`;
|
|
254
|
-
const parsed = await callModelJSONAsync(aliasPrompt, 'haiku', {
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
276
|
+
const parsed = await callModelJSONAsync(aliasPrompt, 'haiku', {
|
|
277
|
+
timeout: BG_LLM_TIMEOUT_MS,
|
|
278
|
+
maxTokens: 300,
|
|
279
|
+
});
|
|
280
|
+
const aliasArr =
|
|
281
|
+
parsed && Array.isArray(parsed.search_aliases)
|
|
282
|
+
? parsed.search_aliases.filter((a) => typeof a === 'string' && a.trim().length > 0)
|
|
283
|
+
: [];
|
|
284
|
+
if (!aliasArr.length) {
|
|
285
|
+
skipped++;
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
259
288
|
const searchAliases = aliasArr.slice(0, 6).join(' ');
|
|
260
289
|
const aliasBigrams = cjkBigrams(searchAliases);
|
|
261
290
|
const appendedText = [cand.text || '', searchAliases, aliasBigrams].filter(Boolean).join(' ');
|
|
@@ -263,8 +292,9 @@ scope: ${SCOPE_PROMPT_LEGEND}`;
|
|
|
263
292
|
// scope rides this call for free (D#135 P3). COALESCE, not a plain set:
|
|
264
293
|
// an omitted or off-enum value normalizes to null and must not erase a
|
|
265
294
|
// classification an earlier face already wrote.
|
|
266
|
-
db.prepare(
|
|
267
|
-
|
|
295
|
+
db.prepare(
|
|
296
|
+
`UPDATE observations SET search_aliases = ?, text = ?, scope = COALESCE(?, scope) WHERE id = ?`,
|
|
297
|
+
).run(safe.search_aliases, safe.text, normalizeScope(parsed.scope), cand.id);
|
|
268
298
|
// Refresh the TF-IDF vector from the just-updated FTS text so the new
|
|
269
299
|
// aliases reach the vector arm too — the narrow/wide branch rebuilds, this
|
|
270
300
|
// one must as well. No-ops when the vector arm is off / vocab unbuilt.
|
|
@@ -284,8 +314,14 @@ lesson_learned: State what was learned. If routine, write "none".
|
|
|
284
314
|
search_aliases: 2-6 alternative search terms (include CJK if applicable).
|
|
285
315
|
scope: ${SCOPE_PROMPT_LEGEND}`;
|
|
286
316
|
|
|
287
|
-
const parsed = await callModelJSONAsync(prompt, 'haiku', {
|
|
288
|
-
|
|
317
|
+
const parsed = await callModelJSONAsync(prompt, 'haiku', {
|
|
318
|
+
timeout: BG_LLM_TIMEOUT_MS,
|
|
319
|
+
maxTokens: 500,
|
|
320
|
+
});
|
|
321
|
+
if (!parsed || !parsed.title) {
|
|
322
|
+
skipped++;
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
289
325
|
|
|
290
326
|
// Auto-hide on importance:0 targets fully-degraded NARROW rows (this branch predates
|
|
291
327
|
// the wide-scope widening). A wide candidate has a substantive narrative (>100 chars)
|
|
@@ -294,15 +330,16 @@ scope: ${SCOPE_PROMPT_LEGEND}`;
|
|
|
294
330
|
// hide a real observation until manual surgery. In wide scope, fall through and let
|
|
295
331
|
// clampImportance floor it to 1 (kept visible, low-ranked) instead of hiding.
|
|
296
332
|
if ((parsed.importance === 0 || parsed.importance === '0') && scope !== 'wide') {
|
|
297
|
-
db.prepare(
|
|
298
|
-
|
|
333
|
+
db.prepare(
|
|
334
|
+
`UPDATE observations SET compressed_into = ${COMPRESSED_AUTO}, optimized_at = ? WHERE id = ?`,
|
|
335
|
+
).run(Date.now(), cand.id);
|
|
299
336
|
processed++;
|
|
300
337
|
continue;
|
|
301
338
|
}
|
|
302
339
|
|
|
303
340
|
// Enrichment ("add a lesson") must not reclassify a specific type down to the generic
|
|
304
341
|
// 'change' (lower TYPE_QUALITY + faster decay); keep the stored type on that downgrade.
|
|
305
|
-
let type = validTypes.has(parsed.type) ? parsed.type :
|
|
342
|
+
let type = validTypes.has(parsed.type) ? parsed.type : cand.type || 'change';
|
|
306
343
|
if (type === 'change' && cand.type && cand.type !== 'change') type = cand.type;
|
|
307
344
|
const concepts = Array.isArray(parsed.concepts) ? parsed.concepts.slice(0, 10) : [];
|
|
308
345
|
const facts = Array.isArray(parsed.facts) ? parsed.facts.slice(0, 10) : [];
|
|
@@ -312,17 +349,21 @@ scope: ${SCOPE_PROMPT_LEGEND}`;
|
|
|
312
349
|
// sets optimized_at, locking the row out of any future re-enrich (:88), so the loss is
|
|
313
350
|
// permanent. Keep the candidate's existing value when the LLM returned nothing. (Narrow
|
|
314
351
|
// candidates are all-null on these by their WHERE, so cand.* is falsy → no-op there.)
|
|
315
|
-
const conceptsText = concepts.length ? concepts.join(' ') :
|
|
316
|
-
const factsText = facts.length ? facts.join(' ') :
|
|
352
|
+
const conceptsText = concepts.length ? concepts.join(' ') : cand.concepts || '';
|
|
353
|
+
const factsText = facts.length ? facts.join(' ') : cand.facts || '';
|
|
317
354
|
// Scrub BEFORE truncate so a secret straddling the cut can't leave a sub-6-char
|
|
318
355
|
// head that scrubSecrets's value-length floor no longer matches (the scrubRecord
|
|
319
356
|
// below would then miss it too). Mirrors the hook-llm save-path fix.
|
|
320
|
-
const lessonLearned =
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
357
|
+
const lessonLearned =
|
|
358
|
+
typeof parsed.lesson_learned === 'string' &&
|
|
359
|
+
parsed.lesson_learned.toLowerCase() !== 'none' &&
|
|
360
|
+
parsed.lesson_learned.trim().length > 0
|
|
361
|
+
? scrubSecrets(parsed.lesson_learned).slice(0, 500)
|
|
362
|
+
: null;
|
|
363
|
+
const searchAliases =
|
|
364
|
+
Array.isArray(parsed.search_aliases) && parsed.search_aliases.length
|
|
365
|
+
? parsed.search_aliases.slice(0, 6).join(' ')
|
|
366
|
+
: cand.search_aliases || null;
|
|
326
367
|
const title = truncate(scrubSecrets(parsed.title || ''), 120);
|
|
327
368
|
const narrative = truncate(scrubSecrets(parsed.narrative || cand.narrative || ''), 500);
|
|
328
369
|
// Floor at the stored importance: re-enrich adds a lesson, it must never silently downgrade
|
|
@@ -337,26 +378,47 @@ scope: ${SCOPE_PROMPT_LEGEND}`;
|
|
|
337
378
|
// Scrub LLM-output text fields at the UPDATE boundary. type is an
|
|
338
379
|
// enum, importance is numeric, minhash_sig is hash bytes.
|
|
339
380
|
const safe = scrubRecord('observations', {
|
|
340
|
-
title,
|
|
381
|
+
title,
|
|
382
|
+
narrative,
|
|
341
383
|
concepts: conceptsText,
|
|
342
384
|
facts: factsText,
|
|
343
385
|
text: textField,
|
|
344
386
|
lesson_learned: lessonLearned,
|
|
345
387
|
search_aliases: searchAliases,
|
|
346
388
|
});
|
|
347
|
-
db.prepare(
|
|
389
|
+
db.prepare(
|
|
390
|
+
`
|
|
348
391
|
UPDATE observations SET type=?, title=?, narrative=?, concepts=?, facts=?,
|
|
349
392
|
text=?, importance=?, lesson_learned=?, search_aliases=?, minhash_sig=?, optimized_at=?,
|
|
350
393
|
scope=COALESCE(?, scope)
|
|
351
394
|
WHERE id = ?
|
|
352
|
-
|
|
353
|
-
|
|
395
|
+
`,
|
|
396
|
+
).run(
|
|
397
|
+
type,
|
|
398
|
+
safe.title,
|
|
399
|
+
safe.narrative,
|
|
400
|
+
safe.concepts,
|
|
401
|
+
safe.facts,
|
|
402
|
+
safe.text,
|
|
403
|
+
importance,
|
|
404
|
+
safe.lesson_learned,
|
|
405
|
+
safe.search_aliases,
|
|
406
|
+
minhashSig,
|
|
407
|
+
Date.now(),
|
|
354
408
|
// COALESCE (mirrors the hook-llm upgrade path): a re-enrich that omits
|
|
355
409
|
// scope, or emits an off-enum value, must never blank an existing label —
|
|
356
410
|
// and THIS update stamps optimized_at, so the loss would be permanent.
|
|
357
|
-
normalizeScope(parsed.scope),
|
|
411
|
+
normalizeScope(parsed.scope),
|
|
412
|
+
cand.id,
|
|
413
|
+
);
|
|
358
414
|
|
|
359
|
-
rebuildVector(db, cand.id, {
|
|
415
|
+
rebuildVector(db, cand.id, {
|
|
416
|
+
title,
|
|
417
|
+
narrative,
|
|
418
|
+
concepts: conceptsText,
|
|
419
|
+
lesson_learned: safe.lesson_learned,
|
|
420
|
+
search_aliases: safe.search_aliases,
|
|
421
|
+
});
|
|
360
422
|
|
|
361
423
|
processed++;
|
|
362
424
|
} catch (e) {
|
|
@@ -441,9 +503,12 @@ Rules:
|
|
|
441
503
|
- Include CJK ↔ English equivalents if present
|
|
442
504
|
- Skip terms that have no synonyms in the list`;
|
|
443
505
|
|
|
444
|
-
const parsed = await callModelJSONAsync(prompt, 'sonnet', {
|
|
506
|
+
const parsed = await callModelJSONAsync(prompt, 'sonnet', {
|
|
507
|
+
timeout: BG_LLM_TIMEOUT_MS,
|
|
508
|
+
maxTokens: 1000,
|
|
509
|
+
});
|
|
445
510
|
if (!parsed?.groups || !Array.isArray(parsed.groups)) return [];
|
|
446
|
-
return parsed.groups.filter(g => g.canonical && Array.isArray(g.aliases) && g.aliases.length > 0);
|
|
511
|
+
return parsed.groups.filter((g) => g.canonical && Array.isArray(g.aliases) && g.aliases.length > 0);
|
|
447
512
|
} catch (e) {
|
|
448
513
|
debugCatch(e, 'normalize-identify');
|
|
449
514
|
return [];
|
|
@@ -467,12 +532,16 @@ export function applyNormalization(db, groups, { project = null } = {}) {
|
|
|
467
532
|
// concepts/search_aliases of EVERY project's observations — the exact cross-project
|
|
468
533
|
// contamination the --project flag was added to prevent. NULL → all projects (legacy
|
|
469
534
|
// unscoped run), matching the search-engine `(? IS NULL OR project = ?)` idiom.
|
|
470
|
-
const rows = db
|
|
535
|
+
const rows = db
|
|
536
|
+
.prepare(
|
|
537
|
+
`
|
|
471
538
|
SELECT id, title, narrative, concepts, search_aliases, lesson_learned FROM observations
|
|
472
539
|
WHERE COALESCE(compressed_into, 0) = 0
|
|
473
540
|
AND concepts IS NOT NULL AND concepts != ''
|
|
474
541
|
AND (? IS NULL OR project = ?)
|
|
475
|
-
|
|
542
|
+
`,
|
|
543
|
+
)
|
|
544
|
+
.all(project, project);
|
|
476
545
|
|
|
477
546
|
let updated = 0;
|
|
478
547
|
const updateStmt = db.prepare(`
|
|
@@ -482,16 +551,21 @@ export function applyNormalization(db, groups, { project = null } = {}) {
|
|
|
482
551
|
for (const row of rows) {
|
|
483
552
|
const terms = row.concepts.split(/\s+/);
|
|
484
553
|
let changed = false;
|
|
485
|
-
const newTerms = terms.map(t => {
|
|
554
|
+
const newTerms = terms.map((t) => {
|
|
486
555
|
const canonical = aliasMap.get(t.toLowerCase());
|
|
487
|
-
if (canonical && canonical !== t) {
|
|
556
|
+
if (canonical && canonical !== t) {
|
|
557
|
+
changed = true;
|
|
558
|
+
return canonical;
|
|
559
|
+
}
|
|
488
560
|
return t;
|
|
489
561
|
});
|
|
490
562
|
|
|
491
563
|
if (changed) {
|
|
492
564
|
const uniqueConcepts = [...new Set(newTerms)].join(' ');
|
|
493
565
|
const existingAliases = row.search_aliases || '';
|
|
494
|
-
const originalTerms = terms.filter(
|
|
566
|
+
const originalTerms = terms.filter(
|
|
567
|
+
(t) => aliasMap.has(t.toLowerCase()) && aliasMap.get(t.toLowerCase()) !== t,
|
|
568
|
+
);
|
|
495
569
|
const newAliases = [existingAliases, ...originalTerms].filter(Boolean).join(' ');
|
|
496
570
|
// Defense-in-depth scrub. Canonical concept names come from LLM output
|
|
497
571
|
// (identifySynonymGroups via Sonnet); existing values are already
|
|
@@ -503,7 +577,13 @@ export function applyNormalization(db, groups, { project = null } = {}) {
|
|
|
503
577
|
updateStmt.run(safe.concepts, safe.search_aliases, Date.now(), row.id);
|
|
504
578
|
// V-F3: normalize mutated concepts + search_aliases (both vector fields) — rebuild the
|
|
505
579
|
// vector so it reflects the canonicalized terms (no-op when the vector arm is disabled).
|
|
506
|
-
rebuildVector(db, row.id, {
|
|
580
|
+
rebuildVector(db, row.id, {
|
|
581
|
+
title: row.title,
|
|
582
|
+
narrative: row.narrative,
|
|
583
|
+
concepts: safe.concepts,
|
|
584
|
+
search_aliases: safe.search_aliases,
|
|
585
|
+
lesson_learned: row.lesson_learned,
|
|
586
|
+
});
|
|
507
587
|
updated++;
|
|
508
588
|
}
|
|
509
589
|
}
|
|
@@ -526,7 +606,13 @@ export async function executeNormalize(db, force = false, { project } = {}) {
|
|
|
526
606
|
// Only the UNSCOPED (whole-store) run advances the shared 7-day gate. A project-scoped run
|
|
527
607
|
// must not reset the global timer (it never consulted it — shouldRunNormalize(project) is
|
|
528
608
|
// always open), or one `--project X` run would silently block the next global normalize.
|
|
529
|
-
if (!project) {
|
|
609
|
+
if (!project) {
|
|
610
|
+
try {
|
|
611
|
+
writeFileSync(NORMALIZE_GATE_FILE, JSON.stringify({ epoch: Date.now() }));
|
|
612
|
+
} catch {
|
|
613
|
+
/* best-effort */
|
|
614
|
+
}
|
|
615
|
+
}
|
|
530
616
|
|
|
531
617
|
return { processed: result.updated, groups: groups.length };
|
|
532
618
|
}
|
|
@@ -596,9 +682,12 @@ export async function executeMergeCluster(db, cluster) {
|
|
|
596
682
|
if (!gotSlot) return { merged: false };
|
|
597
683
|
|
|
598
684
|
try {
|
|
599
|
-
const obsDescriptions = cluster
|
|
600
|
-
|
|
601
|
-
|
|
685
|
+
const obsDescriptions = cluster
|
|
686
|
+
.map(
|
|
687
|
+
(o, i) =>
|
|
688
|
+
`${i + 1}. [${o.type || 'change'}] "${truncate(o.title, 200)}" — ${truncate(o.narrative || '(no narrative)', 500)}`,
|
|
689
|
+
)
|
|
690
|
+
.join('\n');
|
|
602
691
|
|
|
603
692
|
const prompt = `These observations from a code memory database may be about the same topic. Should they be merged into a single observation?
|
|
604
693
|
|
|
@@ -609,18 +698,22 @@ Return ONLY valid JSON:
|
|
|
609
698
|
- If they should NOT be merged: {"should_merge":false}
|
|
610
699
|
- If they SHOULD be merged: {"should_merge":true,"merged_title":"≤120 char comprehensive title","merged_narrative":"comprehensive ≤800 char summary preserving all key details","merged_concepts":["kw1","kw2"],"merged_facts":["specific fact 1"],"merged_lesson":"synthesized non-obvious lesson or null","importance":2}`;
|
|
611
700
|
|
|
612
|
-
const parsed = await callModelJSONAsync(prompt, 'sonnet', {
|
|
701
|
+
const parsed = await callModelJSONAsync(prompt, 'sonnet', {
|
|
702
|
+
timeout: BG_LLM_TIMEOUT_MS,
|
|
703
|
+
maxTokens: 1000,
|
|
704
|
+
});
|
|
613
705
|
if (!parsed || !parsed.should_merge) return { merged: false };
|
|
614
706
|
|
|
615
707
|
// Keeper = highest importance, then highest access_count. Previously access_count
|
|
616
708
|
// alone, so a critical (importance=3) but never-accessed observation lost the keeper
|
|
617
709
|
// role to a trivial (importance=1) accessed one and was compressed away.
|
|
618
710
|
const keeper = cluster.reduce((best, o) => {
|
|
619
|
-
const oi = o.importance || 1,
|
|
711
|
+
const oi = o.importance || 1,
|
|
712
|
+
bi = best.importance || 1;
|
|
620
713
|
if (oi !== bi) return oi > bi ? o : best;
|
|
621
714
|
return (o.access_count || 0) > (best.access_count || 0) ? o : best;
|
|
622
715
|
}, cluster[0]);
|
|
623
|
-
const others = cluster.filter(o => o.id !== keeper.id);
|
|
716
|
+
const others = cluster.filter((o) => o.id !== keeper.id);
|
|
624
717
|
// Floor the merged importance at the cluster max — merging must never silently
|
|
625
718
|
// downgrade the ranking of the most-important member (the LLM default is 2). The keeper
|
|
626
719
|
// is selected by importance-first, so keeper.importance IS the cluster max by construction.
|
|
@@ -631,8 +724,8 @@ Return ONLY valid JSON:
|
|
|
631
724
|
// Preserve-on-empty (mirror the merged_lesson guard below + the re-enrich path): the merge
|
|
632
725
|
// overwrites the keeper in place, so a partial LLM response that omits these must fall back to
|
|
633
726
|
// the keeper's own values, not blank its live concepts/facts (findMergeCandidates now selects them).
|
|
634
|
-
const conceptsText = concepts.length ? concepts.join(' ') :
|
|
635
|
-
const factsText = facts.length ? facts.join(' ') :
|
|
727
|
+
const conceptsText = concepts.length ? concepts.join(' ') : keeper.concepts || '';
|
|
728
|
+
const factsText = facts.length ? facts.join(' ') : keeper.facts || '';
|
|
636
729
|
// Scrub BEFORE truncate (see re-enrich note): keep the boundary cut on
|
|
637
730
|
// already-scrubbed text so a straddling secret can't leak a sub-floor head.
|
|
638
731
|
const title = truncate(scrubSecrets(parsed.merged_title || ''), 120);
|
|
@@ -647,13 +740,16 @@ Return ONLY valid JSON:
|
|
|
647
740
|
// (a tombstoned/retired lesson can't resurrect onto the keeper). The union is scrubbed then
|
|
648
741
|
// capped at 500 chars like a single lesson, so an unusually long union may truncate trailing
|
|
649
742
|
// members — still strictly better than the prior unconditional null (partial > total loss).
|
|
650
|
-
let lessonLearned =
|
|
651
|
-
&& parsed.merged_lesson.trim().length > 0
|
|
652
|
-
|
|
743
|
+
let lessonLearned =
|
|
744
|
+
typeof parsed.merged_lesson === 'string' && parsed.merged_lesson.trim().length > 0
|
|
745
|
+
? scrubSecrets(parsed.merged_lesson).slice(0, 500)
|
|
746
|
+
: null;
|
|
653
747
|
if (!lessonLearned) {
|
|
654
|
-
const memberLessons = [
|
|
655
|
-
|
|
656
|
-
|
|
748
|
+
const memberLessons = [
|
|
749
|
+
...new Set(
|
|
750
|
+
cluster.map((o) => (o.lesson_learned || '').trim()).filter((l) => l && l.toLowerCase() !== 'none'),
|
|
751
|
+
),
|
|
752
|
+
];
|
|
657
753
|
if (memberLessons.length) lessonLearned = scrubSecrets(memberLessons.join(' — ')).slice(0, 500);
|
|
658
754
|
}
|
|
659
755
|
|
|
@@ -665,7 +761,8 @@ Return ONLY valid JSON:
|
|
|
665
761
|
// Scrub LLM-output cluster-merge text fields at the UPDATE boundary.
|
|
666
762
|
// importance is numeric; minhash_sig is hash bytes.
|
|
667
763
|
const safe = scrubRecord('observations', {
|
|
668
|
-
title,
|
|
764
|
+
title,
|
|
765
|
+
narrative,
|
|
669
766
|
concepts: conceptsText,
|
|
670
767
|
facts: factsText,
|
|
671
768
|
text: textField,
|
|
@@ -679,9 +776,9 @@ Return ONLY valid JSON:
|
|
|
679
776
|
// the other members at a tombstoned keeper is precisely the "buried behind a hidden
|
|
680
777
|
// parent" loss that mergeDuplicates' docblock enumerates (lib/maintain-core.mjs), and
|
|
681
778
|
// it is what that function's own isLive gate exists to prevent. Same predicate here.
|
|
682
|
-
const keeperLive = db
|
|
683
|
-
`SELECT 1 FROM observations WHERE id = ? AND ${liveObsFilterSql('')}`
|
|
684
|
-
|
|
779
|
+
const keeperLive = db
|
|
780
|
+
.prepare(`SELECT 1 FROM observations WHERE id = ? AND ${liveObsFilterSql('')}`)
|
|
781
|
+
.get(keeper.id);
|
|
685
782
|
if (!keeperLive) return false;
|
|
686
783
|
|
|
687
784
|
// Snapshot the keeper's pre-merge row BEFORE overwriting it, so its original
|
|
@@ -691,27 +788,43 @@ Return ONLY valid JSON:
|
|
|
691
788
|
// would otherwise destroy its original text irreversibly (HIGH-3 data loss).
|
|
692
789
|
// Column list is derived from the live schema (minus id/compressed_into) so
|
|
693
790
|
// it stays correct as migrations add columns; names are internal identifiers.
|
|
694
|
-
const snapCols = db
|
|
695
|
-
.
|
|
791
|
+
const snapCols = db
|
|
792
|
+
.prepare(`PRAGMA table_info(observations)`)
|
|
793
|
+
.all()
|
|
794
|
+
.map((c) => c.name)
|
|
795
|
+
.filter((c) => c !== 'id' && c !== 'compressed_into');
|
|
696
796
|
const snapColList = snapCols.join(', ');
|
|
697
797
|
db.prepare(
|
|
698
798
|
`INSERT INTO observations (${snapColList}, compressed_into)
|
|
699
|
-
SELECT ${snapColList}, ? FROM observations WHERE id =
|
|
799
|
+
SELECT ${snapColList}, ? FROM observations WHERE id = ?`,
|
|
700
800
|
).run(keeper.id, keeper.id);
|
|
701
801
|
|
|
702
|
-
db.prepare(
|
|
802
|
+
db.prepare(
|
|
803
|
+
`
|
|
703
804
|
UPDATE observations SET title=?, narrative=?, concepts=?, facts=?, text=?,
|
|
704
805
|
importance=?, lesson_learned=?, minhash_sig=?, optimized_at=?
|
|
705
806
|
WHERE id = ?
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
807
|
+
`,
|
|
808
|
+
).run(
|
|
809
|
+
safe.title,
|
|
810
|
+
safe.narrative,
|
|
811
|
+
safe.concepts,
|
|
812
|
+
safe.facts,
|
|
813
|
+
safe.text,
|
|
814
|
+
importance,
|
|
815
|
+
safe.lesson_learned,
|
|
816
|
+
minhashSig,
|
|
817
|
+
Date.now(),
|
|
818
|
+
keeper.id,
|
|
819
|
+
);
|
|
820
|
+
|
|
821
|
+
const otherIds = others.map((o) => o.id);
|
|
710
822
|
const ph = otherIds.map(() => '?').join(',');
|
|
711
823
|
// Live guard on the members too: one already compressed into ANOTHER summary S during
|
|
712
824
|
// the LLM window would be re-pointed here, silently dropping a row out of S's child set.
|
|
713
|
-
db.prepare(
|
|
714
|
-
|
|
825
|
+
db.prepare(
|
|
826
|
+
`UPDATE observations SET compressed_into = ? WHERE id IN (${ph}) AND ${liveObsFilterSql('')}`,
|
|
827
|
+
).run(keeper.id, ...otherIds);
|
|
715
828
|
return true;
|
|
716
829
|
})();
|
|
717
830
|
if (!mergeApplied) {
|
|
@@ -719,7 +832,13 @@ Return ONLY valid JSON:
|
|
|
719
832
|
return { merged: false };
|
|
720
833
|
}
|
|
721
834
|
|
|
722
|
-
rebuildVector(db, keeper.id, {
|
|
835
|
+
rebuildVector(db, keeper.id, {
|
|
836
|
+
title,
|
|
837
|
+
narrative,
|
|
838
|
+
concepts: conceptsText,
|
|
839
|
+
lesson_learned: lessonLearned,
|
|
840
|
+
search_aliases: keeper.search_aliases,
|
|
841
|
+
});
|
|
723
842
|
|
|
724
843
|
debugLog('DEBUG', 'llm-optimize', `merged ${cluster.length} observations into #${keeper.id}`);
|
|
725
844
|
return { merged: true, keeperId: keeper.id, mergedCount: others.length };
|
|
@@ -792,10 +911,12 @@ export function clusterForCompression(candidates, db) {
|
|
|
792
911
|
if (obs.length < 3) continue;
|
|
793
912
|
|
|
794
913
|
let vocab;
|
|
795
|
-
try {
|
|
914
|
+
try {
|
|
915
|
+
vocab = getVocabulary(db);
|
|
916
|
+
} catch {}
|
|
796
917
|
|
|
797
918
|
if (vocab) {
|
|
798
|
-
const vectors = obs.map(o => {
|
|
919
|
+
const vectors = obs.map((o) => {
|
|
799
920
|
const text = [o.title || '', o.narrative || ''].join(' ');
|
|
800
921
|
return computeVector(text, vocab);
|
|
801
922
|
});
|
|
@@ -816,7 +937,7 @@ export function clusterForCompression(candidates, db) {
|
|
|
816
937
|
}
|
|
817
938
|
|
|
818
939
|
if (cluster.length >= 3) {
|
|
819
|
-
const sorted = cluster.map(c => c.obs).sort((a, b) => a.created_at_epoch - b.created_at_epoch);
|
|
940
|
+
const sorted = cluster.map((c) => c.obs).sort((a, b) => a.created_at_epoch - b.created_at_epoch);
|
|
820
941
|
let subCluster = [sorted[0]];
|
|
821
942
|
for (let k = 1; k < sorted.length; k++) {
|
|
822
943
|
if (sorted[k].created_at_epoch - subCluster[0].created_at_epoch > COMPRESS_TIME_SPLIT_MS) {
|
|
@@ -855,9 +976,12 @@ export async function executeSmartCompressCluster(db, observations, project) {
|
|
|
855
976
|
if (!gotSlot) return { compressed: false };
|
|
856
977
|
|
|
857
978
|
try {
|
|
858
|
-
const obsDescriptions = observations
|
|
859
|
-
|
|
860
|
-
|
|
979
|
+
const obsDescriptions = observations
|
|
980
|
+
.map(
|
|
981
|
+
(o, i) =>
|
|
982
|
+
`${i + 1}. [${o.type || 'change'}] "${truncate(o.title || '(untitled)', 200)}" — ${truncate(o.narrative || '(no narrative)', 500)}${o.lesson_learned ? ` | Lesson: ${truncate(o.lesson_learned, 200)}` : ''}`,
|
|
983
|
+
)
|
|
984
|
+
.join('\n');
|
|
861
985
|
|
|
862
986
|
const prompt = `Summarize these related code memory observations into ONE comprehensive summary. Preserve all important decisions, lessons, and specific facts. Return ONLY valid JSON.
|
|
863
987
|
|
|
@@ -866,7 +990,10 @@ ${obsDescriptions}
|
|
|
866
990
|
|
|
867
991
|
JSON: {"title":"descriptive summary ≤120 chars","narrative":"comprehensive summary ≤800 chars preserving key decisions and lessons","concepts":["kw1","kw2"],"facts":["all specific facts preserved"],"lesson_learned":"most important synthesized lesson or 'none'","search_aliases":["alt search 1","alt search 2"]}`;
|
|
868
992
|
|
|
869
|
-
const parsed = await callModelJSONAsync(prompt, 'sonnet', {
|
|
993
|
+
const parsed = await callModelJSONAsync(prompt, 'sonnet', {
|
|
994
|
+
timeout: BG_LLM_TIMEOUT_MS,
|
|
995
|
+
maxTokens: 1000,
|
|
996
|
+
});
|
|
870
997
|
if (!parsed || !parsed.title) return { compressed: false };
|
|
871
998
|
|
|
872
999
|
// Scrub BEFORE truncate (see re-enrich note): boundary cut on scrubbed text.
|
|
@@ -876,25 +1003,29 @@ JSON: {"title":"descriptive summary ≤120 chars","narrative":"comprehensive sum
|
|
|
876
1003
|
const facts = Array.isArray(parsed.facts) ? parsed.facts.slice(0, 10) : [];
|
|
877
1004
|
const conceptsText = concepts.join(' ');
|
|
878
1005
|
const factsText = facts.join(' ');
|
|
879
|
-
const lessonLearned =
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
1006
|
+
const lessonLearned =
|
|
1007
|
+
typeof parsed.lesson_learned === 'string' &&
|
|
1008
|
+
parsed.lesson_learned.toLowerCase() !== 'none' &&
|
|
1009
|
+
parsed.lesson_learned.trim().length > 0
|
|
1010
|
+
? scrubSecrets(parsed.lesson_learned).slice(0, 500)
|
|
1011
|
+
: null;
|
|
883
1012
|
const searchAliases = Array.isArray(parsed.search_aliases)
|
|
884
|
-
? parsed.search_aliases.slice(0, 6).join(' ')
|
|
1013
|
+
? parsed.search_aliases.slice(0, 6).join(' ')
|
|
1014
|
+
: null;
|
|
885
1015
|
|
|
886
1016
|
const bigramText = cjkBigrams((title || '') + ' ' + (narrative || ''));
|
|
887
1017
|
const textField = [conceptsText, factsText, searchAliases || '', bigramText].filter(Boolean).join(' ');
|
|
888
1018
|
|
|
889
|
-
const epochs = observations.map(o => o.created_at_epoch).sort((a, b) => a - b);
|
|
1019
|
+
const epochs = observations.map((o) => o.created_at_epoch).sort((a, b) => a - b);
|
|
890
1020
|
const medianEpoch = epochs[Math.floor(epochs.length / 2)];
|
|
891
1021
|
|
|
892
1022
|
const summaryId = db.transaction(() => {
|
|
893
1023
|
const sessionId = `compress-${project}`;
|
|
894
1024
|
const now = new Date();
|
|
895
|
-
db.prepare(
|
|
1025
|
+
db.prepare(
|
|
1026
|
+
`INSERT OR IGNORE INTO sdk_sessions
|
|
896
1027
|
(content_session_id, memory_session_id, project, started_at, started_at_epoch, status)
|
|
897
|
-
VALUES (?,?,?,?,?,'active')
|
|
1028
|
+
VALUES (?,?,?,?,?,'active')`,
|
|
898
1029
|
).run(sessionId, sessionId, project, now.toISOString(), now.getTime());
|
|
899
1030
|
|
|
900
1031
|
// Defense-in-depth: title/narrative/etc. are LLM-generated compression
|
|
@@ -908,32 +1039,52 @@ JSON: {"title":"descriptive summary ≤120 chars","narrative":"comprehensive sum
|
|
|
908
1039
|
lesson_learned: lessonLearned,
|
|
909
1040
|
search_aliases: searchAliases,
|
|
910
1041
|
});
|
|
911
|
-
const result = db
|
|
1042
|
+
const result = db
|
|
1043
|
+
.prepare(
|
|
1044
|
+
`INSERT INTO observations
|
|
912
1045
|
(memory_session_id, project, text, type, title, subtitle, narrative, concepts, facts,
|
|
913
1046
|
files_read, files_modified, importance, lesson_learned, search_aliases, optimized_at,
|
|
914
1047
|
created_at, created_at_epoch)
|
|
915
|
-
VALUES (?,?,?,?,?,'',?,?,?,'[]','[]',2,?,?,?,?,?)
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
1048
|
+
VALUES (?,?,?,?,?,'',?,?,?,'[]','[]',2,?,?,?,?,?)`,
|
|
1049
|
+
)
|
|
1050
|
+
.run(
|
|
1051
|
+
sessionId,
|
|
1052
|
+
project,
|
|
1053
|
+
safe.text,
|
|
1054
|
+
'discovery',
|
|
1055
|
+
safe.title,
|
|
1056
|
+
safe.narrative,
|
|
1057
|
+
safe.concepts,
|
|
1058
|
+
safe.facts,
|
|
1059
|
+
safe.lesson_learned,
|
|
1060
|
+
safe.search_aliases,
|
|
1061
|
+
Date.now(),
|
|
1062
|
+
new Date(medianEpoch).toISOString(),
|
|
1063
|
+
medianEpoch,
|
|
1064
|
+
);
|
|
919
1065
|
|
|
920
1066
|
const sId = Number(result.lastInsertRowid);
|
|
921
1067
|
|
|
922
|
-
const obsIds = observations.map(o => o.id);
|
|
1068
|
+
const obsIds = observations.map((o) => o.id);
|
|
923
1069
|
const ph = obsIds.map(() => '?').join(',');
|
|
924
1070
|
// Live guard (audit 2026-09-02 P0-3): the candidate SELECT is separated from this write
|
|
925
1071
|
// by a Sonnet round-trip, so a member may already be compressed into another summary or
|
|
926
1072
|
// tombstoned. Re-pointing it here would silently remove a row from that summary's child
|
|
927
1073
|
// set. Members that lost liveness stay where they are; the summary still lands.
|
|
928
|
-
db.prepare(
|
|
929
|
-
|
|
1074
|
+
db.prepare(
|
|
1075
|
+
`UPDATE observations SET compressed_into = ? WHERE id IN (${ph}) AND ${liveObsFilterSql('')}`,
|
|
1076
|
+
).run(sId, ...obsIds);
|
|
930
1077
|
|
|
931
1078
|
return sId;
|
|
932
1079
|
})();
|
|
933
1080
|
|
|
934
1081
|
rebuildVector(db, summaryId, { title, narrative, concepts: conceptsText });
|
|
935
1082
|
|
|
936
|
-
debugLog(
|
|
1083
|
+
debugLog(
|
|
1084
|
+
'DEBUG',
|
|
1085
|
+
'llm-optimize',
|
|
1086
|
+
`smart-compressed ${observations.length} observations into #${summaryId}`,
|
|
1087
|
+
);
|
|
937
1088
|
return { compressed: true, summaryId, count: observations.length };
|
|
938
1089
|
} catch (e) {
|
|
939
1090
|
debugCatch(e, 'smart-compress');
|
|
@@ -1043,14 +1194,18 @@ export function optimizePreview(db, { project, detail = false } = {}) {
|
|
|
1043
1194
|
* @param {string} [opts.project] Filter all tasks to a single project. Opt-in;
|
|
1044
1195
|
* absence preserves the prior all-projects default.
|
|
1045
1196
|
*/
|
|
1046
|
-
export async function optimizeRun(
|
|
1197
|
+
export async function optimizeRun(
|
|
1198
|
+
db,
|
|
1199
|
+
{ tasks, maxItems = 15, force = false, reenrichScope = 'narrow', project } = {},
|
|
1200
|
+
) {
|
|
1047
1201
|
const allTasks = ['re-enrich', 'normalize', 'cluster-merge', 'smart-compress'];
|
|
1048
1202
|
const selectedTasks = tasks && tasks.length > 0 ? tasks : allTasks;
|
|
1049
1203
|
// Single-task mode: give that task the full budget. Distribution only makes sense
|
|
1050
1204
|
// when multiple tasks compete for the same pool.
|
|
1051
|
-
const budget =
|
|
1052
|
-
|
|
1053
|
-
|
|
1205
|
+
const budget =
|
|
1206
|
+
selectedTasks.length === 1
|
|
1207
|
+
? { reenrich: maxItems, normalize: maxItems, clusterMerge: maxItems, smartCompress: maxItems }
|
|
1208
|
+
: distributeBudget(maxItems);
|
|
1054
1209
|
const results = {};
|
|
1055
1210
|
|
|
1056
1211
|
for (const task of selectedTasks) {
|
|
@@ -1088,18 +1243,26 @@ export async function optimizeRun(db, { tasks, maxItems = 15, force = false, ree
|
|
|
1088
1243
|
// Cap is budget.reenrich, so the daily pass adds at most that many cheap
|
|
1089
1244
|
// classification calls and an empty pool still costs nothing.
|
|
1090
1245
|
const half = Math.max(1, Math.floor(budget.reenrich / 2));
|
|
1091
|
-
const aliasBudget = Math.min(
|
|
1246
|
+
const aliasBudget = Math.min(
|
|
1247
|
+
half,
|
|
1248
|
+
findReenrichCandidates(db, half, { scope: 'aliases', project }).length,
|
|
1249
|
+
);
|
|
1092
1250
|
const scopesBudget = Math.min(
|
|
1093
1251
|
budget.reenrich,
|
|
1094
1252
|
findReenrichCandidates(db, budget.reenrich, { scope: 'scopes', project }).length,
|
|
1095
1253
|
);
|
|
1096
|
-
const mainRes = await executeReenrich(db, budget.reenrich - aliasBudget, {
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
const
|
|
1101
|
-
|
|
1102
|
-
|
|
1254
|
+
const mainRes = await executeReenrich(db, budget.reenrich - aliasBudget, {
|
|
1255
|
+
scope: reenrichScope,
|
|
1256
|
+
project,
|
|
1257
|
+
});
|
|
1258
|
+
const aliasRes =
|
|
1259
|
+
aliasBudget > 0
|
|
1260
|
+
? await executeReenrich(db, aliasBudget, { scope: 'aliases', project })
|
|
1261
|
+
: { processed: 0, skipped: 0 };
|
|
1262
|
+
const scopesRes =
|
|
1263
|
+
scopesBudget > 0
|
|
1264
|
+
? await executeReenrich(db, scopesBudget, { scope: 'scopes', project })
|
|
1265
|
+
: { processed: 0, skipped: 0 };
|
|
1103
1266
|
results.reenrich = {
|
|
1104
1267
|
processed: (mainRes.processed || 0) + (aliasRes.processed || 0) + (scopesRes.processed || 0),
|
|
1105
1268
|
skipped: (mainRes.skipped || 0) + (aliasRes.skipped || 0) + (scopesRes.skipped || 0),
|