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
121 lines (120 loc) • 5.42 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/.
/**
* Upsert one `asset_outcome` row. Extracted verbatim from the tail of
* `updateAssetOutcome` in outcome-loop.ts — same SQL text, same bind order;
* the business logic that computes these values (warm-start seed vs.
* differential update) stays in outcome-loop.ts and calls this function.
*
* `review_pressure` is intentionally omitted from both the INSERT column list
* and the ON CONFLICT SET clause: the column's DEFAULT 0 seeds fresh rows, and
* omitting it from SET leaves an existing row's value untouched on update
* (never written going forward). The column itself is dropped in migration 018.
*/
export function upsertAssetOutcome(db, values) {
db.prepare(`INSERT INTO asset_outcome
(asset_ref, last_retrieved_at, retrieval_count, expected_retrieval_rate,
negative_feedback_count, accepted_change_count,
outcome_score, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(asset_ref) DO UPDATE SET
last_retrieved_at = excluded.last_retrieved_at,
retrieval_count = excluded.retrieval_count,
expected_retrieval_rate= excluded.expected_retrieval_rate,
negative_feedback_count= excluded.negative_feedback_count,
accepted_change_count = excluded.accepted_change_count,
outcome_score = excluded.outcome_score,
updated_at = excluded.updated_at`).run(values.ref, values.lastRetrievedAt, values.retrievalCount, values.expectedRetrievalRate, values.negativeFeedbackCount, values.acceptedChangeCount, values.outcomeScore, values.updatedAt);
}
/**
* Load the outcome row for one asset, or `undefined` if not yet written.
*/
export function getAssetOutcome(db, ref) {
const row = db
.prepare(`SELECT asset_ref, last_retrieved_at, retrieval_count, expected_retrieval_rate,
negative_feedback_count, accepted_change_count,
outcome_score, updated_at
FROM asset_outcome WHERE asset_ref = ?`)
.get(ref);
return row == null ? undefined : row;
}
/**
* Load ALL asset_outcome rows. Used for the stash-wide outcome-score
* normalisation in improve preparation.
*/
export function getAllAssetOutcomes(db) {
return db
.prepare(`SELECT asset_ref, last_retrieved_at, retrieval_count, expected_retrieval_rate,
negative_feedback_count, accepted_change_count,
outcome_score, updated_at
FROM asset_outcome ORDER BY asset_ref`)
.all();
}
/**
* Build a Map<ref, outcome_score> for a set of refs in one query.
* Used by `salience.ts` to populate `outcomeSalience`.
*/
export function getOutcomeScoresByRef(db, refs) {
const result = new Map();
if (refs.length === 0)
return result;
const CHUNK = 500;
for (let i = 0; i < refs.length; i += CHUNK) {
const chunk = refs.slice(i, i + CHUNK);
const placeholders = chunk.map(() => "?").join(",");
const rows = db
.prepare(`SELECT asset_ref, outcome_score FROM asset_outcome WHERE asset_ref IN (${placeholders})`)
.all(...chunk);
for (const row of rows) {
result.set(row.asset_ref, row.outcome_score);
}
}
return result;
}
/** List every `asset_outcome` ref with its current `missing_since` marker. */
export function listAssetOutcomeMissingState(db) {
return db.prepare(`SELECT asset_ref, missing_since FROM asset_outcome`).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 stampAssetOutcomeMissing(db, refs, now) {
if (refs.length === 0)
return 0;
const stmt = db.prepare(`UPDATE asset_outcome 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 clearAssetOutcomeMissing(db, refs) {
if (refs.length === 0)
return 0;
const stmt = db.prepare(`UPDATE asset_outcome 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_outcome` 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 deleteAssetOutcomeMissingBefore(db, cutoffMs) {
return Number(db.prepare(`DELETE FROM asset_outcome WHERE missing_since IS NOT NULL AND missing_since < ?`).run(cutoffMs).changes);
}
/** Count `asset_outcome` rows currently marked missing (the live backlog size). */
export function countAssetOutcomeMissing(db) {
const row = db.prepare(`SELECT COUNT(*) AS n FROM asset_outcome WHERE missing_since IS NOT NULL`).get();
return row.n;
}