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
67 lines (66 loc) • 2.91 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/.
import { assetTypeOf } from "./eligibility.js";
import { computeSalience } from "./salience.js";
const DAY_MS = 86_400_000;
/** An asset is due when last reflected/distilled more than this many days ago (or never). */
export const DEFAULT_DUE_DAYS = 30;
/** Default bound on how many assets the selector surfaces per run. */
export const DEFAULT_MAX_PER_RUN = 25;
/** Size floor for the cost term, so tiny files don't divide by ~0. */
const SIZE_FLOOR_BYTES = 200;
/**
* The due gate: never touched, or last touched more than `dueDays` ago. It
* doubles as the rotation cooldown.
*/
function staleness(ref, lastReflectTs, lastDistillTs, dueDays, now) {
const lastTouchMs = Math.max(0, Date.parse(lastReflectTs.get(ref) ?? "") || 0, Date.parse(lastDistillTs.get(ref) ?? "") || 0);
const neverReflected = lastTouchMs === 0;
const staleDays = neverReflected ? Number.POSITIVE_INFINITY : (now - lastTouchMs) / DAY_MS;
return { staleDays, neverReflected, due: neverReflected || staleDays > dueDays };
}
/** Rank due assets by salience (then staleness, then ref) and keep the top N. */
export function selectProactiveMaintenanceRefs(params) {
const now = params.now ?? Date.now();
const dueDays = params.dueDays ?? DEFAULT_DUE_DAYS;
const maxPerRun = params.maxPerRun ?? DEFAULT_MAX_PER_RUN;
const scored = params.candidates.map((candidate) => {
const ref = candidate.ref;
const type = assetTypeOf(ref);
const retrievalFreq = params.retrievalCounts.get(ref) ?? 0;
let sizeBytes = params.sizeBytesOf?.(candidate) ?? 0;
if (!sizeBytes || sizeBytes < 0)
sizeBytes = SIZE_FLOOR_BYTES;
const priority = computeSalience({
ref,
type,
retrievalFreq,
lastUseMs: params.lastUseMs?.get(ref) ?? 0,
sizeBytes,
now,
}).rankScore;
return {
ref: candidate,
type,
...staleness(ref, params.lastReflectTs, params.lastDistillTs, dueDays, now),
retrievalFreq,
sizeBytes,
priority,
};
});
const dueScored = scored.filter((s) => s.due);
const ranked = dueScored.slice().sort((a, b) => {
if (b.priority !== a.priority)
return b.priority - a.priority;
if (b.staleDays !== a.staleDays)
return b.staleDays - a.staleDays;
return a.ref.ref < b.ref.ref ? -1 : a.ref.ref > b.ref.ref ? 1 : 0;
});
return {
selected: ranked.slice(0, Math.max(0, maxPerRun)).map((s) => s.ref),
dueTotal: dueScored.length,
neverReflected: dueScored.filter((s) => s.neverReflected).length,
scored,
};
}