akm-cli
Version:
akm (Agent Knowledge Manager) — a portable, local-first capability library for AI agents. Discover, load, share, and improve reusable skills, scripts, workflows, and knowledge across any shell-capable coding agent, including Claude Code, OpenCode, and Cur
93 lines (92 loc) • 4.39 kB
JavaScript
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
/**
* What improve may rework (#986): an asset that retrieval returned inside the
* usage window, or newly captured material no improve stage has processed.
*
* Fresh feedback and an explicit `--scope <ref>` are usage evidence of their
* own, so the signal-delta and scope lanes need no check. The fallback lanes
* (proactive maintenance, high salience) and consolidation pick assets
* without such evidence, so they pick only inside this scope.
*/
import fs from "node:fs";
import { daysToMs } from "../../core/common.js";
import { warn } from "../../core/warn.js";
import { listUsedEntryRefs, USAGE_EVENT_RETENTION_DAYS } from "../../indexer/usage/usage-events.js";
import { listImproveLedgerRows } from "../../storage/repositories/improve-ledger-repository.js";
import { listProposalRefSources } from "../../storage/repositories/proposals-repository.js";
import { PAIR_PASS_LEDGER_SOURCE, readLedgerDb, stripBundle } from "./ledger.js";
/**
* Ledger and proposal sources that bring material in. Every other source is an
* improve stage reworking an asset (reflect, distill, consolidate, schema repair).
*/
const CAPTURE_SOURCES = new Set(["extract", "propose", "remember", "import"]);
/**
* Load the scope from state.db. The window is the usage log's retention: the
* log keeps nothing older, and anything shorter would drop assets read less
* often than the window. A `.derived` hit counts for its parent memory, whose
* facts it carries. `undefined` (every asset eligible, as before #986) only
* when state.db cannot be read.
*/
export function loadRetrievalScope(access, stashDir) {
const sinceMs = Date.now() - daysToMs(USAGE_EVENT_RETENTION_DAYS);
const used = new Set();
const processed = new Set();
try {
readLedgerDb(access, (db) => {
for (const entryRef of listUsedEntryRefs(db, new Date(sinceMs).toISOString())) {
const conceptId = stripBundle(entryRef);
used.add(conceptId);
if (conceptId.endsWith(".derived"))
used.add(conceptId.slice(0, -".derived".length));
}
if (!stashDir)
return;
// Blocker 1 (second review round): a pair-pass ledger row must NOT mark
// an asset "processed" — unlike every other stage, the pair pass judges
// material against its NEIGHBOURS, not on its own merits, so its own
// attempt is not usage evidence the fallback lanes (proactive,
// high-salience) or promotion retries should be starved by. Left in
// the ledger for the pair pass's OWN eligibility
// (selectInitiators reads content_hash directly, never this scope).
// Proposal rows are untouched: a MINTED retire proposal is real
// evidence something happened to the asset.
for (const row of listImproveLedgerRows(db, stashDir)) {
if (row.source === PAIR_PASS_LEDGER_SOURCE)
continue;
if (!CAPTURE_SOURCES.has(row.source))
processed.add(stripBundle(row.ref));
}
for (const row of listProposalRefSources(db, stashDir)) {
if (!CAPTURE_SOURCES.has(row.source))
processed.add(stripBundle(row.ref));
}
});
}
catch (error) {
warn(`[improve] usage history unreadable, so every asset stays eligible: ${error instanceof Error ? error.message : String(error)}`);
return undefined;
}
return { used, processed, sinceMs };
}
/**
* Whether improve may rework `ref` (whose file is `filePath`) under `scope`. An
* unprocessed asset whose file cannot be read is left to the disk check that
* follows, which reports it as missing.
*/
export function isInRetrievalScope(scope, ref, filePath) {
if (!scope)
return true;
const conceptId = stripBundle(ref);
if (scope.used.has(conceptId))
return true;
if (scope.processed.has(conceptId))
return false;
try {
return filePath === undefined || fs.statSync(filePath).mtimeMs >= scope.sinceMs;
}
catch {
return true;
}
}