akm-cli 0.9.0-beta.57 → 0.9.0-beta.59
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/assets/prompts/extract-session.md +5 -1
- package/dist/cli/config-migrate.js +7 -1
- package/dist/commands/config-cli.js +8 -11
- package/dist/commands/health/stash-exposure.js +46 -0
- package/dist/commands/health/windows.js +6 -7
- package/dist/commands/health.js +31 -10
- package/dist/commands/improve/collapse-detector.js +2 -1
- package/dist/commands/improve/consolidate/eligibility.js +0 -17
- package/dist/commands/improve/consolidate.js +209 -167
- package/dist/commands/improve/distill/promote-memory.js +4 -3
- package/dist/commands/improve/distill/quality-gate.js +7 -4
- package/dist/commands/improve/distill-promotion-policy.js +826 -167
- package/dist/commands/improve/distill.js +26 -12
- package/dist/commands/improve/extract-prompt.js +16 -2
- package/dist/commands/improve/extract.js +16 -8
- package/dist/commands/improve/improve-auto-accept.js +22 -1
- package/dist/commands/improve/loop-stages.js +7 -2
- package/dist/commands/improve/memory/memory-belief.js +14 -15
- package/dist/commands/improve/memory/memory-contradiction-detect.js +60 -32
- package/dist/commands/improve/memory/memory-improve.js +27 -27
- package/dist/commands/improve/preparation.js +6 -5
- package/dist/commands/improve/procedural.js +1 -0
- package/dist/commands/improve/recombine.js +3 -11
- package/dist/commands/improve/reflect-noise.js +1 -1
- package/dist/commands/improve/reflect.js +4 -3
- package/dist/commands/improve/shared.js +9 -6
- package/dist/commands/proposal/drain-policies.js +4 -2
- package/dist/commands/read/remember-cli.js +1 -1
- package/dist/commands/read/show.js +15 -0
- package/dist/commands/remember.js +11 -12
- package/dist/commands/sources/init.js +5 -1
- package/dist/commands/sources/stash-skeleton.js +34 -0
- package/dist/commands/tasks/default-tasks.js +3 -2
- package/dist/core/asset/frontmatter.js +22 -0
- package/dist/core/common.js +1 -15
- package/dist/core/config/config-io.js +10 -1
- package/dist/core/config/config-migration.js +2 -15
- package/dist/core/config/config-schema.js +15 -3
- package/dist/core/config/config.js +22 -14
- package/dist/core/paths.js +4 -4
- package/dist/core/time.js +53 -0
- package/dist/indexer/db/db.js +51 -46
- package/dist/indexer/graph/graph-extraction.js +1 -13
- package/dist/indexer/indexer.js +77 -65
- package/dist/indexer/search/db-search.js +41 -6
- package/dist/indexer/search/ranking-contributors.js +14 -8
- package/dist/indexer/search/search-source.js +15 -3
- package/dist/llm/feature-gate.js +4 -8
- package/dist/output/renderers.js +4 -0
- package/dist/scripts/migrate-storage.js +83 -59
- package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +6 -0
- package/dist/storage/repositories/registry-cache.js +2 -1
- package/dist/storage/repositories/registry-index-cache-repository.js +46 -0
- package/dist/workflows/runtime/runs.js +6 -1
- package/package.json +1 -1
- package/dist/assets/tasks/core/update-stashes.yml +0 -4
package/dist/indexer/db/db.js
CHANGED
|
@@ -257,6 +257,49 @@ export function getDerivedForParent(db, parentRef) {
|
|
|
257
257
|
return null;
|
|
258
258
|
}
|
|
259
259
|
}
|
|
260
|
+
/**
|
|
261
|
+
* 03-R3: for the given derived-twin row ids, fetch each twin's BASE memory
|
|
262
|
+
* `beliefState`, keyed by twin id.
|
|
263
|
+
*
|
|
264
|
+
* Used by the derived-twin belief inheritance in search ranking: a `.derived`
|
|
265
|
+
* twin has no belief state of its own, so it inherits its base memory's
|
|
266
|
+
* demoting state (contradicted/superseded/…) at search time. A twin's
|
|
267
|
+
* `entry_key` is exactly its base's `entry_key` plus the `.derived` suffix
|
|
268
|
+
* (same stash + type prefix, `<name>` vs `<name>.derived`), so the base is
|
|
269
|
+
* found by stripping that suffix — no ref/prefix reconstruction. Returns a map
|
|
270
|
+
* of twin id → base beliefState for bases that carry a non-empty state.
|
|
271
|
+
* Best-effort: any query error (e.g. legacy DB) yields no inheritance rather
|
|
272
|
+
* than failing the search.
|
|
273
|
+
*/
|
|
274
|
+
export function getBaseBeliefStatesForDerivedTwins(db, twinIds) {
|
|
275
|
+
const out = new Map();
|
|
276
|
+
if (twinIds.length === 0)
|
|
277
|
+
return out;
|
|
278
|
+
// Chunk at SQLITE_CHUNK_SIZE like the sibling bulk-by-id helpers, so a large
|
|
279
|
+
// `--limit` candidate set never trips SQLITE_MAX_VARIABLE_NUMBER (which would
|
|
280
|
+
// otherwise fall into the best-effort catch and silently disable the feature).
|
|
281
|
+
for (let i = 0; i < twinIds.length; i += SQLITE_CHUNK_SIZE) {
|
|
282
|
+
const chunk = twinIds.slice(i, i + SQLITE_CHUNK_SIZE);
|
|
283
|
+
const placeholders = chunk.map(() => "?").join(",");
|
|
284
|
+
bestEffort(() => {
|
|
285
|
+
const rows = db
|
|
286
|
+
.prepare(`SELECT twin.id AS twin_id, json_extract(base.entry_json, '$.beliefState') AS belief
|
|
287
|
+
FROM entries twin
|
|
288
|
+
JOIN entries base
|
|
289
|
+
ON base.entry_type = 'memory'
|
|
290
|
+
AND base.entry_key = substr(twin.entry_key, 1, length(twin.entry_key) - length('.derived'))
|
|
291
|
+
WHERE twin.id IN (${placeholders})
|
|
292
|
+
AND twin.entry_key LIKE '%.derived'
|
|
293
|
+
AND json_extract(base.entry_json, '$.beliefState') IS NOT NULL`)
|
|
294
|
+
.all(...chunk);
|
|
295
|
+
for (const r of rows) {
|
|
296
|
+
if (typeof r.belief === "string" && r.belief.trim().length > 0)
|
|
297
|
+
out.set(r.twin_id, r.belief.trim());
|
|
298
|
+
}
|
|
299
|
+
}, "legacy DB / entry_json without beliefState — treat as no twin inheritance");
|
|
300
|
+
}
|
|
301
|
+
return out;
|
|
302
|
+
}
|
|
260
303
|
/**
|
|
261
304
|
* Phase 2A / Rec 5: bulk-load positive feedback event counts for the given
|
|
262
305
|
* entry ids. Used by the utility-decay forgetting curve to stabilize
|
|
@@ -648,7 +691,8 @@ function runFtsQuery(db, ftsQuery, limit, entryType, excludeTypes) {
|
|
|
648
691
|
}
|
|
649
692
|
return results;
|
|
650
693
|
}
|
|
651
|
-
catch {
|
|
694
|
+
catch (err) {
|
|
695
|
+
warn("[db] runFtsQuery failed:", err instanceof Error ? err.message : String(err));
|
|
652
696
|
return [];
|
|
653
697
|
}
|
|
654
698
|
}
|
|
@@ -747,8 +791,7 @@ export function getEntryCount(db) {
|
|
|
747
791
|
return row.cnt;
|
|
748
792
|
}
|
|
749
793
|
export function getEmbeddableEntryCount(db) {
|
|
750
|
-
|
|
751
|
-
return row.cnt;
|
|
794
|
+
return getEntryCount(db);
|
|
752
795
|
}
|
|
753
796
|
export function getEmbeddingCount(db) {
|
|
754
797
|
const row = db.prepare("SELECT COUNT(*) AS cnt FROM embeddings").get();
|
|
@@ -1327,49 +1370,11 @@ export function relinkUsageEvents(db) {
|
|
|
1327
1370
|
}, "usage_events table may not exist yet during entry_id re-resolution");
|
|
1328
1371
|
}
|
|
1329
1372
|
// ── registry_index_cache helpers ─────────────────────────────────────────────
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
* @param indexJson - Serialised registry index document (JSON string).
|
|
1336
|
-
* @param opts.etag - HTTP ETag from the response (optional).
|
|
1337
|
-
* @param opts.lastModified - HTTP Last-Modified from the response (optional).
|
|
1338
|
-
*/
|
|
1339
|
-
export function upsertRegistryIndexCache(db, registryUrl, indexJson, opts) {
|
|
1340
|
-
db.prepare(`
|
|
1341
|
-
INSERT INTO registry_index_cache (registry_url, fetched_at, etag, last_modified, index_json)
|
|
1342
|
-
VALUES (?, ?, ?, ?, ?)
|
|
1343
|
-
ON CONFLICT(registry_url) DO UPDATE SET
|
|
1344
|
-
fetched_at = excluded.fetched_at,
|
|
1345
|
-
etag = excluded.etag,
|
|
1346
|
-
last_modified = excluded.last_modified,
|
|
1347
|
-
index_json = excluded.index_json
|
|
1348
|
-
`).run(registryUrl, new Date().toISOString(), opts?.etag ?? null, opts?.lastModified ?? null, indexJson);
|
|
1349
|
-
}
|
|
1350
|
-
/**
|
|
1351
|
-
* Look up a cached registry index entry from index.db.
|
|
1352
|
-
* Returns undefined when not found or when the entry is older than `maxAgeMs`.
|
|
1353
|
-
*
|
|
1354
|
-
* TTL check: if `Date.now() - new Date(fetched_at).getTime() > maxAgeMs` the
|
|
1355
|
-
* entry is considered a cache miss and undefined is returned.
|
|
1356
|
-
*
|
|
1357
|
-
* @param db - Open index.db connection.
|
|
1358
|
-
* @param registryUrl - Canonical URL of the registry (primary key).
|
|
1359
|
-
* @param maxAgeMs - Maximum age in milliseconds before the entry is stale (default: 1 hour).
|
|
1360
|
-
*/
|
|
1361
|
-
export function getRegistryIndexCache(db, registryUrl, maxAgeMs = 3_600_000 /* 1 hour */) {
|
|
1362
|
-
const row = db
|
|
1363
|
-
.prepare(`SELECT fetched_at, etag, last_modified, index_json
|
|
1364
|
-
FROM registry_index_cache WHERE registry_url = ?`)
|
|
1365
|
-
.get(registryUrl);
|
|
1366
|
-
if (!row)
|
|
1367
|
-
return undefined;
|
|
1368
|
-
const fetchedAt = Date.parse(row.fetched_at);
|
|
1369
|
-
if (Number.isNaN(fetchedAt) || Date.now() - fetchedAt > maxAgeMs)
|
|
1370
|
-
return undefined;
|
|
1371
|
-
return { indexJson: row.index_json, etag: row.etag, lastModified: row.last_modified };
|
|
1372
|
-
}
|
|
1373
|
+
// The raw SQL for the `registry_index_cache` table now lives in the storage
|
|
1374
|
+
// layer (`src/storage/repositories/registry-index-cache-repository.ts`) so the
|
|
1375
|
+
// dependency arrow points indexer → storage. These thin re-exports preserve the
|
|
1376
|
+
// previously-public symbols for any importer of this module.
|
|
1377
|
+
export { getRegistryIndexCache, upsertRegistryIndexCache, } from "../../storage/repositories/registry-index-cache-repository.js";
|
|
1373
1378
|
/**
|
|
1374
1379
|
* Walk indexed entries and collect a deduplicated set of tags. When
|
|
1375
1380
|
* `entryType` is provided, only entries of that type contribute tags.
|
|
@@ -838,14 +838,10 @@ export function rankCandidatesByUtility(db, candidates, _stashRoot) {
|
|
|
838
838
|
*
|
|
839
839
|
* Inferred-child memories (frontmatter `inferred: true`) are skipped — they
|
|
840
840
|
* are already derived summaries, with no additional internal graph structure worth
|
|
841
|
-
* extracting.
|
|
841
|
+
* extracting.
|
|
842
842
|
*
|
|
843
843
|
* Exported for direct unit testing.
|
|
844
844
|
*/
|
|
845
|
-
// #632 — canonical session-capture name shape, mirrored from
|
|
846
|
-
// isSessionCaptureMemoryName (src/commands/improve/consolidate/eligibility.ts).
|
|
847
|
-
// Inlined to avoid an improve-layer import from the indexer layer.
|
|
848
|
-
const SESSION_CAPTURE_NAME_RE = /-(session|checkpoint)-\d{8}/;
|
|
849
845
|
export function collectEligibleFiles(stashRoot, includeTypes = [...DEFAULT_GRAPH_EXTRACTION_INCLUDE_TYPES]) {
|
|
850
846
|
const out = [];
|
|
851
847
|
for (const rawType of includeTypes) {
|
|
@@ -871,14 +867,6 @@ export function collectEligibleFiles(stashRoot, includeTypes = [...DEFAULT_GRAPH
|
|
|
871
867
|
// graph to extract from a single-fact body.
|
|
872
868
|
if (type === "memory" && parsed.data.inferred === true)
|
|
873
869
|
continue;
|
|
874
|
-
// #632 — skip session-capture telemetry checkpoints (named
|
|
875
|
-
// `<harness>-(session|checkpoint)-<YYYYMMDD>-<id>`). Their bodies are
|
|
876
|
-
// pipeline bookkeeping; graph-extracting them lifts metadata fields as
|
|
877
|
-
// entities that then dominate clustering as bland stash-wide buckets.
|
|
878
|
-
// Mirrors isSessionCaptureMemoryName (consolidate/eligibility.ts); inlined
|
|
879
|
-
// here to keep the indexer layer free of an improve-layer import.
|
|
880
|
-
if (type === "memory" && SESSION_CAPTURE_NAME_RE.test(path.basename(filePath, ".md")))
|
|
881
|
-
continue;
|
|
882
870
|
const body = parsed.content.trim();
|
|
883
871
|
if (!body)
|
|
884
872
|
continue;
|
package/dist/indexer/indexer.js
CHANGED
|
@@ -331,7 +331,6 @@ async function akmIndexReal(options) {
|
|
|
331
331
|
walkWarnings: [],
|
|
332
332
|
dirsNeedingLlm: [],
|
|
333
333
|
embeddingResult: null,
|
|
334
|
-
graphExtractionResult: null,
|
|
335
334
|
};
|
|
336
335
|
onProgress({
|
|
337
336
|
phase: "summary",
|
|
@@ -399,14 +398,21 @@ async function akmIndexReal(options) {
|
|
|
399
398
|
}
|
|
400
399
|
});
|
|
401
400
|
}
|
|
402
|
-
|
|
403
|
-
|
|
401
|
+
/**
|
|
402
|
+
* Phase 1 (async): walk every source directory and pre-generate all metadata
|
|
403
|
+
* outside any transaction, producing the per-directory scan records that
|
|
404
|
+
* {@link persistDirRecords} later writes.
|
|
405
|
+
*
|
|
406
|
+
* generateMetadataFlat is async (uses dynamic import for the matcher/renderer
|
|
407
|
+
* registry), so it cannot run inside a db.transaction() callback — hence the
|
|
408
|
+
* split into a pure-ish scan pass and a synchronous persist pass.
|
|
409
|
+
*/
|
|
410
|
+
async function scanSourceDirs(db, allSourceEntries, isIncremental, builtAtMs, hadRemovedSources, onProgress) {
|
|
404
411
|
let scannedDirs = 0;
|
|
405
412
|
let skippedDirs = 0;
|
|
406
413
|
let generatedCount = 0;
|
|
407
414
|
const warnings = [];
|
|
408
415
|
const seenPaths = new Set();
|
|
409
|
-
const dirsNeedingLlm = [];
|
|
410
416
|
const dirRecords = [];
|
|
411
417
|
let processedDirs = 0;
|
|
412
418
|
let priorDirsChanged = hadRemovedSources;
|
|
@@ -426,6 +432,53 @@ async function indexEntries(db, allSourceEntries, isIncremental, builtAtMs, hadR
|
|
|
426
432
|
reportScanProgress(`${kind === "scan" ? "Rescanning" : "Skipping"} ${path.relative(currentStashDir, dirPath) || "."} ` +
|
|
427
433
|
`from ${currentStashDir}: ${reason.kind}${detail}${rowInfo}`);
|
|
428
434
|
};
|
|
435
|
+
// Duplicate-directory guard shared by the wiki-root and normal branches. A
|
|
436
|
+
// dir may surface via multiple stash roots; only the first occurrence is
|
|
437
|
+
// indexed, and the later ones are recorded as skips. Returns true when the
|
|
438
|
+
// dir was already seen (caller should skip further processing).
|
|
439
|
+
const markSeenOrSkipDuplicate = (dirPath, currentStashDir, files) => {
|
|
440
|
+
const resolved = path.resolve(dirPath);
|
|
441
|
+
if (seenPaths.has(resolved)) {
|
|
442
|
+
const reason = { kind: "duplicate-dir" };
|
|
443
|
+
dirRecords.push({ dirPath, currentStashDir, files, stash: null, skip: true, reason });
|
|
444
|
+
reportDirDecision("skip", dirPath, currentStashDir, reason);
|
|
445
|
+
return true;
|
|
446
|
+
}
|
|
447
|
+
seenPaths.add(resolved);
|
|
448
|
+
return false;
|
|
449
|
+
};
|
|
450
|
+
// Incremental freshness gate shared by both branches: consult the persisted
|
|
451
|
+
// dir state and record either a skip (unchanged + eligible for incremental
|
|
452
|
+
// skip) or a scan record carrying the candidate stash.
|
|
453
|
+
const recordFreshnessDecision = (dirPath, currentStashDir, stateFiles, stash) => {
|
|
454
|
+
const previousState = getDirIndexState(db, dirPath, stateFiles, builtAtMs);
|
|
455
|
+
if (isIncremental && !previousState.stale && canUseIncrementalSkip(previousState, priorDirsChanged)) {
|
|
456
|
+
skippedDirs++;
|
|
457
|
+
dirRecords.push({
|
|
458
|
+
dirPath,
|
|
459
|
+
currentStashDir,
|
|
460
|
+
files: stateFiles,
|
|
461
|
+
stash: null,
|
|
462
|
+
skip: true,
|
|
463
|
+
reason: previousState.reason,
|
|
464
|
+
});
|
|
465
|
+
reportDirDecision("skip", dirPath, currentStashDir, previousState.reason, previousState.persistedRowCount);
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
scannedDirs++;
|
|
469
|
+
priorDirsChanged = true;
|
|
470
|
+
const reason = isIncremental ? previousState.reason : { kind: "full-rebuild" };
|
|
471
|
+
dirRecords.push({
|
|
472
|
+
dirPath,
|
|
473
|
+
currentStashDir,
|
|
474
|
+
files: stateFiles,
|
|
475
|
+
stash,
|
|
476
|
+
skip: false,
|
|
477
|
+
reason,
|
|
478
|
+
persistedRowCount: previousState.persistedRowCount,
|
|
479
|
+
});
|
|
480
|
+
reportDirDecision("scan", dirPath, currentStashDir, reason, previousState.persistedRowCount);
|
|
481
|
+
};
|
|
429
482
|
for (const sourceAdded of allSourceEntries) {
|
|
430
483
|
const currentStashDir = sourceAdded.path;
|
|
431
484
|
const fileContexts = walkStashFlat(currentStashDir);
|
|
@@ -463,33 +516,9 @@ async function indexEntries(db, allSourceEntries, isIncremental, builtAtMs, hadR
|
|
|
463
516
|
}
|
|
464
517
|
}
|
|
465
518
|
for (const [dirPath, { files, entries }] of wikiDirGroups) {
|
|
466
|
-
if (
|
|
467
|
-
const reason = { kind: "duplicate-dir" };
|
|
468
|
-
dirRecords.push({ dirPath, currentStashDir, files, stash: null, skip: true, reason });
|
|
469
|
-
reportDirDecision("skip", dirPath, currentStashDir, reason);
|
|
470
|
-
continue;
|
|
471
|
-
}
|
|
472
|
-
seenPaths.add(path.resolve(dirPath));
|
|
473
|
-
const previousState = getDirIndexState(db, dirPath, files, builtAtMs);
|
|
474
|
-
if (isIncremental && !previousState.stale && canUseIncrementalSkip(previousState, priorDirsChanged)) {
|
|
475
|
-
skippedDirs++;
|
|
476
|
-
dirRecords.push({ dirPath, currentStashDir, files, stash: null, skip: true, reason: previousState.reason });
|
|
477
|
-
reportDirDecision("skip", dirPath, currentStashDir, previousState.reason, previousState.persistedRowCount);
|
|
519
|
+
if (markSeenOrSkipDuplicate(dirPath, currentStashDir, files))
|
|
478
520
|
continue;
|
|
479
|
-
}
|
|
480
|
-
scannedDirs++;
|
|
481
|
-
priorDirsChanged = true;
|
|
482
|
-
const reason = isIncremental ? previousState.reason : { kind: "full-rebuild" };
|
|
483
|
-
dirRecords.push({
|
|
484
|
-
dirPath,
|
|
485
|
-
currentStashDir,
|
|
486
|
-
files,
|
|
487
|
-
stash: { entries },
|
|
488
|
-
skip: false,
|
|
489
|
-
reason,
|
|
490
|
-
persistedRowCount: previousState.persistedRowCount,
|
|
491
|
-
});
|
|
492
|
-
reportDirDecision("scan", dirPath, currentStashDir, reason, previousState.persistedRowCount);
|
|
521
|
+
recordFreshnessDecision(dirPath, currentStashDir, files, { entries });
|
|
493
522
|
}
|
|
494
523
|
continue;
|
|
495
524
|
}
|
|
@@ -504,13 +533,8 @@ async function indexEntries(db, allSourceEntries, isIncremental, builtAtMs, hadR
|
|
|
504
533
|
}
|
|
505
534
|
for (const [dirPath, files] of dirGroups) {
|
|
506
535
|
const indexableFiles = files.filter((file) => shouldIndexStashFile(currentStashDir, file));
|
|
507
|
-
if (
|
|
508
|
-
const reason = { kind: "duplicate-dir" };
|
|
509
|
-
dirRecords.push({ dirPath, currentStashDir, files: indexableFiles, stash: null, skip: true, reason });
|
|
510
|
-
reportDirDecision("skip", dirPath, currentStashDir, reason);
|
|
536
|
+
if (markSeenOrSkipDuplicate(dirPath, currentStashDir, indexableFiles))
|
|
511
537
|
continue;
|
|
512
|
-
}
|
|
513
|
-
seenPaths.add(path.resolve(dirPath));
|
|
514
538
|
if (indexableFiles.length === 0) {
|
|
515
539
|
skippedDirs++;
|
|
516
540
|
const reason = { kind: "no-indexable-files" };
|
|
@@ -540,37 +564,17 @@ async function indexEntries(db, allSourceEntries, isIncremental, builtAtMs, hadR
|
|
|
540
564
|
if (generated.entries.length > 0) {
|
|
541
565
|
generatedCount += generated.entries.length;
|
|
542
566
|
}
|
|
543
|
-
|
|
544
|
-
if (isIncremental && !previousState.stale && canUseIncrementalSkip(previousState, priorDirsChanged)) {
|
|
545
|
-
skippedDirs++;
|
|
546
|
-
dirRecords.push({
|
|
547
|
-
dirPath,
|
|
548
|
-
currentStashDir,
|
|
549
|
-
files: staleFiles,
|
|
550
|
-
stash: null,
|
|
551
|
-
skip: true,
|
|
552
|
-
reason: previousState.reason,
|
|
553
|
-
});
|
|
554
|
-
reportDirDecision("skip", dirPath, currentStashDir, previousState.reason, previousState.persistedRowCount);
|
|
555
|
-
continue;
|
|
556
|
-
}
|
|
557
|
-
scannedDirs++;
|
|
558
|
-
priorDirsChanged = true;
|
|
559
|
-
const reason = isIncremental ? previousState.reason : { kind: "full-rebuild" };
|
|
560
|
-
dirRecords.push({
|
|
561
|
-
dirPath,
|
|
562
|
-
currentStashDir,
|
|
563
|
-
files: staleFiles,
|
|
564
|
-
stash,
|
|
565
|
-
skip: false,
|
|
566
|
-
reason,
|
|
567
|
-
persistedRowCount: previousState.persistedRowCount,
|
|
568
|
-
});
|
|
569
|
-
reportDirDecision("scan", dirPath, currentStashDir, reason, previousState.persistedRowCount);
|
|
567
|
+
recordFreshnessDecision(dirPath, currentStashDir, staleFiles, stash);
|
|
570
568
|
}
|
|
571
569
|
}
|
|
572
|
-
|
|
573
|
-
|
|
570
|
+
return { dirRecords, scannedDirs, skippedDirs, generatedCount, warnings };
|
|
571
|
+
}
|
|
572
|
+
/**
|
|
573
|
+
* Phase 2 (sync): write all pre-generated scan records inside a single
|
|
574
|
+
* transaction, returning the directories that still need LLM enrichment.
|
|
575
|
+
*/
|
|
576
|
+
function persistDirRecords(db, dirRecords, doFullDelete, warnings) {
|
|
577
|
+
const dirsNeedingLlm = [];
|
|
574
578
|
// Cross-stash dedup: track indexed assets by content identity
|
|
575
579
|
// (type + filename + description) so the same asset from a lower-priority
|
|
576
580
|
// stash root is skipped when a higher-priority root already covers it.
|
|
@@ -692,6 +696,14 @@ async function indexEntries(db, allSourceEntries, isIncremental, builtAtMs, hadR
|
|
|
692
696
|
}
|
|
693
697
|
});
|
|
694
698
|
insertTransaction();
|
|
699
|
+
return { dirsNeedingLlm };
|
|
700
|
+
}
|
|
701
|
+
async function indexEntries(db, allSourceEntries, isIncremental, builtAtMs, hadRemovedSources, doFullDelete = false, onProgress) {
|
|
702
|
+
// Phase 1 (async): walk directories and pre-generate all metadata outside the
|
|
703
|
+
// transaction.
|
|
704
|
+
const { dirRecords, scannedDirs, skippedDirs, generatedCount, warnings } = await scanSourceDirs(db, allSourceEntries, isIncremental, builtAtMs, hadRemovedSources, onProgress);
|
|
705
|
+
// Phase 2 (sync): write all pre-generated metadata inside a single transaction.
|
|
706
|
+
const { dirsNeedingLlm } = persistDirRecords(db, dirRecords, doFullDelete, warnings);
|
|
695
707
|
return { scannedDirs, skippedDirs, generatedCount, warnings, dirsNeedingLlm };
|
|
696
708
|
}
|
|
697
709
|
async function enhanceDirsWithLlm(db, config, dirsNeedingLlm, onProgress, signal, reEnrich = false) {
|
|
@@ -20,7 +20,7 @@ import { defaultRendererRegistry } from "../../core/asset/asset-registry.js";
|
|
|
20
20
|
import { getDbPath } from "../../core/paths.js";
|
|
21
21
|
import { warn } from "../../core/warn.js";
|
|
22
22
|
import { getCurrentWorkflowScopeKey } from "../../workflows/authoring/scope-key.js";
|
|
23
|
-
import { closeDatabase, getAllEntries, getEntryById, getEntryCount, getMeta, getPositiveFeedbackCountsByIds, openExistingDatabase, sanitizeFtsQuery, searchFts, searchVec, } from "../db/db.js";
|
|
23
|
+
import { closeDatabase, getAllEntries, getBaseBeliefStatesForDerivedTwins, getEntryById, getEntryCount, getMeta, getPositiveFeedbackCountsByIds, openExistingDatabase, sanitizeFtsQuery, searchFts, searchVec, } from "../db/db.js";
|
|
24
24
|
import { ensureIndex } from "../ensure-index.js";
|
|
25
25
|
import { collectGraphRelatedHit, computeGraphBoost, loadGraphBoostContext, } from "../graph/graph-boost.js";
|
|
26
26
|
import { isProposedQuality } from "../passes/metadata.js";
|
|
@@ -201,7 +201,11 @@ async function searchDatabase(db, query, searchType, limit, stashDir, allSourceD
|
|
|
201
201
|
const qualityFiltered = includeProposed
|
|
202
202
|
? scopeFiltered
|
|
203
203
|
: scopeFiltered.filter((ie) => !isProposedQuality(ie.entry.quality));
|
|
204
|
-
|
|
204
|
+
// 03-R3: derived twins inherit their base's demoting belief state here too,
|
|
205
|
+
// so the belief FILTER (and the reported hit state) stays consistent on the
|
|
206
|
+
// enumerate/browse path — not only on the FTS-scored path below.
|
|
207
|
+
inheritDerivedTwinBeliefStates(db, qualityFiltered);
|
|
208
|
+
const beliefFiltered = qualityFiltered.filter((ie) => matchBeliefFilter(ie.entry.beliefState, beliefFilter));
|
|
205
209
|
const selected = beliefFiltered.slice(0, limit);
|
|
206
210
|
const hits = await Promise.all(selected.map((ie) => buildDbHit({
|
|
207
211
|
entry: ie.entry,
|
|
@@ -302,6 +306,9 @@ async function searchDatabase(db, query, searchType, limit, stashDir, allSourceD
|
|
|
302
306
|
catch {
|
|
303
307
|
// Non-fatal — ranking proceeds without scoped utility on any error.
|
|
304
308
|
}
|
|
309
|
+
// 03-R3: derived twins inherit their base's demoting belief state before
|
|
310
|
+
// ranking, so the (03) belief-state ranker demotes a stale flag-free twin.
|
|
311
|
+
inheritDerivedTwinBeliefStates(db, scored);
|
|
305
312
|
applyRankingRules({
|
|
306
313
|
db,
|
|
307
314
|
query,
|
|
@@ -358,7 +365,7 @@ async function searchDatabase(db, query, searchType, limit, stashDir, allSourceD
|
|
|
358
365
|
const qualityFiltered = includeProposed
|
|
359
366
|
? scopeFiltered
|
|
360
367
|
: scopeFiltered.filter((item) => !isProposedQuality(item.entry.quality));
|
|
361
|
-
const beliefFiltered = qualityFiltered.filter((item) => matchBeliefFilter(item.entry.
|
|
368
|
+
const beliefFiltered = qualityFiltered.filter((item) => matchBeliefFilter(item.entry.beliefState, beliefFilter));
|
|
362
369
|
const rankMs = Date.now() - tRank0;
|
|
363
370
|
const selected = beliefFiltered.slice(0, limit);
|
|
364
371
|
const hits = await Promise.all(selected.map(({ entry, filePath, score, rankingMode, utilityBoosted }) => {
|
|
@@ -386,11 +393,39 @@ async function searchDatabase(db, query, searchType, limit, stashDir, allSourceD
|
|
|
386
393
|
}));
|
|
387
394
|
return { embedMs, rankMs, hits };
|
|
388
395
|
}
|
|
389
|
-
|
|
396
|
+
/**
|
|
397
|
+
* 03-R3: let each `.derived` twin inherit its base memory's demoting belief
|
|
398
|
+
* state for this ranking pass, so a stale flag-free twin is demoted like its
|
|
399
|
+
* corrected base. The base carries the flag (a contradicted base takes a real
|
|
400
|
+
* ranking penalty); its near-duplicate `.derived` twin carries none and would
|
|
401
|
+
* otherwise outrank the corrected copy. Done in-memory at search time — NOT by
|
|
402
|
+
* writing the twin's frontmatter — because the SCC belief resolver refreshes any
|
|
403
|
+
* non-frozen state written to a derived memory back to `active` on the next
|
|
404
|
+
* improve run, erasing it. Only twins with no state of their own inherit; an
|
|
405
|
+
* explicit twin state always wins. Reuses the (03) belief-state ranker + filter.
|
|
406
|
+
*/
|
|
407
|
+
function inheritDerivedTwinBeliefStates(db, items) {
|
|
408
|
+
const DEMOTING = new Set(["contradicted", "superseded", "deprecated", "archived"]);
|
|
409
|
+
const twins = items.filter((it) => it.entry.type === "memory" &&
|
|
410
|
+
it.entry.beliefState === undefined &&
|
|
411
|
+
it.entry.name.toLowerCase().endsWith(".derived"));
|
|
412
|
+
if (twins.length === 0)
|
|
413
|
+
return;
|
|
414
|
+
const baseBeliefByTwinId = getBaseBeliefStatesForDerivedTwins(db, twins.map((t) => t.id));
|
|
415
|
+
for (const t of twins) {
|
|
416
|
+
const baseBelief = baseBeliefByTwinId.get(t.id);
|
|
417
|
+
// Only inherit DEMOTIONS — never let a base's active/asserted state lift a twin.
|
|
418
|
+
if (baseBelief && DEMOTING.has(baseBelief)) {
|
|
419
|
+
t.entry.beliefState = baseBelief;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
function matchBeliefFilter(beliefState, filter) {
|
|
390
424
|
if (filter === "all")
|
|
391
425
|
return true;
|
|
392
|
-
|
|
393
|
-
|
|
426
|
+
// 03: the belief filter applies to ANY flagged entry, not just memories, so
|
|
427
|
+
// `current`/`historical` filters catch contradicted/superseded KNOWLEDGE too.
|
|
428
|
+
// Unflagged entries (beliefState === undefined) still pass the `current` filter.
|
|
394
429
|
if (filter === "current") {
|
|
395
430
|
// Phase 1A: `asserted` is a "current" state (stronger authority than `active`);
|
|
396
431
|
// `deprecated` is excluded from current results.
|
|
@@ -40,8 +40,10 @@ const DEFAULT_RECENCY_HALF_LIFE_DAYS = 30;
|
|
|
40
40
|
const FEEDBACK_HALF_LIFE_CAP_MULTIPLIER = 4;
|
|
41
41
|
function beliefStateBoost(item) {
|
|
42
42
|
const entry = item.entry;
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
// 03: belief-state penalties/boosts apply to ANY flagged entry (memory OR
|
|
44
|
+
// knowledge), so contradicted/superseded KNOWLEDGE is demoted from results
|
|
45
|
+
// just like flagged memories. Entries without a belief state fall through to
|
|
46
|
+
// the `return 0` below (default-safe — no effect on unflagged assets).
|
|
45
47
|
// Phase 1A: `asserted` and `deprecated` are first-class states.
|
|
46
48
|
// `asserted` carries stronger user-explicit authority than `active`.
|
|
47
49
|
// `deprecated` is a frozen historical state — penalized but milder than `superseded`.
|
|
@@ -87,14 +89,18 @@ const typeRankingContributor = {
|
|
|
87
89
|
return TYPE_BOOST[item.entry.type] ?? 0;
|
|
88
90
|
},
|
|
89
91
|
};
|
|
90
|
-
const
|
|
91
|
-
name: "
|
|
92
|
+
const beliefStateRankingContributor = {
|
|
93
|
+
name: "belief-state-ranking",
|
|
92
94
|
appliesTo(item) {
|
|
93
|
-
|
|
95
|
+
// Fire for any entry that carries a belief state, regardless of type — so
|
|
96
|
+
// contradicted/superseded knowledge is demoted, not just memories. The
|
|
97
|
+
// `.derived`-twin `derivedBoost` (±0.12/−0.08) is deleted (03-R3): it made
|
|
98
|
+
// stale flag-free twins outrank their corrected base memory; belief-state
|
|
99
|
+
// demotion is the principled signal, not the twin-name heuristic.
|
|
100
|
+
return item.entry.beliefState !== undefined;
|
|
94
101
|
},
|
|
95
102
|
adjust(item) {
|
|
96
|
-
|
|
97
|
-
return derivedBoost + beliefStateBoost(item);
|
|
103
|
+
return beliefStateBoost(item);
|
|
98
104
|
},
|
|
99
105
|
};
|
|
100
106
|
const tagRankingContributor = {
|
|
@@ -330,7 +336,7 @@ const projectContextRankingContributor = {
|
|
|
330
336
|
export const defaultRankingContributors = [
|
|
331
337
|
exactNameRankingContributor,
|
|
332
338
|
typeRankingContributor,
|
|
333
|
-
|
|
339
|
+
beliefStateRankingContributor,
|
|
334
340
|
tagRankingContributor,
|
|
335
341
|
searchHintRankingContributor,
|
|
336
342
|
aliasRankingContributor,
|
|
@@ -167,14 +167,26 @@ export function getWritableStashDirs(overrideStashDir, existingConfig) {
|
|
|
167
167
|
}
|
|
168
168
|
/**
|
|
169
169
|
* Find which source a file path belongs to.
|
|
170
|
+
*
|
|
171
|
+
* Longest-matching-prefix wins: a source nested inside another (e.g. `akm add
|
|
172
|
+
* ./sub` where `./sub` lives under the primary stash — which is always
|
|
173
|
+
* `sources[0]`) is the more specific owner and must win over the enclosing
|
|
174
|
+
* source regardless of array order. A first-match-in-order scan would
|
|
175
|
+
* misattribute every asset under the nested source to the primary stash,
|
|
176
|
+
* corrupting origin / editability / provenance decisions for the affected files.
|
|
170
177
|
*/
|
|
171
178
|
export function findSourceForPath(filePath, sources) {
|
|
172
179
|
const resolved = path.resolve(filePath);
|
|
180
|
+
let best;
|
|
181
|
+
let bestLen = -1;
|
|
173
182
|
for (const source of sources) {
|
|
174
|
-
|
|
175
|
-
|
|
183
|
+
const base = path.resolve(source.path);
|
|
184
|
+
if (resolved.startsWith(base + path.sep) && base.length > bestLen) {
|
|
185
|
+
best = source;
|
|
186
|
+
bestLen = base.length;
|
|
187
|
+
}
|
|
176
188
|
}
|
|
177
|
-
return
|
|
189
|
+
return best;
|
|
178
190
|
}
|
|
179
191
|
/**
|
|
180
192
|
* Return the primary stash source (first entry in the list).
|
package/dist/llm/feature-gate.js
CHANGED
|
@@ -22,12 +22,11 @@ const FEATURE_LOCATION = {
|
|
|
22
22
|
graph_extraction: (cfg) => cfg.profiles?.improve?.default?.processes?.graphExtraction?.enabled ?? true,
|
|
23
23
|
// Legacy default: false
|
|
24
24
|
metadata_enhance: (cfg) => cfg.index?.metadataEnhance?.enabled ?? false,
|
|
25
|
-
// Legacy default: false
|
|
26
|
-
curate_rerank: (cfg) => cfg.search?.curateRerank?.enabled ?? false,
|
|
27
25
|
// Default ON since R3 (docs/design/improve-self-learning-analysis.md G5):
|
|
28
|
-
// distill is a primary acquisition path
|
|
29
|
-
//
|
|
30
|
-
//
|
|
26
|
+
// distill is a primary acquisition path, so the gate guards minted content by
|
|
27
|
+
// default. The judge fails CLOSED (07 P0-2): no LLM / timeout / parse failure
|
|
28
|
+
// reject the proposal rather than passing it through — an unjudgeable proposal
|
|
29
|
+
// must not slip into the stash. Opt out via
|
|
31
30
|
// profiles.improve.default.processes.distill.qualityGate.enabled: false.
|
|
32
31
|
lesson_quality_gate: (cfg) => cfg.profiles?.improve?.default?.processes?.distill?.qualityGate?.enabled ?? true,
|
|
33
32
|
// Legacy default: false
|
|
@@ -112,9 +111,6 @@ export function isProcessEnabled(section, processName, config) {
|
|
|
112
111
|
return isLlmFeatureEnabled(config, "graph_extraction");
|
|
113
112
|
}
|
|
114
113
|
}
|
|
115
|
-
if (section === "search" && (processName === "curate_rerank" || processName === "curateRerank")) {
|
|
116
|
-
return config.search?.curateRerank?.enabled ?? false;
|
|
117
|
-
}
|
|
118
114
|
if (section === "improve") {
|
|
119
115
|
const processes = config.profiles?.improve?.default?.processes;
|
|
120
116
|
const entry = processes?.[processName];
|
package/dist/output/renderers.js
CHANGED
|
@@ -210,6 +210,10 @@ const agentMdRenderer = {
|
|
|
210
210
|
action: "Dispatch using the prompt below verbatim. Use modelHint and toolPolicy if present.",
|
|
211
211
|
description: asNonEmptyString(parsedMd.data.description),
|
|
212
212
|
prompt: parsedMd.content,
|
|
213
|
+
// `tools` is self-declared frontmatter. The provenance CEILING that decides
|
|
214
|
+
// whether this self-declared policy is honoured is applied at the show
|
|
215
|
+
// layer (`akmShowUnified`), which knows whether the source is the operator's
|
|
216
|
+
// own writable stash vs a read-only third-party source (07 P1-D).
|
|
213
217
|
toolPolicy: parsedMd.data.tools,
|
|
214
218
|
modelHint: typeof parsedMd.data.model === "string" ? parsedMd.data.model : undefined,
|
|
215
219
|
};
|