akm-cli 0.9.0-beta.5 → 0.9.0-beta.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -6,6 +6,54 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.9.0-beta.6] - 2026-06-12
10
+
11
+ Pipeline optimization: new per-process config fields wire up the consolidation
12
+ and improve pipeline knobs exposed by the optimization report — incremental
13
+ consolidation, pool caps, distill gating, and memory inference throttling.
14
+
15
+ ### Added
16
+
17
+ - **`consolidate.incrementalSince`** — profile config field that narrows the
18
+ consolidation candidate pool to memories modified within the given window
19
+ (e.g. `"1h"`, `"4h"`) plus their graph neighbours. Enables frequent
20
+ consolidation passes (e.g. `quick-shredder` every 15 min) without full-pool
21
+ sweeps. Absent = full-pool sweep (correct for nightly runs).
22
+ - **`consolidate.limit`** — hard cap on memories processed per consolidation
23
+ pass, applied after incremental narrowing. Prevents runaway full-pool sweeps
24
+ in the nightly default profile.
25
+ - **`consolidate.neighborsPerChanged`** — configurable graph-neighbour count
26
+ per changed memory during incremental consolidation (was hardcoded to 5).
27
+ `quick-shredder` sets this to 3 for a 40% candidate reduction per burst.
28
+ - **`distill.requirePlannedRefs`** — when `true`, the distill process is
29
+ skipped entirely for distill-only refs when the reflect phase produced zero
30
+ planned refs. Eliminates hundreds of `distill-skipped` events on quiet passes
31
+ where all refs are on reflect cooldown.
32
+ - **`memoryInference.minPendingCount`** — minimum pending split-parent memory
33
+ count below which the inference pass is skipped entirely (zero LLM calls).
34
+ Prevents lock acquisition on passes where there is nothing to infer.
35
+ - **`reflect.limit`** — per-process ref limit for the reflect/distill loop,
36
+ applied as the improve run limit when no CLI `--limit` is given.
37
+ - **New `reflect-distill` improve profile** — dedicated reflect + distill +
38
+ memoryInference + triage profile for the every-4h `akm-improve-frequent`
39
+ task. `reflect.limit: 25` bounds LLM cost per pass.
40
+
41
+ ### Changed
42
+
43
+ - **`quick-shredder` profile tuned**: `incrementalSince` `4h` → `1h`,
44
+ `maxChunkSize` 25 → 35, added `minPoolSize: 10`, `neighborsPerChanged: 3`,
45
+ `memoryInference.minPendingCount: 5`. All `profile: "qwen-9b-shredder"`
46
+ process references removed — falls back to default LLM.
47
+ - **`default` improve profile** (nightly): extract disabled (dedicated
48
+ `akm-extract` task runs at 01:48), consolidate gets `limit: 500`,
49
+ reflect gets `limit: 100` and `allowedTypes`, distill gets
50
+ `requirePlannedRefs: true`, triage enabled at 50 accepts/run,
51
+ graphExtraction explicitly enabled.
52
+ - **Cron schedule optimised**: extract reverted to `8,28,48 * * * *` (3×/hr),
53
+ quick-shredder shifted to `4,19,34,49` (4-min extract gap), health-report
54
+ shifted to `:03` (avoids `:00` collision), `akm-improve-frequent` re-enabled
55
+ at `45 */4` with `reflect-distill` profile.
56
+
9
57
  ## [0.9.0-beta.3] - 2026-06-12
10
58
 
11
59
  Stabilization batch closing the remaining 0.9.0 milestone: DB-locking and
@@ -809,7 +809,7 @@ export async function akmConsolidate(opts = {}) {
809
809
  };
810
810
  }
811
811
  if (opts.incrementalSince) {
812
- memories = narrowToIncrementalCandidates(memories, opts.incrementalSince, warnings);
812
+ memories = narrowToIncrementalCandidates(memories, opts.incrementalSince, warnings, opts.neighborsPerChanged);
813
813
  if (memories.length === 0) {
814
814
  return {
815
815
  schemaVersion: 1,
@@ -828,6 +828,10 @@ export async function akmConsolidate(opts = {}) {
828
828
  };
829
829
  }
830
830
  }
831
+ if (opts.limit !== undefined && memories.length > opts.limit) {
832
+ warnings.push(`Consolidation: pool capped at ${opts.limit} memories (limit option).`);
833
+ memories = memories.slice(0, opts.limit);
834
+ }
831
835
  // Consolidation always uses the HTTP LLM client directly — never the agent
832
836
  // CLI. The agent CLI is for interactive agent sessions (reflect, propose);
833
837
  // structured JSON generation works better and faster via HTTP.
@@ -2004,7 +2008,7 @@ function parseSinceToIso(since) {
2004
2008
  const multiplier = { m: 60_000, h: 3_600_000, d: 86_400_000 }[m[2]];
2005
2009
  return new Date(Date.now() - parseInt(m[1], 10) * multiplier).toISOString();
2006
2010
  }
2007
- export function narrowToIncrementalCandidates(memories, since, warnings) {
2011
+ export function narrowToIncrementalCandidates(memories, since, warnings, neighborsPerChanged = 5) {
2008
2012
  const sinceIso = parseSinceToIso(since);
2009
2013
  const isChanged = (m) => {
2010
2014
  try {
@@ -2019,7 +2023,6 @@ export function narrowToIncrementalCandidates(memories, since, warnings) {
2019
2023
  return [];
2020
2024
  if (changed.length === memories.length)
2021
2025
  return memories;
2022
- const NEIGHBORS_PER_CHANGED = 5;
2023
2026
  const byName = new Map(memories.map((m) => [m.name, m]));
2024
2027
  const keep = new Set(changed.map((m) => m.name));
2025
2028
  let db;
@@ -2029,7 +2032,7 @@ export function narrowToIncrementalCandidates(memories, since, warnings) {
2029
2032
  const id = findEntryIdByRef(db, `memory:${m.name}`);
2030
2033
  if (id === undefined)
2031
2034
  continue;
2032
- for (const hit of getNeighborsByEntryId(db, id, NEIGHBORS_PER_CHANGED + 1)) {
2035
+ for (const hit of getNeighborsByEntryId(db, id, neighborsPerChanged + 1)) {
2033
2036
  if (hit.id === id)
2034
2037
  continue;
2035
2038
  const entry = getEntryById(db, hit.id);
@@ -20,7 +20,7 @@ import { closeDatabase, getAllEntries, getEntryCount, getRetrievalCounts, getUti
20
20
  import { ensureIndex } from "../../indexer/ensure-index.js";
21
21
  import { runGraphExtractionPass } from "../../indexer/graph/graph-extraction.js";
22
22
  import { akmIndex } from "../../indexer/indexer.js";
23
- import { runMemoryInferencePass } from "../../indexer/passes/memory-inference.js";
23
+ import { collectPendingMemories, runMemoryInferencePass, } from "../../indexer/passes/memory-inference.js";
24
24
  import { runStalenessDetectionPass } from "../../indexer/passes/staleness-detect.js";
25
25
  import { getWritableStashDirs, resolveSourceEntries } from "../../indexer/search/search-source.js";
26
26
  import { countUsageEventsByType } from "../../indexer/usage/usage-events.js";
@@ -471,7 +471,9 @@ export async function akmImprove(options = {}) {
471
471
  options = {
472
472
  ...options,
473
473
  autoAccept: options.autoAccept ?? improveProfile.autoAccept,
474
- limit: options.limit ?? improveProfile.limit,
474
+ // Profile-level limit, then process-level reflect.limit as fallback.
475
+ // CLI --limit takes precedence over both.
476
+ limit: options.limit ?? improveProfile?.processes?.reflect?.limit ?? improveProfile.limit,
475
477
  };
476
478
  let primaryStashDir;
477
479
  try {
@@ -1385,13 +1387,13 @@ async function runConsolidationPass(args) {
1385
1387
  // Tie consolidate proposals back to this improve invocation so
1386
1388
  // accept-rate-per-run aggregation works. Mirrors reflect/propose/extract.
1387
1389
  sourceRun: `consolidate-${Date.now()}`,
1388
- // Full-pool sweep: consolidation only runs on the nightly default-profile
1389
- // pass (quick/frequent disable it), so a complete re-cluster is correct and
1390
- // affordable here. Do NOT pass incrementalSince the time-window narrowing
1391
- // it triggers permanently excludes stale-but-unmerged duplicate clusters,
1392
- // starving merge recall and letting the pool grow unbounded. (The narrowing
1393
- // was a band-aid for an every-30-min consolidation cadence that the profile
1394
- // split has since eliminated.) lastConsolidateTs still gates whether we run.
1390
+ // Pass profile-configured options. incrementalSince narrows the pool to
1391
+ // recently-changed memories + graph neighbours use this for frequent
1392
+ // passes (quick-shredder). Leave absent in the nightly default profile for
1393
+ // a full-pool sweep that catches stale-but-unmerged duplicates.
1394
+ incrementalSince: improveProfile?.processes?.consolidate?.incrementalSince,
1395
+ limit: improveProfile?.processes?.consolidate?.limit,
1396
+ neighborsPerChanged: improveProfile?.processes?.consolidate?.neighborsPerChanged,
1395
1397
  maxChunkSize: improveProfile?.processes?.consolidate?.maxChunkSize,
1396
1398
  // Honor profile.autoAccept (already merged into options.autoAccept at the
1397
1399
  // top of akmImprove). The CLI parser always supplies 90 when --auto-accept
@@ -2067,6 +2069,14 @@ async function runImproveLoopStage(args) {
2067
2069
  // receives only its fair share of the wall-clock budget.
2068
2070
  const remainingBudgetMs = () => Math.max(0, budgetMs - (Date.now() - startMs));
2069
2071
  const RECENT_ERRORS_CAP = 3;
2072
+ // requirePlannedRefs guard: when the distill profile sets this flag, skip
2073
+ // distill for distill-only refs if the reflect phase produced no planned refs.
2074
+ // Prevents the distill loop from generating hundreds of distill-skipped events
2075
+ // on quiet passes (all refs on reflect cooldown, no new signal to distill).
2076
+ const requirePlannedRefs = improveProfile?.processes?.distill?.requirePlannedRefs === true;
2077
+ const _distillOnlyRefNames = new Set(distillOnlyRefs.map((r) => r.ref));
2078
+ const hasReflectEligibleRefs = loopRefs.some((r) => !_distillOnlyRefNames.has(r.ref));
2079
+ const skipDistillDueToRequirePlannedRefs = requirePlannedRefs && !hasReflectEligibleRefs;
2070
2080
  // R-2 / #389: Self-Consistency multi-sample voting helpers.
2071
2081
  // Wang et al. arXiv:2203.11171 — N=3 samples beat single-shot on reasoning tasks.
2072
2082
  const SC_THRESHOLD = options.selfConsistencyThreshold ?? 0.7;
@@ -2364,6 +2374,18 @@ async function runImproveLoopStage(args) {
2364
2374
  info(`[improve] ${completedCount}/${loopRefs.length} ${planned.ref}`);
2365
2375
  continue;
2366
2376
  }
2377
+ // requirePlannedRefs guard: skip distill for distill-only refs when no
2378
+ // reflect-eligible refs were planned this run, preventing mass skip events.
2379
+ if (skipDistillDueToRequirePlannedRefs && isDistillOnly) {
2380
+ actions.push({
2381
+ ref: planned.ref,
2382
+ mode: "distill-skipped",
2383
+ result: { ok: true, reason: "require_planned_refs" },
2384
+ });
2385
+ completedCount++;
2386
+ info(`[improve] ${completedCount}/${loopRefs.length} ${planned.ref}`);
2387
+ continue;
2388
+ }
2367
2389
  // See `isDistillCandidateRef` — excludes `lesson:*` (and anything else in
2368
2390
  // DISTILL_REFUSED_INPUT_TYPES) so distill never gets queued for an input
2369
2391
  // it will refuse.
@@ -2634,9 +2656,23 @@ export async function runImproveMaintenancePasses(args) {
2634
2656
  // candidates from the filesystem-of-truth. The this-run set is still
2635
2657
  // logged as a hint but no longer used as a filter.
2636
2658
  const memoryInferenceDisabledByProfile = improveProfile?.processes?.memoryInference?.enabled === false;
2659
+ const minPendingCount = improveProfile?.processes?.memoryInference?.minPendingCount;
2660
+ const pendingBelowMinCount = (() => {
2661
+ if (!primaryStashDir || minPendingCount === undefined || minPendingCount <= 0)
2662
+ return false;
2663
+ const pending = collectPendingMemories(primaryStashDir).length;
2664
+ if (pending < minPendingCount) {
2665
+ info(`[improve] memory inference skipped (${pending} pending < minPendingCount ${minPendingCount})`);
2666
+ return true;
2667
+ }
2668
+ return false;
2669
+ })();
2637
2670
  if (memoryInferenceDisabledByProfile) {
2638
2671
  info("[improve] memory inference skipped (disabled by improve profile)");
2639
2672
  }
2673
+ else if (pendingBelowMinCount) {
2674
+ // skipped — message already emitted above
2675
+ }
2640
2676
  else {
2641
2677
  const hintRefs = memoryRefsForInference.size;
2642
2678
  info(hintRefs > 0
@@ -144,6 +144,20 @@ export const ImproveProcessConfigSchema = z
144
144
  // on the `extract` process.
145
145
  minContentChars: z.number().int().min(0).optional(),
146
146
  maxChunkSize: z.number().int().min(1).max(50).optional(),
147
+ // Consolidate process: narrow candidate pool to memories modified within
148
+ // this duration window plus their graph neighbours. Only meaningful on
149
+ // the `consolidate` process. Absent = full-pool sweep.
150
+ incrementalSince: z.string().optional(),
151
+ // Consolidate process: hard cap on memories processed per pass.
152
+ // Reflect/distill: max refs processed (same as profile-level `limit`).
153
+ limit: positiveInt.optional(),
154
+ // Consolidate process: graph neighbours per changed memory during
155
+ // incremental consolidation. Default 5. Only meaningful with incrementalSince.
156
+ neighborsPerChanged: z.number().int().min(1).optional(),
157
+ // Distill process: skip distill entirely when reflect produced zero planned refs.
158
+ requirePlannedRefs: z.boolean().optional(),
159
+ // MemoryInference process: minimum pending memory count to run the pass.
160
+ minPendingCount: z.number().int().min(0).optional(),
147
161
  // Extract process: minimum number of new (unseen, in-window) candidate
148
162
  // sessions below which the extract pass skips entirely (emits an
149
163
  // `improve_skipped` event with `reason: "below_min_new_sessions"`). 0
@@ -15510,6 +15510,11 @@ var init_config_schema = __esm(() => {
15510
15510
  maxTotalChars: positiveInt.optional(),
15511
15511
  minContentChars: exports_external.number().int().min(0).optional(),
15512
15512
  maxChunkSize: exports_external.number().int().min(1).max(50).optional(),
15513
+ incrementalSince: exports_external.string().optional(),
15514
+ limit: positiveInt.optional(),
15515
+ neighborsPerChanged: exports_external.number().int().min(1).optional(),
15516
+ requirePlannedRefs: exports_external.boolean().optional(),
15517
+ minPendingCount: exports_external.number().int().min(0).optional(),
15513
15518
  minNewSessions: exports_external.number().int().min(0).optional(),
15514
15519
  indexSessions: exports_external.boolean().optional(),
15515
15520
  minSessionDuration: exports_external.number().min(0).optional(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akm-cli",
3
- "version": "0.9.0-beta.5",
3
+ "version": "0.9.0-beta.6",
4
4
  "type": "module",
5
5
  "description": "akm (Agent Knowledge Management) — A package manager for AI agent skills, commands, tools, and knowledge. Works with Claude Code, OpenCode, Cursor, and any AI coding assistant.",
6
6
  "keywords": [
@@ -51,7 +51,7 @@
51
51
  },
52
52
  "scripts": {
53
53
  "preinstall": "node -e \"var ua=process.env.npm_config_user_agent||'';var major=parseInt((process.versions.node||'0').split('.')[0],10);if(process.versions.bun||ua.startsWith('bun/')||process.env.BUN_INSTALL||major>=20){process.exit(0)}console.error('\\n ERROR: akm-cli requires the Bun runtime (https://bun.sh), Node.js >= 20, or the prebuilt binary.\\n Install options:\\n 1. Bun: curl -fsSL https://bun.sh/install | bash && bun install -g akm-cli\\n 2. Binary: curl -fsSL https://github.com/itlackey/akm/releases/latest/download/install.sh | bash\\n');process.exit(1)\"",
54
- "build": "rm -rf dist && bun run tsc --project ./tsconfig.build.json && bun scripts/copy-assets.ts && bun scripts/fix-esm-extensions.ts",
54
+ "build": "rm -rf dist && bun scripts/gen-config-schema.ts &&bun run tsc --project ./tsconfig.build.json && bun scripts/copy-assets.ts && bun scripts/fix-esm-extensions.ts",
55
55
  "check": "bun run lint && bunx tsc --noEmit && bun run test:unit && bun run test:integration",
56
56
  "check:fast": "bun run lint && bunx tsc --noEmit && bun run test:unit",
57
57
  "check:changed": "bun test tests/output-baseline.test.ts tests/integration/e2e.test.ts tests/stash-search.test.ts && bun run lint && bunx tsc --noEmit",