UNPKG

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

156 lines (155 loc) • 7.85 kB
// 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/. /** * Upsert salience scores for one asset into state.db. * * Idempotent: safe to call every run; updates the outcome / retrieval / rank * columns on conflict. * * #644 — encoding provenance guard: the `encoding_salience` + `encoding_source` * columns are NOT lowered from a real content-derived score to a type-weight * stub. When the stored row is `encoding_source = 'content'` and the incoming * vector is a `type-stub` fallback, the stored encoding score and its provenance * are preserved (only the other sub-scores and `rank_score` advance). A `content` * write always wins; a `type-stub` write only seeds a row that has no content * score yet. This stops the improve loop's type-weight fallback re-asserting the * stub over a distill-written score on every run. * * NOTE: when the guard preserves the stored encoding score, the incoming * `vector.rankScore` (computed from the stub encoding) is still written. Callers * that want the rank_score to reflect the preserved content score should pass the * stored content score back in as `inputs.encodingSalience` to `computeSalience` * — which the improve loop does. The guard here is the defensive backstop. */ export function upsertAssetSalience(db, ref, vector, now) { const ts = now ?? Date.now(); db.prepare(`INSERT INTO asset_salience (asset_ref, encoding_salience, outcome_salience, retrieval_salience, rank_score, consecutive_no_ops, updated_at, encoding_source) VALUES (?, ?, ?, ?, ?, 0, ?, ?) ON CONFLICT(asset_ref) DO UPDATE SET -- #644: never lower a real content-derived score to a type-weight stub. -- Keep the stored encoding score + provenance when the stored row is -- 'content' and the incoming write is a 'type-stub' fallback. encoding_salience = CASE WHEN asset_salience.encoding_source = 'content' AND excluded.encoding_source = 'type-stub' THEN asset_salience.encoding_salience ELSE excluded.encoding_salience END, encoding_source = CASE WHEN asset_salience.encoding_source = 'content' AND excluded.encoding_source = 'type-stub' THEN asset_salience.encoding_source ELSE excluded.encoding_source END, outcome_salience = excluded.outcome_salience, retrieval_salience = excluded.retrieval_salience, rank_score = excluded.rank_score, updated_at = excluded.updated_at`).run(ref, vector.encoding, vector.outcome, vector.retrieval, vector.rankScore, ts, vector.encodingSource ?? "type-stub"); } /** * Load the salience row for one asset, or undefined if not yet computed. */ export function getAssetSalience(db, ref) { const row = db .prepare(`SELECT asset_ref, encoding_salience, outcome_salience, retrieval_salience, rank_score, consecutive_no_ops, updated_at, encoding_source FROM asset_salience WHERE asset_ref = ?`) .get(ref); // Bun SQLite returns null (not undefined) when no row found. return row == null ? undefined : row; } // ── Plasticity helpers ──────────────────────────────────────────────────────── /** * Increment `consecutive_no_ops` for an asset. Called after a no-op reflect/distill. * Has NO effect on `rank_score` — the plasticity counter only dampens consolidation * selection, not retrieval ranking. See plan §WS-1 step 8. * * Invariant: recordNoOp must never originate rank_score semantics. If the asset has * no salience row yet (persistence's best-effort try/catch may have swallowed an * error), we do nothing — a no-op counter is meaningless without a rank_score row, * and a synthetic INSERT would fabricate a rank_score=0 entry with nothing behind it. */ export function recordNoOp(db, ref) { db.prepare(`UPDATE asset_salience SET consecutive_no_ops = consecutive_no_ops + 1, updated_at = ? WHERE asset_ref = ?`).run(Date.now(), ref); // If changes === 0 the asset has no salience row yet — leave the table unchanged. } /** * Reset `consecutive_no_ops` to 0 when an asset produces an accepted change. * Call after a successful proposal acceptance or detected mutation. */ export function resetConsecutiveNoOps(db, ref) { db.prepare(`UPDATE asset_salience SET consecutive_no_ops = 0, updated_at = ? WHERE asset_ref = ?`).run(Date.now(), ref); } /** * Return the `consecutive_no_ops` count for one asset. 0 when unknown. */ export function getConsecutiveNoOps(db, ref) { const row = db.prepare(`SELECT consecutive_no_ops FROM asset_salience WHERE asset_ref = ?`).get(ref); return row?.consecutive_no_ops ?? 0; } // ── New in #672 part 2 ──────────────────────────────────────────────────────── /** * Load every observed retrieval-salience value for a currently resolvable * asset. Zero is the no-observation floor and is excluded so corpus sparsity * does not masquerade as score entrenchment. * * Health computes its distribution diagnostic over this full observed set. * Sampling the top-N rows by `rank_score` first truncates the distribution and * can make a healthy corpus look artificially uniform. */ export function getObservedRetrievalSalience(db) { return db .prepare(`SELECT retrieval_salience FROM asset_salience WHERE retrieval_salience > 0 AND missing_since IS NULL ORDER BY retrieval_salience ASC`) .all(); } /** List every `asset_salience` ref with its current `missing_since` marker. */ export function listAssetSalienceMissingState(db) { return db.prepare(`SELECT asset_ref, missing_since FROM asset_salience`).all(); } /** * Stamp `missing_since = now` for the given refs, but ONLY where it is * currently NULL — a ref already stamped keeps its original timestamp so the * grace window measures from first sighting, not last. Refs with no row at * all are silently skipped (0 rows affected). Returns the number of rows * actually stamped. */ export function stampAssetSalienceMissing(db, refs, now) { if (refs.length === 0) return 0; const stmt = db.prepare(`UPDATE asset_salience SET missing_since = ? WHERE asset_ref = ? AND missing_since IS NULL`); let changed = 0; for (const ref of refs) changed += Number(stmt.run(now, ref).changes); return changed; } /** * Clear `missing_since` for refs that resolved again. Returns the number of * rows actually cleared. */ export function clearAssetSalienceMissing(db, refs) { if (refs.length === 0) return 0; const stmt = db.prepare(`UPDATE asset_salience SET missing_since = NULL WHERE asset_ref = ? AND missing_since IS NOT NULL`); let changed = 0; for (const ref of refs) changed += Number(stmt.run(ref).changes); return changed; } /** * Delete `asset_salience` rows whose `missing_since` is older than * `cutoffMs`. A single DELETE — SQLite runs it as one implicit transaction. * Returns the number of rows deleted. */ export function deleteAssetSalienceMissingBefore(db, cutoffMs) { return Number(db.prepare(`DELETE FROM asset_salience WHERE missing_since IS NOT NULL AND missing_since < ?`).run(cutoffMs) .changes); } /** Count `asset_salience` rows currently marked missing (the live backlog size). */ export function countAssetSalienceMissing(db) { const row = db.prepare(`SELECT COUNT(*) AS n FROM asset_salience WHERE missing_since IS NOT NULL`).get(); return row.n; }