akm-cli 0.9.0-beta.1 → 0.9.0-beta.11

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.
Files changed (56) hide show
  1. package/CHANGELOG.md +492 -0
  2. package/dist/assets/templates/html/default.html +78 -0
  3. package/dist/assets/templates/html/health.html +732 -0
  4. package/dist/assets/templates/html/vendor/echarts.min.js +45 -0
  5. package/dist/cli/shared.js +21 -5
  6. package/dist/cli.js +47 -5
  7. package/dist/commands/config-cli.js +0 -10
  8. package/dist/commands/feedback-cli.js +42 -37
  9. package/dist/commands/graph/graph.js +75 -71
  10. package/dist/commands/health/checks.js +48 -0
  11. package/dist/commands/health/html-report.js +666 -0
  12. package/dist/commands/health.js +201 -14
  13. package/dist/commands/improve/consolidate.js +39 -6
  14. package/dist/commands/improve/distill.js +26 -5
  15. package/dist/commands/improve/extract-prompt.js +1 -1
  16. package/dist/commands/improve/extract.js +73 -8
  17. package/dist/commands/improve/improve-auto-accept.js +63 -3
  18. package/dist/commands/improve/improve-cli.js +7 -0
  19. package/dist/commands/improve/improve-profiles.js +4 -0
  20. package/dist/commands/improve/improve.js +874 -447
  21. package/dist/commands/improve/proactive-maintenance.js +113 -0
  22. package/dist/commands/improve/reflect-noise.js +0 -0
  23. package/dist/commands/improve/reflect.js +31 -0
  24. package/dist/commands/proposal/drain.js +73 -6
  25. package/dist/commands/proposal/proposal-cli.js +22 -10
  26. package/dist/commands/proposal/proposal.js +17 -1
  27. package/dist/commands/proposal/validators/proposals.js +365 -329
  28. package/dist/commands/read/curate.js +17 -0
  29. package/dist/commands/remember.js +6 -2
  30. package/dist/commands/sources/stash-cli.js +10 -2
  31. package/dist/commands/tasks/tasks.js +32 -8
  32. package/dist/core/config/config-schema.js +33 -0
  33. package/dist/core/logs-db.js +304 -0
  34. package/dist/core/paths.js +3 -0
  35. package/dist/core/state-db.js +152 -14
  36. package/dist/indexer/db/db.js +99 -13
  37. package/dist/indexer/ensure-index.js +152 -17
  38. package/dist/indexer/index-writer-lock.js +99 -0
  39. package/dist/indexer/indexer.js +114 -111
  40. package/dist/indexer/passes/memory-inference.js +61 -22
  41. package/dist/integrations/harnesses/claude/session-log.js +17 -5
  42. package/dist/llm/client.js +38 -4
  43. package/dist/llm/usage-persist.js +77 -0
  44. package/dist/llm/usage-telemetry.js +103 -0
  45. package/dist/output/context.js +3 -2
  46. package/dist/output/html-render.js +73 -0
  47. package/dist/output/shapes/helpers.js +17 -1
  48. package/dist/output/text/helpers.js +69 -1
  49. package/dist/scripts/migrate-storage.js +154 -25
  50. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +21 -2
  51. package/dist/sources/providers/tar-utils.js +16 -8
  52. package/dist/tasks/backends/cron.js +46 -9
  53. package/dist/tasks/runner.js +99 -16
  54. package/dist/workflows/db.js +4 -0
  55. package/package.json +3 -2
  56. package/dist/commands/config-edit.js +0 -344
@@ -39,9 +39,26 @@ import { chatCompletion } from "../../llm/client.js";
39
39
  import { isLlmFeatureEnabled, tryLlmFeature } from "../../llm/feature-gate.js";
40
40
  import { createProposal, isProposalSkipped } from "../proposal/validators/proposals.js";
41
41
  import { buildExtractPrompt, EXTRACT_JSON_SCHEMA, parseExtractPayload } from "./extract-prompt.js";
42
+ import { resolveProcessEnabled } from "./improve-profiles.js";
42
43
  import { buildSessionSummaryPrompt, parseSessionSummary, SESSION_SUMMARY_JSON_SCHEMA, sessionMeetsDurationGate, writeSessionAsset, } from "./session-asset.js";
43
44
  /** Default minimum session duration (minutes) for session indexing (#561). */
44
45
  const DEFAULT_MIN_SESSION_DURATION_MINUTES = 5;
46
+ /**
47
+ * Default minimum raw session size (chars) below which the extract LLM call is
48
+ * skipped (#595/#596). Deliberately tiny: analysis of 218 candidate-producing
49
+ * sessions showed sessions of 22–368 raw chars regularly yield 1–5 candidates,
50
+ * so size is not a reliable proxy for value — only truly empty sessions
51
+ * (0 chars, journal files) are safe to skip.
52
+ */
53
+ const DEFAULT_MIN_CONTENT_CHARS = 10;
54
+ /**
55
+ * Default cap on NEW sessions the extract pass will LLM-process in a single run
56
+ * (`processes.extract.maxSessionsPerRun` overrides; `0` disables). Bounds per-run
57
+ * wall time + token spend so a backlog of accumulated sessions can't run a single
58
+ * pass past its scheduled-task timeout. Overflow sessions stay unseen and are
59
+ * processed by subsequent runs, so coverage is preserved — just spread out.
60
+ */
61
+ const DEFAULT_MAX_SESSIONS_PER_RUN = 25;
45
62
  // ── Helpers ──────────────────────────────────────────────────────────────────
46
63
  /**
47
64
  * Parse a since-string into an absolute ms-epoch cutoff. Accepts:
@@ -115,7 +132,7 @@ function buildCandidateProposal(candidate, sourceRef) {
115
132
  * proposal validation failure) the session result records a warning and
116
133
  * keeps going — one session's bad luck never aborts a multi-session run.
117
134
  */
118
- async function processSession(harness, sessionRef, stashDir, config, llmConfig, chat, ctx, sourceRun, dryRun, timeoutMs, maxTotalChars, sessionIndexing) {
135
+ async function processSession(harness, sessionRef, stashDir, config, llmConfig, chat, ctx, sourceRun, dryRun, timeoutMs, maxTotalChars, minContentChars, sessionIndexing) {
119
136
  const warnings = [];
120
137
  let data;
121
138
  try {
@@ -136,6 +153,31 @@ async function processSession(harness, sessionRef, stashDir, config, llmConfig,
136
153
  const filtered = preFilterSession(data, {
137
154
  ...(typeof maxTotalChars === "number" ? { maxTotalChars } : {}),
138
155
  });
156
+ // #595/#596 — minContentChars gate: skip the LLM call for sessions whose RAW
157
+ // size is below threshold. Measured on the raw event text BEFORE the noise
158
+ // pre-filter, NOT on post-filter output — the pre-filter strips boilerplate
159
+ // so aggressively that even signal-bearing sessions can have tiny output
160
+ // (#596: gating post-filter filtered out 100% of sessions). Note: the 0.8.x
161
+ // fix gated on `filtered.stats.inputCount`, which is an EVENT count, not a
162
+ // char count — this port measures actual raw chars so the threshold matches
163
+ // the config key's documented unit.
164
+ const rawContentChars = data.events.reduce((sum, event) => sum + event.text.length, 0);
165
+ if (minContentChars > 0 && rawContentChars < minContentChars) {
166
+ return {
167
+ sessionId: sessionRef.sessionId,
168
+ harness: harness.name,
169
+ candidateCount: 0,
170
+ proposalIds: [],
171
+ preFilter: {
172
+ inputCount: filtered.stats.inputCount,
173
+ outputCount: filtered.stats.outputCount,
174
+ truncatedCount: filtered.stats.truncatedCount,
175
+ },
176
+ warnings: [],
177
+ skipped: true,
178
+ skipReason: "too_short",
179
+ };
180
+ }
139
181
  const prompt = buildExtractPrompt({ data, events: filtered.events, inlineRefs: data.inlineRefs });
140
182
  // #561 — ADDITIVE session indexing. Generate + write the session asset
141
183
  // (`sessions/<harness>/<id>.md`). FAIL-OPEN: any failure only records a
@@ -290,13 +332,20 @@ export async function akmExtract(options) {
290
332
  const stashDir = options.stashDir ?? resolveStashDir();
291
333
  const dryRun = options.dryRun ?? false;
292
334
  const sourceRun = options.sourceRun ?? `extract-${timestampForFilename()}`;
293
- // Read the per-process extract config from the active improve profile. Matches
294
- // the pattern reflect/distill/consolidate use: `profiles.improve.<active>.processes.extract`.
295
- // Only the `default` improve profile is consulted here extract isn't invoked
296
- // with a profile flag yet (parity item for a future change).
297
- const extractProcess = config.profiles?.improve?.default?.processes?.extract;
335
+ // Read the per-process extract config + enabled-state from the ACTIVE improve
336
+ // profile when `akmImprove` threaded it; otherwise fall back to the `default`
337
+ // profile (the explicit `akm extract` path, which has no active profile). This
338
+ // is what stops a non-default profile's `extract.enabled` from being silently
339
+ // overridden by the default profile and vice-versa.
340
+ const activeProfile = options.improveProfile;
341
+ const extractProcess = activeProfile
342
+ ? activeProfile.processes?.extract
343
+ : config.profiles?.improve?.default?.processes?.extract;
344
+ const extractEnabled = activeProfile
345
+ ? resolveProcessEnabled("extract", activeProfile)
346
+ : isLlmFeatureEnabled(config, "session_extraction");
298
347
  // Feature-gate early so we get a clean "skipped because disabled" envelope.
299
- if (!isLlmFeatureEnabled(config, "session_extraction")) {
348
+ if (!extractEnabled) {
300
349
  return {
301
350
  schemaVersion: 1,
302
351
  ok: true,
@@ -339,6 +388,15 @@ export async function akmExtract(options) {
339
388
  60_000;
340
389
  // Pre-filter budget — process config can raise it for large-context models.
341
390
  const maxTotalChars = typeof extractProcess?.maxTotalChars === "number" ? extractProcess.maxTotalChars : undefined;
391
+ // #595/#596 — minimum raw session size; sessions below it skip the LLM call
392
+ // entirely. Set `processes.extract.minContentChars: 0` to disable the gate.
393
+ const minContentChars = typeof extractProcess?.minContentChars === "number" ? extractProcess.minContentChars : DEFAULT_MIN_CONTENT_CHARS;
394
+ // Cap on NEW sessions LLM-processed per run; 0 disables. Absent = default.
395
+ // Bounds per-run wall time / LLM cost so a backlog can't push a run past its
396
+ // task timeout — the overflow stays unseen and is picked up by later runs.
397
+ const maxSessionsPerRun = typeof extractProcess?.maxSessionsPerRun === "number"
398
+ ? extractProcess.maxSessionsPerRun
399
+ : DEFAULT_MAX_SESSIONS_PER_RUN;
342
400
  // Default discovery window — process config can override the built-in 24h.
343
401
  const effectiveSince = options.since ?? extractProcess?.defaultSince;
344
402
  // #561 — resolve session-indexing config. Default ON: we only reach this code
@@ -482,8 +540,15 @@ export async function akmExtract(options) {
482
540
  skippedCount += 1;
483
541
  continue;
484
542
  }
543
+ // Per-run cap on LLM-processed sessions (skip-tracked seen sessions above
544
+ // don't count). Single-session / --force modes bypass the cap (explicit
545
+ // intent). Overflow sessions are left unseen for the next run.
546
+ if (!options.sessionId && !options.force && maxSessionsPerRun > 0 && processedCount >= maxSessionsPerRun) {
547
+ topLevelWarnings.push(`Reached maxSessionsPerRun=${maxSessionsPerRun}; ${candidates.length - processedCount - skippedCount} session(s) deferred to a later run.`);
548
+ break;
549
+ }
485
550
  try {
486
- const result = await processSession(harness, summary, stashDir, config, llmConfig, chat, options.ctx, sourceRun, dryRun, timeoutMs, maxTotalChars, sessionIndexing);
551
+ const result = await processSession(harness, summary, stashDir, config, llmConfig, chat, options.ctx, sourceRun, dryRun, timeoutMs, maxTotalChars, minContentChars, sessionIndexing);
487
552
  sessions.push(result);
488
553
  if (result.skipped)
489
554
  skippedCount += 1;
@@ -4,7 +4,24 @@
4
4
  import { loadConfig } from "../../core/config/config.js";
5
5
  import { appendEvent } from "../../core/events.js";
6
6
  import { info, warn } from "../../core/warn.js";
7
- import { promoteProposal } from "../proposal/validators/proposals.js";
7
+ import { promoteProposal, recordGateDecision } from "../proposal/validators/proposals.js";
8
+ /**
9
+ * Derive a stable, low-cardinality reason bucket from an auto-accept promotion
10
+ * error. `promoteProposal` throws a `validateProposal` report formatted as
11
+ * `[kind] message` lines; we extract the first finding kind. Non-validation
12
+ * throws collapse to `promote-error`.
13
+ */
14
+ function classifyPromoteFailure(err) {
15
+ const message = err instanceof Error ? err.message : String(err);
16
+ const finding = /\[([a-z][a-z0-9-]*)\]/i.exec(message);
17
+ if (finding)
18
+ return `validation:${finding[1]}`;
19
+ if (/not pending/i.test(message))
20
+ return "not-pending";
21
+ if (/unknown asset type/i.test(message))
22
+ return "unknown-type";
23
+ return "promote-error";
24
+ }
8
25
  // ---------------------------------------------------------------------------
9
26
  // Gate implementation
10
27
  // ---------------------------------------------------------------------------
@@ -18,7 +35,7 @@ import { promoteProposal } from "../proposal/validators/proposals.js";
18
35
  * @param promoteFn Injectable override for `promoteProposal` (test seam).
19
36
  */
20
37
  export async function runAutoAcceptGate(candidates, cfg, promoteFn = promoteProposal) {
21
- const result = { promoted: [], skipped: [], failed: [] };
38
+ const result = { promoted: [], skipped: [], failed: [], failedByReason: {} };
22
39
  // --- Guard: gate is disabled or context is incomplete ---
23
40
  if (cfg.dryRun || cfg.globalThreshold === undefined || !cfg.stashDir) {
24
41
  result.skipped = candidates.map((c) => c.proposalId);
@@ -26,14 +43,40 @@ export async function runAutoAcceptGate(candidates, cfg, promoteFn = promoteProp
26
43
  }
27
44
  const effectiveThreshold = Math.max(cfg.globalThreshold, cfg.minimumThreshold ?? 0) / 100;
28
45
  const resolvedConfig = typeof cfg.config === "function" ? cfg.config() : cfg.config;
46
+ const gateLabel = `improve:${cfg.phase}`;
47
+ // #577: stamp the gate's verdict onto each proposal so `akm proposal show`
48
+ // can explain why a proposal is pending (e.g. "deferred: below-threshold,
49
+ // 0.72 < 0.90"). Best-effort — a recording failure must never abort the gate.
50
+ const stamp = (proposalId, decision) => {
51
+ try {
52
+ recordGateDecision(cfg.stashDir, proposalId, decision);
53
+ }
54
+ catch (err) {
55
+ warn(`[improve] ${cfg.phase} failed to record gate decision for ${proposalId}: ${err instanceof Error ? err.message : String(err)}`);
56
+ }
57
+ };
29
58
  for (const candidate of candidates) {
30
59
  const { proposalId, confidence } = candidate;
31
60
  if (confidence === undefined || confidence < effectiveThreshold) {
61
+ stamp(proposalId, {
62
+ outcome: "deferred",
63
+ reason: confidence === undefined ? "no-confidence" : "below-threshold",
64
+ ...(confidence !== undefined ? { confidence } : {}),
65
+ thresholds: { autoAccept: effectiveThreshold },
66
+ gate: gateLabel,
67
+ });
32
68
  result.skipped.push(proposalId);
33
69
  continue;
34
70
  }
35
71
  try {
36
72
  const promotion = await promoteFn(cfg.stashDir, resolvedConfig, proposalId, {}, undefined);
73
+ stamp(promotion.proposal.id, {
74
+ outcome: "auto-accepted",
75
+ reason: "above-threshold",
76
+ confidence,
77
+ thresholds: { autoAccept: effectiveThreshold },
78
+ gate: gateLabel,
79
+ });
37
80
  appendEvent({
38
81
  eventType: "promoted",
39
82
  ref: promotion.ref,
@@ -46,14 +89,31 @@ export async function runAutoAcceptGate(candidates, cfg, promoteFn = promoteProp
46
89
  confidence,
47
90
  threshold: effectiveThreshold,
48
91
  phase: cfg.phase,
92
+ // Attribution tagging: carry the eligibility lane from the proposal
93
+ // record onto the auto-accept promoted event so the lane survives to
94
+ // accept time even when promotion happens in a later run.
95
+ ...(promotion.proposal.eligibilitySource !== undefined
96
+ ? { eligibilitySource: promotion.proposal.eligibilitySource }
97
+ : {}),
49
98
  },
50
99
  }, cfg.eventsCtx ?? {});
51
100
  info(`[improve] auto-accepted ${promotion.ref} (${cfg.phase}; confidence=${confidence.toFixed(2)} >= threshold=${effectiveThreshold.toFixed(2)})`);
52
101
  result.promoted.push(proposalId);
53
102
  }
54
103
  catch (err) {
55
- warn(`[improve] ${cfg.phase} auto-accept failed for ${proposalId}: ${err instanceof Error ? err.message : String(err)}`);
104
+ const reason = classifyPromoteFailure(err);
105
+ warn(`[improve] ${cfg.phase} auto-accept failed for ${proposalId} (${reason}): ${err instanceof Error ? err.message : String(err)}`);
56
106
  result.failed.push(proposalId);
107
+ result.failedByReason[reason] = (result.failedByReason[reason] ?? 0) + 1;
108
+ // Record WHY on the proposal so `akm proposal show` explains the rejection
109
+ // and the leak is no longer blind. Best-effort.
110
+ stamp(proposalId, {
111
+ outcome: "auto-rejected",
112
+ reason,
113
+ confidence,
114
+ thresholds: { autoAccept: effectiveThreshold },
115
+ gate: gateLabel,
116
+ });
57
117
  }
58
118
  }
59
119
  return result;
@@ -54,6 +54,11 @@ export const improveCommand = defineCommand({
54
54
  description: "Emit the full JSON result on stdout (legacy behaviour). (0.8.0+: full result is recorded in the improve_runs table of state.db and stdout is empty; use this flag for the prior behaviour, e.g. `akm improve --json-to-stdout | jq`.)",
55
55
  default: false,
56
56
  },
57
+ "skip-if-locked": {
58
+ type: "boolean",
59
+ description: "If another improve run already holds the lock, skip gracefully (exit 0) instead of failing with 'already running' (exit 78). Use for high-frequency scheduled runs so they don't pile up failures while a longer run is in progress.",
60
+ default: false,
61
+ },
57
62
  profile: {
58
63
  type: "string",
59
64
  description: "Named improve profile from profiles.improve or built-in profiles (default, quick, thorough, memory-focus, graph-refresh). Controls which sub-processes run and which asset types are processed.",
@@ -92,6 +97,7 @@ export const improveCommand = defineCommand({
92
97
  const minRetrievalCountRaw = getHyphenatedArg(args, "min-retrieval-count");
93
98
  const minRetrievalCount = parseNonNegativeIntFlag(minRetrievalCountRaw, "--min-retrieval-count");
94
99
  const requireFeedbackSignal = getHyphenatedBoolean(args, "require-feedback-signal");
100
+ const skipIfLocked = getHyphenatedBoolean(args, "skip-if-locked");
95
101
  const profileArg = getStringArg(args, "profile");
96
102
  // Only set the keys the user actually passed (citty leaves the flag
97
103
  // undefined unless `--sync`/`--no-sync` / `--push`/`--no-push` appears),
@@ -189,6 +195,7 @@ export const improveCommand = defineCommand({
189
195
  ...(timeoutMs !== undefined ? { timeoutMs } : {}),
190
196
  ...(minRetrievalCount !== undefined ? { minRetrievalCount } : {}),
191
197
  ...(requireFeedbackSignal ? { requireFeedbackSignal } : {}),
198
+ ...(skipIfLocked ? { skipIfLocked } : {}),
192
199
  ...(profileArg !== undefined ? { profile: profileArg } : {}),
193
200
  ...(Object.keys(syncOverride).length > 0 ? { sync: syncOverride } : {}),
194
201
  consolidateOptions: {
@@ -54,6 +54,10 @@ const IMPROVE_PROCESS_DEFAULTS = {
54
54
  // proposal-queue triage drains the standing backlog. Opt-in (default off),
55
55
  // like `validation` — needs an explicit `enabled: true`.
56
56
  triage: false,
57
+ // Layer 2 proactive-maintenance selector. Opt-in (default off) — surfaces
58
+ // stale high-value assets on a schedule. Enable per-profile with an explicit
59
+ // `processes.proactiveMaintenance.enabled: true`.
60
+ proactiveMaintenance: false,
57
61
  };
58
62
  /**
59
63
  * Compute the effective enabled-state for a named improve process.