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
1,048 lines • 51.7 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/.
/**
* `akm improve`: under one run lock, bootstrap the index, drain the proposal
* backlog (triage), select candidates, run preparation → loop → post-loop, then
* commit what the run wrote (auto-sync). A dry run plans on read-only state.
*/
import fs from "node:fs";
import path from "node:path";
import { parseRefInput } from "../../core/asset/resolve-ref.js";
import { bundlesToSourceEntries, loadConfig } from "../../core/config/config.js";
import { ConfigError, rethrowIfTestIsolationError, UsageError } from "../../core/errors.js";
import { appendEvent, readEvents } from "../../core/events.js";
import { classifyImproveAction, foldDistillSkipped } from "../../core/improve-types.js";
import { resolveMutationTarget } from "../../core/mutation-target.js";
import { getDbPath, getStashLocksDir, getStateDbPathInDataDir } from "../../core/paths.js";
import { redactSensitiveText } from "../../core/redaction.js";
import { openStateDatabase } from "../../core/state-db.js";
import { info, warn, warnVerbose } from "../../core/warn.js";
import { beginWriteProvenance, relativeWrittenPath } from "../../core/write-provenance.js";
import { resolveWorkingStashTarget, resolveWritable, resolveWriteTarget } from "../../core/write-source.js";
import { ensureIndex } from "../../indexer/ensure-index.js";
import { indexWrittenAssets } from "../../indexer/index-written-assets.js";
import { akmIndex } from "../../indexer/indexer.js";
import { collectPendingMemories } from "../../indexer/passes/memory-inference.js";
import { resolveEntryContentDir, resolveSourceEntries } from "../../indexer/search/search-source.js";
import { collectEngineCredentialValues } from "../../integrations/agent/engine-resolution.js";
import { installLlmUsagePersistence, LLM_USAGE_EVENT } from "../../llm/usage-persist.js";
import { withLlmStage } from "../../llm/usage-telemetry.js";
import { isGitBackedStash, listGitChangedPaths, resolveWritableOverride, saveGitStash, } from "../../sources/providers/git.js";
import { closeDatabase, openExistingDatabase } from "../../storage/repositories/index-connection.js";
import { getEntryCount } from "../../storage/repositories/index-entries-repository.js";
import { openSqliteReadSnapshot, SqliteReadSnapshotUnavailableError } from "../../storage/sqlite-read-snapshot.js";
import { summarizeLlmUsageCrossTab } from "../health/llm-usage.js";
import { drainProposals } from "../proposal/drain.js";
import { describeGatedLanes, isAutonomyLaneAllowed } from "./autonomy-gate.js";
import { akmDistill } from "./distill.js";
import { collectEligibleRefs, collectEligibleRefsReadOnly, memoryCleanupParentRef, resolveImproveScope, shouldAnalyzeMemoryCleanup, } from "./eligibility.js";
import { eligibleRefCount, projectResolvedProcessRouting, resolveImprovePlan, resolveImproveStrategy, } from "./improve-strategies.js";
import { buildImproveUsageReport } from "./improve-usage-report.js";
import { improveLockPath, releaseImproveLock, tryAcquireImproveLock } from "./locks.js";
import { runImproveLoopStage, runImprovePostLoopStage } from "./loop-stages.js";
import { analyzeMemoryCleanup, purgeGracedArchive, RETIRE_GRACE_DAYS, } from "./memory/memory-improve.js";
import { buildImproveExecutionPlan } from "./planner.js";
import { CONSOLIDATION_CONFIG_KEYS, pickDefined, recordImproveSkip, runImprovePreparationStage } from "./preparation.js";
import { akmReflect } from "./reflect.js";
import { errMessage, noticeSet } from "./stage.js";
export { runImproveMaintenancePasses } from "./loop-stages.js";
export function renderSyncCommitMessage(template, result, nowMs) {
const iso = new Date(nowMs).toISOString();
const tokens = {
timestamp: `${iso.slice(0, 10)} ${iso.slice(11, 19)}`,
date: iso.slice(0, 10),
time: iso.slice(11, 19),
scope: result.scope.value ?? result.scope.mode,
refs: String(result.plannedRefs.length),
accepted: String(result.triage?.promoted ?? 0),
triage_promoted: String(result.triage?.promoted ?? 0),
triage_rejected: String(result.triage?.rejected ?? 0),
runId: result.runId ?? "",
};
return template.replace(/\{(\w+)\}/g, (match, key) => tokens[key] ?? match);
}
/**
* How long the loop waits for its first engine response before printing one
* "still waiting" line (#957): a dead endpoint otherwise looks like a slow run.
* Armed when the loop starts, not at run start.
*/
export const FIRST_ENGINE_RESPONSE_HEARTBEAT_MS = 5_000;
/**
* Abort the run at the budget; force-exit (0 — budget exhaustion is a normal
* scheduled-task outcome) only if the drain overruns the grace period.
* Returns an idempotent disposer.
*/
export function armBudgetWatchdog(budgetMs, controller, deps) {
const setTimeoutFn = deps?.setTimeoutFn ?? setTimeout;
const clearTimeoutFn = deps?.clearTimeoutFn ?? clearTimeout;
const exitFn = deps?.exitFn ?? ((code) => process.exit(code));
const hardKillGraceMs = deps?.hardKillGraceMs ?? 5_000;
let hardKillTimer;
const budgetTimer = setTimeoutFn(() => {
controller.abort("improve budget exhausted");
hardKillTimer = setTimeoutFn(() => exitFn(0), hardKillGraceMs);
hardKillTimer.unref?.();
}, budgetMs);
return () => {
clearTimeoutFn(budgetTimer);
if (hardKillTimer !== undefined) {
clearTimeoutFn(hardKillTimer);
hardKillTimer = undefined;
}
};
}
export async function akmImprove(options = {}) {
const setup = resolveImproveRunSetup(options);
options = setup.options;
const { budgetMs, budgetAbortController, scope, selectedStrategy, syncRepoDir, resolvedStateDbPath } = setup;
let clearBudgetTimer = () => { };
let clearFirstResponseHeartbeat = () => { };
// Set by the usage sink when any engine call terminates, prepass included.
let firstEngineResponseSeen = false;
let initialGitPaths = new Set();
// The write-provenance journal (#652) spans exactly the window this run holds the lock.
let journal;
const closeJournal = () => {
journal?.end();
journal = undefined;
};
const preEnsureCleanupWarnings = [];
let collected;
let triageDrain;
let ensureIndexDurationMs;
let improveLockOwnership;
let exitBackstop;
let eventsDb;
// Boundary-pinned for the prepass; replaced by the long-lived handle after it.
// The usage sink resolves it per append.
let eventsCtx = { dbPath: resolvedStateDbPath };
let disposeLlmUsageSink = () => { };
const releaseRunLock = () => {
const ownership = improveLockOwnership;
if (!ownership)
return;
improveLockOwnership = undefined;
try {
releaseImproveLock(ownership);
}
catch {
// Best-effort; exact ownership never deletes a successor's lock.
}
};
const teardown = () => {
// The usage sink goes before eventsDb closes; the journal closes after the
// crash-safety commit that still needs it.
disposeLlmUsageSink();
clearFirstResponseHeartbeat();
clearBudgetTimer();
if (exitBackstop) {
// Only our own listener: removeAllListeners would drop the host's.
process.removeListener("exit", exitBackstop);
exitBackstop = undefined;
}
releaseRunLock();
closeJournal();
};
const commitStashBatch = makeCommitStashBatch({
run: setup,
getInitialGitPaths: () => initialGitPaths,
getWriteJournal: () => journal,
getEventsCtx: () => eventsCtx,
});
if (!options.dryRun) {
const remainingBudget = budgetAbortController.signal.remainingBudgetMs;
clearBudgetTimer = armBudgetWatchdog(Math.max(1, remainingBudget ?? budgetMs), budgetAbortController);
}
try {
if (!options.dryRun) {
const acquisition = tryAcquireImproveLock(setup.resolvedLockPath, options.skipIfLocked, {
dbPath: resolvedStateDbPath,
});
if (acquisition.state === "skipped") {
clearBudgetTimer();
return buildLockSkippedResult(selectedStrategy.name, scope, options.runId);
}
improveLockOwnership = acquisition.ownership;
disposeLlmUsageSink = installLlmUsagePersistence(() => eventsCtx, () => {
firstEngineResponseSeen = true;
clearFirstResponseHeartbeat();
});
exitBackstop = releaseRunLock;
process.on("exit", exitBackstop);
initialGitPaths =
syncRepoDir && isGitBackedStash(syncRepoDir) ? new Set(listGitChangedPaths(syncRepoDir)) : new Set();
journal = beginWriteProvenance();
// Step 8 (alpha.9 plan §5.4): the purge sweep. Deterministic, no LLM,
// and first — before anything else this run might touch the archive
// for. Git-backed bundles only; a bundle with no git history of its
// own is left untouched (akm health reports its size instead).
if (setup.primaryStashDir) {
try {
const purge = purgeGracedArchive(setup.primaryStashDir);
if (purge.purgedFiles > 0) {
info(`[improve] archive purge: ${purge.purgedFiles} file(s) across ${purge.purgedDirs} retirement(s) past the ${RETIRE_GRACE_DAYS}d grace period removed`);
}
}
catch (err) {
preEnsureCleanupWarnings.push(`archive purge failed: ${err instanceof Error ? err.message : String(err)}`);
}
}
// The index is made current BEFORE triage (R6): triage promotes into the
// stash, and a reindex after it would always find fresh work.
const bootstrap = await runIndexBootstrapPass(setup, budgetAbortController.signal);
preEnsureCleanupWarnings.push(...bootstrap.warnings);
ensureIndexDurationMs = bootstrap.ensureIndexDurationMs;
triageDrain = await runTriagePrePass(setup);
// Index triage's own writes incrementally so selection sees them.
const triageWrittenPaths = journal?.writtenPaths() ?? [];
if (setup.primaryStashDir && triageWrittenPaths.length > 0) {
await indexWrittenAssets(setup.primaryStashDir, triageWrittenPaths);
}
}
collected = await indexAndCollect(setup);
if (options.dryRun) {
const result = await runDryPlanningStage(setup, collected, preEnsureCleanupWarnings);
clearBudgetTimer();
return result;
}
}
catch (err) {
teardown();
throw err;
}
try {
try {
eventsDb = openStateDatabase(resolvedStateDbPath);
eventsCtx = { db: eventsDb };
}
catch (err) {
rethrowIfTestIsolationError(err);
}
if (!firstEngineResponseSeen) {
const firstResponseTimer = setTimeout(() => {
warn("[improve] Still waiting for the first engine response...");
}, FIRST_ENGINE_RESPONSE_HEARTBEAT_MS);
firstResponseTimer.unref?.();
clearFirstResponseHeartbeat = () => clearTimeout(firstResponseTimer);
}
const seq = await runImproveStageSequence(setup, collected, preEnsureCleanupWarnings, eventsCtx);
const result = finalizeImproveResult({ run: setup, seq, collected, triageDrain, ensureIndexDurationMs, eventsCtx });
// The run's write provenance goes on the envelope before the sync, so
// `writtenPaths` is exactly the set the commit is scoped to.
const writtenPaths = describeRunWrittenPaths(setup, journal?.writtenPaths() ?? []);
if (writtenPaths.length > 0)
result.writtenPaths = writtenPaths;
result.sync = commitStashBatch(result);
return result;
}
catch (err) {
recordImproveFailure(err, setup, eventsCtx);
// Crash/abort safety net (#662): commit what this run already wrote.
// commitStashBatch never throws and no-ops a clean tree.
commitStashBatch({ scope, plannedRefs: collected.plannedRefs, runId: options.runId });
throw err;
}
finally {
teardown();
try {
eventsDb?.close();
}
catch {
// already closed
}
}
}
/**
* The run's journaled paths for `result.writtenPaths`: POSIX-relative to the
* primary stash when inside it, absolute otherwise; deduped and sorted.
*/
function describeRunWrittenPaths(setup, writtenPaths) {
const root = setup.primaryStashDir ?? setup.syncRepoDir;
const described = new Set();
for (const absolutePath of writtenPaths) {
const relative = root ? relativeWrittenPath(root, absolutePath) : undefined;
described.add(relative ?? absolutePath.replaceAll(path.sep, "/"));
}
return [...described].sort();
}
/** How `akm improve` spells its destination flag: the shared target resolvers name it in their errors. */
export const IMPROVE_TARGET_FLAG = "--bundle";
/**
* The source a dry run (or `--show-prompt`) inspects, without adapting it into
* a write target.
*/
export function resolveImproveReadSource(config, scopedRef, explicitTarget, fallbackStashDir) {
if (scopedRef?.origin && explicitTarget && scopedRef.origin !== explicitTarget) {
throw new UsageError(`Qualified ref bundle "${scopedRef.origin}" conflicts with ${IMPROVE_TARGET_FLAG} "${explicitTarget}".`, "INVALID_FLAG_VALUE", `Drop ${IMPROVE_TARGET_FLAG} or use ${IMPROVE_TARGET_FLAG} ${scopedRef.origin}.`);
}
const selector = scopedRef?.origin ?? explicitTarget ?? config.defaultWriteTarget;
if (!selector && fallbackStashDir)
return { source: { name: "stash", path: fallbackStashDir } };
if (!selector && process.env.AKM_BUNDLE_DIR?.trim()) {
// A live run's working bundle starts from AKM_BUNDLE_DIR (`resolveWorkingStashTarget`), so its preview does too.
const { source } = resolveWorkingStashTarget(config, { requireWritable: false });
return { source: { name: source.name, path: source.path } };
}
const configuredSelector = selector ?? config.defaultBundle;
if (configuredSelector) {
const entry = bundlesToSourceEntries(config)?.find((source) => source.name === configuredSelector);
if (!entry) {
throw new UsageError(`No source named "${configuredSelector}" is configured. Run \`akm bundle list\` to see available sources.`, "INVALID_FLAG_VALUE");
}
const sourcePath = resolveEntryContentDir(entry);
if (!sourcePath) {
throw new ConfigError(`Source "${configuredSelector}" has no resolvable on-disk path; improve cannot inspect this entry.`, "INVALID_CONFIG_FILE");
}
return { selector: configuredSelector, source: { name: configuredSelector, path: sourcePath } };
}
const implicit = resolveSourceEntries(undefined, config)[0];
if (!implicit)
throw new ConfigError("no source configured; run `akm bundle create`", "STASH_DIR_NOT_FOUND");
return { source: { name: implicit.registryId ?? "stash", path: implicit.path } };
}
/**
* Run setup, fully synchronous: the budget signal, the invocation plan, the
* write target, the profile-defaulted options, and the state.db and lock paths
* pinned before the first await (C2: a later env change cannot redirect them).
*/
function resolveImproveRunSetup(options) {
const startMs = Date.now();
const budgetMs = options.timeoutMs ?? 2 * 60 * 60 * 1000;
const budgetAbortController = new AbortController();
Object.defineProperty(budgetAbortController.signal, "remainingBudgetMs", {
get: () => Math.max(0, budgetMs - (Date.now() - startMs)),
enumerable: false,
configurable: true,
});
const scope = resolveImproveScope(options.scope);
const config = options.config ?? loadConfig();
const configuredImproveProfile = resolveImproveStrategy(options.strategy, config).config;
// A dry run never dispatches, so an all-disabled strategy must not abort it.
const resolvedPlan = options.resolvedPlan ??
resolveImprovePlan(options.strategy, config, {
repairValidationFailures: options.repairValidationFailures,
allowAllDisabled: options.dryRun,
});
const selectedStrategy = resolvedPlan.strategy;
const improveProfile = selectedStrategy.config;
const configuredLimits = {
...(options.limit !== undefined ? { cli: options.limit } : {}),
...(configuredImproveProfile.limit !== undefined ? { profile: configuredImproveProfile.limit } : {}),
...(configuredImproveProfile.processes?.reflect?.limit !== undefined
? { reflect: configuredImproveProfile.processes.reflect.limit }
: {}),
};
// --limit, then the reflect process limit, then the profile limit.
const effectiveLimit = options.limit ?? improveProfile?.processes?.reflect?.limit ?? improveProfile.limit;
const scopedRef = scope.mode === "ref" && scope.value ? parseRefInput(scope.value) : undefined;
const readSource = options.dryRun
? options.writeTarget
? { selector: options.writeTarget.selector, source: options.writeTarget.source }
: resolveImproveReadSource(config, scopedRef, options.target, options.stashDir)
: undefined;
const writeTarget = options.dryRun
? undefined
: scopedRef?.origin
? resolveMutationTarget(config, scopedRef, options.writeTarget?.source.name ?? options.target, {
flag: IMPROVE_TARGET_FLAG,
}).target
: (options.writeTarget ??
(options.target || config.defaultWriteTarget || !options.stashDir
? resolveWriteTarget(config, options.target, { flag: IMPROVE_TARGET_FLAG })
: {
source: { kind: "filesystem", name: "stash", path: options.stashDir },
config: { type: "filesystem", name: "stash", path: options.stashDir, writable: true },
}));
const selectedSource = writeTarget?.source ?? readSource?.source;
if (!selectedSource)
throw new ConfigError("improve could not resolve a source", "STASH_DIR_NOT_FOUND");
// Every stage reads this one config snapshot; nothing reloads it mid-run.
options = {
...options,
config,
target: writeTarget?.selector ?? readSource?.selector,
sourceName: selectedSource.name,
...(writeTarget ? { writeTarget } : {}),
stashDir: selectedSource.path,
limit: effectiveLimit,
};
let primaryStashDir;
try {
primaryStashDir = resolveSourceEntries(options.stashDir)[0]?.path;
}
catch {
primaryStashDir = undefined;
}
return {
startMs,
budgetMs,
budgetAbortController,
scope,
reflectFn: options.reflectFn ?? akmReflect,
distillFn: options.distillFn ?? akmDistill,
ensureIndexFn: options.ensureIndexFn ?? ensureIndex,
reindexFn: options.reindexFn ?? akmIndex,
drainProposalsFn: options.drainProposalsFn ?? drainProposals,
collectEligibleRefsImpl: options.collectEligibleRefsFn ?? (options.dryRun ? collectEligibleRefsReadOnly : collectEligibleRefs),
runImprovePreparationStageImpl: options.runImprovePreparationStageFn ?? runImprovePreparationStage,
runImproveLoopStageImpl: options.runImproveLoopStageFn ?? runImproveLoopStage,
runImprovePostLoopStageImpl: options.runImprovePostLoopStageFn ?? runImprovePostLoopStage,
config,
resolvedPlan,
selectedStrategy,
improveSensitiveValues: collectEngineCredentialValues(config),
improveProfile,
configuredImproveProfile,
configuredLimits,
effectiveLimit,
writeTarget,
options,
primaryStashDir,
syncRepoDir: writeTarget?.source.repoPath ?? primaryStashDir,
resolvedStateDbPath: getStateDbPathInDataDir(),
// The run lock is machine-local state, kept outside the bundle (#890).
resolvedLockPath: improveLockPath(getStashLocksDir(primaryStashDir ?? options.stashDir ?? ".")),
effectiveSync: { ...improveProfile.sync, ...options.sync },
};
}
/** The redacted `improve_failed` event for a crashed run. */
function recordImproveFailure(err, run, eventsCtx) {
appendEvent({
eventType: "improve_failed",
ref: run.scope.mode === "ref" ? run.scope.value : `improve:${run.scope.mode}:${run.scope.value ?? "all"}`,
metadata: {
strategy: run.selectedStrategy.name,
error: redactSensitiveText(errMessage(err), run.improveSensitiveValues),
durationMs: Date.now() - run.startMs,
},
}, eventsCtx);
}
function probeEntryCount() {
try {
if (!fs.existsSync(getDbPath()))
return undefined;
const db = openExistingDatabase();
try {
return getEntryCount(db);
}
finally {
closeDatabase(db);
}
}
catch (err) {
rethrowIfTestIsolationError(err);
return undefined;
}
}
/**
* ensureIndex before selection (#339): the eligible-ref query reads `entries`,
* which a DB version upgrade may just have dropped. An index that was empty
* and is now populated means an upgrade rebuild happened; say so.
*/
async function runIndexBootstrapPass(run, signal) {
const { primaryStashDir, options } = run;
const warnings = [];
if (!primaryStashDir || options.dryRun)
return { warnings };
const preEnsureEntryCount = probeEntryCount();
let ensureIndexDurationMs;
try {
await run.ensureIndexFn(primaryStashDir, {
mode: "blocking",
signal,
onReindexTiming: ({ durationMs }) => {
ensureIndexDurationMs = durationMs;
},
});
}
catch (err) {
if (signal.aborted)
throw err;
warnings.push(`ensureIndex failed: ${errMessage(err)}`);
}
if (preEnsureEntryCount === 0 && (probeEntryCount() ?? 0) > 0) {
warn("[improve] index was empty after DB version upgrade — repopulating before continuing");
}
return { warnings, ensureIndexDurationMs };
}
/** Candidate selection plus the memory-cleanup plan and the autonomy-gated direct lanes. */
async function indexAndCollect(run) {
const { scope, options, primaryStashDir, improveProfile, config } = run;
const { plannedRefs, memorySummary, strategyFilteredRefs, indexSnapshot } = await run.collectEligibleRefsImpl(scope, options.stashDir, improveProfile, config);
const cleanupParentRef = memoryCleanupParentRef(scope, options.stashDir);
const memoryCleanupPlan = shouldAnalyzeMemoryCleanup(scope, memorySummary.eligible, primaryStashDir)
? analyzeMemoryCleanup(primaryStashDir, cleanupParentRef ? { parentRef: cleanupParentRef } : undefined)
: undefined;
// A lane that would not have run anyway was not suppressed by the gate.
const cleanupWouldMutate = Boolean(memoryCleanupPlan &&
(memoryCleanupPlan.pruneCandidates.length > 0 ||
memoryCleanupPlan.beliefStateTransitions.length > 0 ||
memoryCleanupPlan.relativeDateCandidates.length > 0));
return {
plannedRefs,
memorySummary,
strategyFilteredRefs: strategyFilteredRefs ?? [],
indexSnapshot,
memoryCleanupPlan,
autonomyGatedDirectLanes: cleanupWouldMutate && !isAutonomyLaneAllowed("memoryCleanup", config) ? ["memoryCleanup"] : [],
guidance: memorySummary.eligible > 0
? "Improve folds memory cleanup into the same proposal queue: speculative promotions still go through reflect/distill proposals, while high-confidence redundant derived memories are moved into a recoverable cleanup archive instead of being left active in the stash."
: undefined,
};
}
/** The envelope for a run skipped because another run holds the lock. */
export function buildLockSkippedResult(strategyName, scope, runId) {
return {
schemaVersion: 2,
ok: true,
strategy: strategyName,
scope,
dryRun: false,
skipped: { reason: "lock-held" },
memorySummary: { eligible: 0, derived: 0 },
plannedRefs: [],
actions: [],
...(runId !== undefined ? { runId } : {}),
};
}
/** A dry run: the real selectors on a read-only state.db snapshot, stopping before every writer. */
async function runDryPlanningStage(run, collected, initialCleanupWarnings) {
let stateDb;
let stateSnapshotUnavailable = false;
try {
try {
stateDb = openSqliteReadSnapshot(run.resolvedStateDbPath);
stateSnapshotUnavailable = !stateDb;
}
catch (error) {
if (!(error instanceof SqliteReadSnapshotUnavailableError))
throw error;
stateSnapshotUnavailable = true;
}
const preparation = await run.runImprovePreparationStageImpl({
...preparationArgs(run, collected, initialCleanupWarnings),
eventsCtx: {
...(stateDb ? { db: stateDb } : { dbPath: run.resolvedStateDbPath }),
readOnly: true,
...(stateSnapshotUnavailable ? { readOnlySnapshotUnavailable: true } : {}),
},
planOnly: true,
});
return buildDryRunResult(run, collected, preparation);
}
finally {
stateDb?.close();
}
}
function preparationArgs(run, collected, initialCleanupWarnings) {
return {
scope: run.scope,
options: run.options,
plannedRefs: collected.plannedRefs,
memoryCleanupPlan: collected.memoryCleanupPlan,
primaryStashDir: run.primaryStashDir,
memorySummary: collected.memorySummary,
reindexFn: run.reindexFn,
startMs: run.startMs,
budgetMs: run.budgetMs,
initialCleanupWarnings,
improveProfile: run.improveProfile,
resolvedPlan: run.resolvedPlan,
strategyName: run.selectedStrategy.name,
budgetSignal: run.budgetAbortController.signal,
};
}
/** The plan-only envelope of a dry run. */
export function buildDryRunResult(run, collected, preparation) {
const { guidance, memorySummary, memoryCleanupPlan, plannedRefs, strategyFilteredRefs } = collected;
const notices = collectImproveNotices(run.resolvedPlan, []);
return {
schemaVersion: 2,
ok: true,
strategy: run.selectedStrategy.name,
scope: run.scope,
dryRun: true,
...notices.fields(),
...(guidance ? { guidance } : {}),
memorySummary,
...(memoryCleanupPlan ? { memoryCleanup: shapeMemoryCleanup(memoryCleanupPlan) } : {}),
plannedRefs: preparation?.loopRefs ?? plannedRefs,
...(preparation?.planning ? { plan: buildResultExecutionPlan(run, preparation, collected, true) } : {}),
...(strategyFilteredRefs.length > 0 ? { strategyFilteredRefs } : {}),
...(run.resolvedPlan.engineUnavailable.length > 0 ? { skippedProcesses: run.resolvedPlan.engineUnavailable } : {}),
...(run.options.engineProbe !== undefined ? { engineProbe: run.options.engineProbe } : {}),
...(preparation?.proactiveMaintenance ? { proactiveMaintenance: preparation.proactiveMaintenance } : {}),
};
}
/** The public execution-plan projection shared by the dry and live results. */
function buildResultExecutionPlan(run, preparation, collected, dryRun) {
const { improveProfile, configuredImproveProfile, resolvedPlan, scope } = run;
const { strategyFilteredRefs } = collected;
const configuredTriage = configuredImproveProfile.processes?.triage;
const inferenceMinPending = improveProfile.processes?.memoryInference?.minPendingCount;
const pendingMemories = run.primaryStashDir && inferenceMinPending !== undefined && inferenceMinPending > 0
? collectPendingMemories(run.primaryStashDir).length
: undefined;
const belowMinPending = pendingMemories !== undefined && inferenceMinPending !== undefined && pendingMemories < inferenceMinPending;
const memoryInferenceEnabled = resolvedPlan.processes.memoryInference.enabled && !belowMinPending;
// Per-process routing, plus how many effective refs each ref-scoped process would act on (#947).
const processes = projectResolvedProcessRouting(resolvedPlan).map((row) => {
const eligibleRefs = eligibleRefCount(preparation.loopRefs, row.process, resolvedPlan.strategy.config);
return eligibleRefs === undefined ? row : { ...row, eligibleRefs };
});
const proactive = preparation.planning.proactive
? {
...preparation.planning.proactive,
configured: pickDefined(configuredImproveProfile.processes?.proactiveMaintenance, [
"dueDays",
"maxPerRun",
"limit",
]),
}
: undefined;
return buildImproveExecutionPlan({
dryRun,
snapshot: collected.indexSnapshot ?? {
status: "unknown",
reason: "the injected selector did not report an index snapshot status",
},
rawInScope: collected.plannedRefs.length + strategyFilteredRefs.length,
selectedRefs: preparation.actionableRefs,
effectiveRefs: preparation.loopRefs,
distillOnlyRefs: new Set(preparation.distillOnlyRefs.map((entry) => entry.ref)),
configuredLimits: run.configuredLimits,
effectiveLimit: run.effectiveLimit,
gates: [
{
name: "profile",
removed: strategyFilteredRefs.length,
reason: "all enabled per-ref processes refuse the asset type",
},
...preparation.planning.gates,
],
processes,
...(proactive ? { proactive } : {}),
consolidation: {
...preparation.planning.consolidation,
configured: pickDefined(configuredImproveProfile.processes?.consolidate, CONSOLIDATION_CONFIG_KEYS),
},
stageConfig: {
extract: { enabled: preparation.planning.extract.wouldRun, reason: preparation.planning.extract.reason },
memoryInference: {
enabled: memoryInferenceEnabled,
reason: !resolvedPlan.processes.memoryInference.enabled
? "disabled"
: !memoryInferenceEnabled
? `${pendingMemories ?? 0} pending memories is below minPendingCount ${inferenceMinPending}`
: "enabled; the pass discovers pending memories independently of selected refs",
},
},
triage: {
enabled: scope.mode !== "ref" && resolvedPlan.processes.triage.enabled,
configuredMode: configuredTriage?.applyMode ?? "queue",
mode: improveProfile.processes?.triage?.applyMode ?? "queue",
maxAcceptsPerRun: configuredTriage?.maxAcceptsPerRun ?? 25,
},
});
}
/** Drain the proposal backlog before generating more (non-fatal; a single-ref scope never drains). */
async function runTriagePrePass(run) {
const { primaryStashDir, resolvedPlan, scope, options, improveProfile } = run;
if (!primaryStashDir || !resolvedPlan.processes.triage.enabled)
return undefined;
if (scope.mode === "ref") {
warn("[improve] triage pre-pass skipped (single-ref scope never drains the whole queue)");
return undefined;
}
try {
const triageConfig = improveProfile.processes?.triage;
return await withLlmStage("triage", () => run.drainProposalsFn({
stashDir: primaryStashDir,
...(options.target ? { target: options.target } : {}),
config: options.config,
applyMode: triageConfig?.applyMode ?? "queue",
maxAccepts: triageConfig?.maxAcceptsPerRun ?? 25,
dryRun: false,
excludeIds: new Set(),
judgment: resolvedPlan.triageJudgment,
}), { engine: resolvedPlan.triageJudgment?.engine, process: "triage.judgment" });
}
catch (err) {
warn(`[improve] triage pre-pass failed (non-fatal): ${errMessage(err)}`);
return undefined;
}
}
/**
* The exact repo-relative paths the auto-sync commit stages (#652): the paths
* this run wrote that Git reports changed (a journaled path already dirty at
* start stays in — this run rewrote it). Without a journal, the pre-#652
* dirty-path diff. `unattributed` counts in-scope paths that went dirty during
* the run without this run writing them (concurrent edits, left alone). Lock
* files are never staged.
*/
export function resolveSyncPathSet(input) {
const { repoDir, assetPrefix, changedPaths, initialPaths, writtenPaths, provenance } = input;
const inScope = (relativePath) => !path.basename(relativePath).includes(".lock") &&
(!assetPrefix || relativePath === assetPrefix || relativePath.startsWith(`${assetPrefix}/`));
if (!provenance) {
return { paths: changedPaths.filter((p) => !initialPaths.has(p) && inScope(p)), unattributed: [] };
}
const changed = new Set(changedPaths);
const attributed = new Set();
for (const absolutePath of writtenPaths) {
const relativePath = relativeWrittenPath(repoDir, absolutePath);
if (relativePath && changed.has(relativePath) && inScope(relativePath))
attributed.add(relativePath);
}
return {
paths: [...attributed].sort(),
unattributed: changedPaths.filter((p) => !attributed.has(p) && !initialPaths.has(p) && inScope(p)).sort(),
};
}
/**
* The auto-sync commit (#662), used at end of run and from the crash path:
* idempotent and never throws. The getters read bindings reassigned after this
* factory runs.
*/
function makeCommitStashBatch(deps) {
const { writeTarget, primaryStashDir, effectiveSync, options, config, improveProfile } = deps.run;
return (messageContext) => {
const eventsCtx = deps.getEventsCtx();
const writeJournal = deps.getWriteJournal();
const repoDir = writeTarget?.source.repoPath ?? primaryStashDir;
if (!primaryStashDir || !repoDir || effectiveSync.enabled === false || !isGitBackedStash(repoDir)) {
return undefined;
}
const saveGitStashFn = options.saveGitStashFn ?? saveGitStash;
const writableOverride = writeTarget ? resolveWritable(writeTarget.config) : resolveWritableOverride(config);
const push = options.sync?.push ?? improveProfile.sync?.push ?? true;
const message = renderSyncCommitMessage(effectiveSync.message ?? "akm improve auto-sync", messageContext, Date.now());
const record = (metadata) => appendEvent({ eventType: "stash_synced", metadata }, eventsCtx);
try {
const assetRoot = writeTarget?.source.path ?? primaryStashDir;
const { paths, unattributed } = resolveSyncPathSet({
repoDir,
assetPrefix: path.relative(repoDir, assetRoot).replaceAll(path.sep, "/"),
changedPaths: listGitChangedPaths(repoDir),
initialPaths: deps.getInitialGitPaths(),
writtenPaths: writeJournal?.writtenPaths() ?? [],
provenance: writeJournal !== undefined,
});
if (unattributed.length > 0) {
warnVerbose(`[improve] auto-sync left ${unattributed.length} path(s) uncommitted — not written by this run: ${unattributed.join(", ")}`);
}
const syncResult = saveGitStashFn(undefined, message, writableOverride, { push, repoDir, paths });
record({
committed: syncResult.committed,
pushed: syncResult.pushed,
skipped: syncResult.skipped,
reason: syncResult.reason ?? null,
attributed: paths.length,
unattributed: unattributed.length,
});
return {
committed: syncResult.committed,
pushed: syncResult.pushed,
skipped: syncResult.skipped,
...(syncResult.reason !== undefined ? { reason: syncResult.reason } : {}),
};
}
catch (syncErr) {
const reason = errMessage(syncErr);
warn(`improve: stash sync failed (non-fatal): ${reason}`);
record({ committed: false, pushed: false, skipped: true, reason });
return { committed: false, pushed: false, skipped: true, reason };
}
};
}
/**
* The audit events for refs and lanes this run will not touch, then
* preparation → loop → post-loop. No post-loop work starts past the budget; the
* result still finalizes, so budget exhaustion exits 0.
*/
async function runImproveStageSequence(run, collected, preEnsureCleanupWarnings, eventsCtx) {
const { scope, options, primaryStashDir, improveProfile, resolvedPlan, budgetAbortController } = run;
const strategy = run.selectedStrategy.name;
// One count-only row for planner-filtered refs, never one per ref (#592).
if (collected.strategyFilteredRefs.length > 0) {
recordImproveSkip(eventsCtx, undefined, {
strategy,
reason: "strategy_filtered_all_passes",
count: collected.strategyFilteredRefs.length,
});
}
// A gated lane names the config key that would enable it.
for (const lane of [...resolvedPlan.autonomyGated, ...describeGatedLanes(collected.autonomyGatedDirectLanes)]) {
warn(`[improve] ${lane.lane} skipped — it ${lane.reason}. Set \`${lane.configKey}: true\` to enable it.`);
recordImproveSkip(eventsCtx, undefined, {
strategy,
reason: "autonomy_gated",
lane: lane.lane,
configKey: lane.configKey,
});
}
for (const item of resolvedPlan.engineUnavailable) {
warn(`[improve] ${item.process} skipped — it ${item.reason}.`);
recordImproveSkip(eventsCtx, undefined, {
strategy,
reason: "engine_unavailable",
process: item.process,
configKey: item.configKey,
});
}
const preparation = await run.runImprovePreparationStageImpl({
...preparationArgs(run, collected, preEnsureCleanupWarnings),
eventsCtx,
});
const loopResult = await run.runImproveLoopStageImpl({
eventsCtx,
budgetSignal: budgetAbortController.signal,
primaryStashDir,
scope,
options,
reflectFn: run.reflectFn,
distillFn: run.distillFn,
loopRefs: preparation.loopRefs,
actions: preparation.actions,
signalBearingSet: preparation.signalBearingSet,
distillCooledRefs: preparation.distillCooledRefs,
distillOnlyRefs: preparation.distillOnlyRefs,
recentErrors: preparation.recentErrors,
startMs: run.startMs,
budgetMs: run.budgetMs,
improveProfile,
resolvedPlan,
});
let postLoop = { allWarnings: [], memoryInferenceDurationMs: 0 };
const remainingBudget = budgetAbortController.signal.remainingBudgetMs;
if (budgetAbortController.signal.aborted || (remainingBudget !== undefined && remainingBudget <= 0)) {
info("[improve] post-loop maintenance skipped (wall-clock budget exhausted)");
}
else {
postLoop = await run.runImprovePostLoopStageImpl({
scope,
options,
primaryStashDir,
actionableRefs: preparation.actionableRefs,
appliedCleanup: preparation.appliedCleanup,
cleanupWarnings: preparation.cleanupWarnings,
memoryRefsForInference: loopResult.memoryRefsForInference,
eventsCtx,
budgetSignal: budgetAbortController.signal,
improveProfile,
resolvedPlan,
});
}
return {
preparation,
postLoop,
reflectsWithErrorContext: loopResult.reflectsWithErrorContext,
finalActions: [...preparation.actions, ...(postLoop.maintenanceActions ?? [])],
};
}
/** Assemble the result envelope and emit `improve_completed`. */
function finalizeImproveResult(args) {
const { run, collected, triageDrain, ensureIndexDurationMs, eventsCtx } = args;
const { preparation, postLoop, reflectsWithErrorContext, finalActions } = args.seq;
const { options, startMs, resolvedPlan } = run;
const { memoryCleanupPlan, strategyFilteredRefs } = collected;
const consolidation = preparation.consolidation;
const { memoryInference, allWarnings, deadUrls, deadUrlCoverage, orphansPurged, proposalsExpired } = postLoop;
const { memoryInferenceDurationMs } = postLoop;
// The per-ref distill-skipped rows fold into a bounded aggregate before persistence (C1).
const { actions: persistedActions, aggregate: distillSkippedAggregate } = foldDistillSkipped(finalActions);
// This run's LLM accounting (#944): llm_usage rows carry no run id, so the
// read is bounded by the run's own wall clock.
const usageEvents = readEvents({ since: new Date(startMs).toISOString(), type: LLM_USAGE_EVENT }, eventsCtx).events;
const usageReport = buildImproveUsageReport({
resolvedPlan,
byProcessEngineModel: summarizeLlmUsageCrossTab(usageEvents),
strategyFilteredRefsCount: strategyFilteredRefs.length,
loopRefs: preparation.loopRefs,
persistedActions,
distillSkippedAggregate,
});
const notices = collectImproveNotices(resolvedPlan, [
...finalActions.map((action) => action.result),
...preparation.schemaRepairs,
consolidation,
...(preparation.extract ?? []).flatMap((extract) => [extract, ...(extract.sessions ?? [])]),
memoryInference,
triageDrain,
]);
const applied = preparation.appliedCleanup;
const countMode = (mode) => finalActions.filter((a) => a.mode === mode).length;
const result = {
schemaVersion: 2,
ok: true,
strategy: run.selectedStrategy.name,
scope: run.scope,
dryRun: false,
...notices.fields(),
...(collected.guidance ? { guidance: collected.guidance } : {}),
memorySummary: collected.memorySummary,
...(memoryCleanupPlan
? {
memoryCleanup: {
...shapeMemoryCleanup(memoryCleanupPlan),
...(applied
? {
archived: applied.archived,
...(applied.transitionLogPath ? { transitionLogPath: applied.transitionLogPath } : {}),
...(applied.transitionLogEntries !== undefined
? { transitionLogEntries: applied.transitionLogEntries }
: {}),
...(allWarnings.length > 0 ? { warnings: allWarnings } : {}),
}
: preparation.cleanupWarnings.length > 0
? { warnings: preparation.cleanupWarnings }
: {}),
},
}
: {}),
plannedRefs: preparation.loopRefs,
...(preparation.planning ? { plan: buildResultExecutionPlan(run, preparation, collected, false) } : {}),
...(strategyFilteredRefs.length > 0 ? { strategyFilteredRefs } : {}),
...(resolvedPlan.engineUnavailable.length > 0 ? { skippedProcesses: resolvedPlan.engineUnavailable } : {}),
...(options.engineProbe !== undefined ? { engineProbe: options.engineProbe } : {}),
...(usageReport ? { usageReport } : {}),
actions: persistedActions,
...(distillSkippedAggregate ? { distillSkipped: distillSkippedAggregate } : {}),
...(preparation.validationFailures.length > 0 ? { validationFailures: preparation.validationFailures } : {}),
...(preparation.schemaRepairs.length > 0 ? { schemaRepairs: preparation.schemaRepairs } : {}),
...(consolidation.processed > 0 || consolidation.warnings.length > 0 ? { consolidation } : {}),
...(preparation.lintSummary !== undefined ? { lintSummary: preparation.lintSummary } : {}),
...(preparation.memoryIndexHealth !== undefined ? { memoryIndexHealth: preparation.memoryIndexHealth } : {}),
...(preparation.coverageGaps.length > 0 ? { coverageGaps: preparation.coverageGaps } : {}),
...(preparation.extract && preparation.extract.length > 0 ? { extract: preparation.extract } : {}),
...(deadUrls !== undefined && deadUrls.length > 0 ? { deadUrls } : {}),
// Present whenever the check ran, so health knows the coverage of a clean run (#892).
...(deadUrlCoverage !== undefined ? { deadUrlCoverage } : {}),
...(reflectsWithErrorContext > 0 ? { reflectsWithErrorContext } : {}),
...(memoryInference ? { memoryInference } : {}),
// Top-level phase durations feed health's wall-time buckets; a phase that
// did not run is omitted, not zero.
...(memoryInferenceDurationMs > 0 ? { memoryInferenceDurationMs } : {}),
...(ensureIndexDurationMs !== undefined ? { ensureIndexDurationMs } : {}),
...(orphansPurged !== undefined ? { orphansPurged } : {}),
...(proposalsExpired !== undefined && proposalsExpired > 0 ? { proposalsExpired } : {}),
reflectCooldownActions: countMode("reflect-cooldown"),
reflectSkippedActions: countMode("reflect-skipped"),
reflectGuardRejectedActions: countMode("reflect-guard-rejected"),
...(triageDrain
? {
triage: {
promoted: triageDrain.promoted.length,
rejected: triageDrain.rejected.length,
deferred: triageDrain.deferred.length,
failed: triageDrain.failed.length,
skippedByCap: triageDrain.skippedByCap.length,
},
}
: {}),
...(preparation.proactiveMaintenance ? { proactiveMaintenance: preparation.proactiveMaintenance } : {}),
...(options.runId !== undefined ? { runId: options.runId } : {}),
};
emitImproveCompletedEvent(result, {
memoryInferenceDurationMs,
totalDurationMs: Date.now() - startMs,
warningCount: allWarnings.length,
orphansPurged: orphansPurged ?? 0,
}, eventsCtx);
return result;
}
/** Every lowering notice the plan and this run's stage results carry, deduplicated. */
function collectImproveNotices(resolvedPlan, carriers) {
const notices = noticeSet();
const collect = (carrier) => {
const list = carrier?.notices;
if (typeof carrier === "object" && Array.isArray(list))
notices.add(list);
};
for (const process of Object.values(resolvedPlan.processes))
collect(process);
notices.add(resolvedPlan.triageJudgmentNotices ?? []);
for (const carrier of carriers)
collect(carrier);
return notices;
}
/** `improve_completed` per-mode counters; keyed by every mode so a new one cannot be dropped. */
const ACTION_COUNTER = {
reflect: "reflectActions",
distill: "distillActions",
"distill-skipped": "distillSkippedActions",
"memory-prune": "memoryPruneActions",
"memory-inference": "memoryInferenceActions",
error: "errorActions",
"reflect-failed": "reflectFailedActions",
"reflect-cooldown": "reflectCooldownActions",
"reflect-skipped": "reflectSkippedActions",
"reflect-guard-rejected": "reflectGuardRejectedActions",
};
function emitImproveCompletedEvent(result, durations, eventsCtx) {
const counts = Object.fromEntries(Object.values(ACTION_COUNTER).map((key) => [key, 0]));
// The coarse buckets come from the same classifier the persisted metrics use.
const classCounts = { accepted: 0, rejected: 0, skipped: 0, error: 0, noop: 0 };
for (const action of result.actions ?? []) {
const key = ACTION_COUNTER[action.mode];
if (key)
counts[key] = (counts[key] ?? 0) + 1;
classCounts[classifyImproveAction(action.mode)] += 1;
}
// distill-skipped rows were folded into the aggregate; count them back in.
const distillSkippedTotal = result.distillSkipped?.total ?? 0;
counts.distillSkippedActions = (counts.distillSkippedActions ?? 0) + distillSkippedTotal;
classCounts.skipped += distillSkippedTotal;
const cleanup = result.memoryCleanup;
appendEvent({
eventType: "improve_completed",
ref: result.scope.mode === "ref"
? result.scope.value
: `improve:${result.scope.mode}:${result.scope.value ?? "all"}`,
metadata: {
strategy: result.strategy,
plannedRefs: result.plannedRefs.length,
...counts,
acceptedActions: classCounts.accepted,
rejectedActions: classCounts.rejected,
skippedActions: classCounts.skipped,
noopActions: classCounts.noop,
reflectsWithErrorContext: result.reflectsWithErrorContext ?? 0,
coverageGapCount: result.coverageGaps?.length ?? 0,
deadUrlCount: result.deadUrls?.length ?? 0,
deadUrlsChecked: result.deadUrlCoverage?.checked ?? 0,
deadUrlsTotal: result.deadUrlCoverage?.total ?? 0,
deadUrlsSkipped: result.deadUrlCoverage?.skipped ?? 0,
memoryEligible: result.memorySummary.eligible,
memoryDerived: result.memorySummary.derived,
memoryCleanupPruneCandidates: cleanup?.pruneCandidates.length ?? 0,
memoryCleanupContradictionCandidates: cleanup?.contradictionCandidates.length ?? 0,
memoryCleanupBeliefStateTransitions: cleanup?.beliefStateTransitions.length ?? 0,
memoryCleanupConsolidationCandidates: cleanup?.consolidationCandidates.length ?? 0,
memoryCleanupArchived: cleanup?.archived?.length ?? 0,
memoryCleanupWarnings: cleanup?.warnings?.length ?? 0,
consolidationProcessed: result.consolidation?.processed ?? 0,
consolidationDurationMs: result.consolidation?.durationMs ?? 0,
memoryInferenceWrites: result.memoryInference?.writtenFacts ?? 0,
memoryInferenceDurationMs: durations.memoryInferenceDurationMs,
proactiveSelected: result.proactiveMaintenance?.selected ?? 0,
proactiveDueTotal: result.proactiveMaintenance?.dueTotal ?? 0,
proactiveNeverReflected: result.proactiveMaintenance?.neverReflected ?? 0,
durationMs: durations.totalDurationMs,
warningCount: durations.warningCount,
orphansPurged: durations.orphansPurged,
},
}, eventsCtx);
}
function shapeMemoryCleanup(plan) {
return {
analyzedDerived: plan.analyzedDerived,
pruneCandidates: plan.pruneCandidates,
contradictionCandidates: plan.contradictionCandidates,
beliefStateTransitions: plan.beliefStateTransitions,
consolidationCandidates: plan.consolidationCandidates,
...(plan.relativeDateCandidates.length > 0 ? { relativeDateCandidates: plan.relativeDateCandidates } : {}),
};
}