akm-cli 0.9.0-beta.2 → 0.9.0-beta.27
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 +660 -0
- package/dist/assets/prompts/consolidate-system.md +23 -0
- package/dist/assets/prompts/contradiction-judge.md +33 -0
- package/dist/assets/prompts/distill-knowledge-system.md +22 -0
- package/dist/assets/prompts/distill-lesson-system.md +36 -0
- package/dist/assets/prompts/extract-session.md +5 -1
- package/dist/assets/prompts/graph-extract-system.md +1 -0
- package/dist/assets/prompts/memory-infer-system.md +1 -0
- package/dist/assets/prompts/memory-infer-user.md +5 -0
- package/dist/assets/prompts/metadata-enhance-system.md +1 -0
- package/dist/assets/prompts/procedural-system.md +44 -0
- package/dist/assets/prompts/recombine-system.md +40 -0
- package/dist/assets/prompts/staleness-detect-system.md +6 -0
- package/dist/assets/prompts/validate-summary-judge.md +1 -0
- package/dist/assets/templates/html/default.html +78 -0
- package/dist/assets/templates/html/health.html +730 -0
- package/dist/assets/templates/html/vendor/echarts.min.js +45 -0
- package/dist/cli/shared.js +21 -5
- package/dist/cli.js +47 -5
- package/dist/commands/agent/contribute-cli.js +16 -3
- package/dist/commands/feedback-cli.js +15 -6
- package/dist/commands/graph/graph.js +75 -71
- package/dist/commands/health/checks.js +48 -0
- package/dist/commands/health/html-report.js +790 -0
- package/dist/commands/health.js +478 -15
- package/dist/commands/improve/calibration.js +161 -0
- package/dist/commands/improve/consolidate.js +634 -111
- package/dist/commands/improve/dedup.js +482 -0
- package/dist/commands/improve/distill.js +145 -69
- package/dist/commands/improve/encoding-salience.js +205 -0
- package/dist/commands/improve/extract-cli.js +115 -1
- package/dist/commands/improve/extract-prompt.js +33 -2
- package/dist/commands/improve/extract-watch.js +140 -0
- package/dist/commands/improve/extract.js +280 -35
- package/dist/commands/improve/feedback-valence.js +54 -0
- package/dist/commands/improve/homeostatic.js +467 -0
- package/dist/commands/improve/improve-auto-accept.js +139 -6
- package/dist/commands/improve/improve-profiles.js +12 -0
- package/dist/commands/improve/improve.js +2079 -608
- package/dist/commands/improve/memory/memory-contradiction-detect.js +23 -28
- package/dist/commands/improve/outcome-loop.js +256 -0
- package/dist/commands/improve/proactive-maintenance.js +87 -0
- package/dist/commands/improve/procedural.js +409 -0
- package/dist/commands/improve/recombine.js +488 -0
- package/dist/commands/improve/reflect-noise.js +0 -0
- package/dist/commands/improve/reflect.js +51 -1
- package/dist/commands/improve/related-sessions.js +120 -0
- package/dist/commands/improve/salience.js +386 -0
- package/dist/commands/improve/triage.js +95 -0
- package/dist/commands/lint/agent-linter.js +19 -24
- package/dist/commands/lint/base-linter.js +173 -60
- package/dist/commands/lint/command-linter.js +19 -24
- package/dist/commands/lint/env-key-rules.js +34 -1
- package/dist/commands/lint/fact-linter.js +39 -0
- package/dist/commands/lint/index.js +31 -13
- package/dist/commands/lint/memory-linter.js +1 -1
- package/dist/commands/lint/registry.js +7 -2
- package/dist/commands/lint/task-linter.js +3 -3
- package/dist/commands/lint/workflow-linter.js +26 -1
- package/dist/commands/proposal/drain.js +73 -6
- package/dist/commands/proposal/proposal-cli.js +22 -10
- package/dist/commands/proposal/proposal.js +17 -1
- package/dist/commands/proposal/validators/proposals.js +369 -329
- package/dist/commands/read/curate.js +344 -80
- package/dist/commands/read/search-cli.js +7 -0
- package/dist/commands/read/search.js +1 -0
- package/dist/commands/read/show.js +67 -2
- package/dist/commands/remember.js +6 -2
- package/dist/commands/sources/installed-stashes.js +5 -1
- package/dist/commands/sources/stash-cli.js +10 -2
- package/dist/core/asset/asset-registry.js +2 -0
- package/dist/core/asset/asset-spec.js +14 -0
- package/dist/core/asset/frontmatter.js +166 -167
- package/dist/core/asset/markdown.js +8 -0
- package/dist/core/config/config-schema.js +255 -2
- package/dist/core/config/config.js +2 -2
- package/dist/core/logs-db.js +305 -0
- package/dist/core/paths.js +3 -0
- package/dist/core/state-db.js +706 -42
- package/dist/indexer/db/db.js +364 -38
- package/dist/indexer/db/graph-db.js +129 -86
- package/dist/indexer/ensure-index.js +152 -17
- package/dist/indexer/graph/graph-boost.js +51 -41
- package/dist/indexer/graph/graph-extraction.js +203 -3
- package/dist/indexer/index-writer-lock.js +99 -0
- package/dist/indexer/indexer.js +114 -111
- package/dist/indexer/passes/memory-inference.js +71 -25
- package/dist/indexer/passes/staleness-detect.js +2 -5
- package/dist/indexer/search/db-search.js +15 -4
- package/dist/indexer/search/ranking-contributors.js +22 -0
- package/dist/indexer/search/ranking.js +4 -0
- package/dist/indexer/walk/matchers.js +9 -0
- package/dist/integrations/agent/prompts.js +1 -0
- package/dist/integrations/harnesses/claude/session-log.js +27 -5
- package/dist/integrations/harnesses/opencode/session-log.js +9 -0
- package/dist/integrations/session-logs/index.js +16 -0
- package/dist/llm/client.js +38 -4
- package/dist/llm/embedder.js +27 -3
- package/dist/llm/embedders/local.js +66 -2
- package/dist/llm/graph-extract.js +2 -1
- package/dist/llm/memory-infer.js +4 -8
- package/dist/llm/metadata-enhance.js +9 -1
- package/dist/llm/usage-persist.js +77 -0
- package/dist/llm/usage-telemetry.js +103 -0
- package/dist/output/context.js +3 -2
- package/dist/output/html-render.js +73 -0
- package/dist/output/renderers.js +73 -1
- package/dist/output/shapes/curate.js +14 -2
- package/dist/output/shapes/helpers.js +17 -1
- package/dist/output/text/helpers.js +78 -1
- package/dist/runtime.js +25 -1
- package/dist/scripts/migrate-storage.js +1262 -591
- package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +485 -270
- package/dist/sources/providers/tar-utils.js +16 -8
- package/dist/storage/sqlite-pragmas.js +146 -0
- package/dist/tasks/runner.js +99 -16
- package/dist/workflows/db.js +5 -2
- package/dist/workflows/validate-summary.js +2 -7
- package/docs/data-and-telemetry.md +1 -0
- package/package.json +9 -6
package/dist/commands/health.js
CHANGED
|
@@ -4,12 +4,15 @@
|
|
|
4
4
|
import fs from "node:fs";
|
|
5
5
|
import { ConfigError, UsageError } from "../core/errors.js";
|
|
6
6
|
import { appendEvent, readEvents } from "../core/events.js";
|
|
7
|
+
import { buildTaskRunId, getLoggedRunIds, openLogsDatabase } from "../core/logs-db.js";
|
|
7
8
|
import { getStateDbPathInDataDir } from "../core/paths.js";
|
|
8
|
-
import { listExistingTableNames, openStateDatabase, queryCompletedTaskIntervals, queryImproveRuns, queryTaskHistory, } from "../core/state-db.js";
|
|
9
|
+
import { listExistingTableNames, listProposalGateDecisions, listStateProposals, openStateDatabase, queryCompletedTaskIntervals, queryImproveRuns, queryTaskHistory, } from "../core/state-db.js";
|
|
9
10
|
import { parseSinceToIso } from "../core/time.js";
|
|
10
11
|
import { readSemanticStatus } from "../indexer/search/semantic-status.js";
|
|
11
12
|
import { getExecutionLogCandidates } from "../integrations/session-logs/index.js";
|
|
13
|
+
import { LLM_USAGE_EVENT } from "../llm/usage-persist.js";
|
|
12
14
|
import { HEALTH_CHECKS } from "./health/checks.js";
|
|
15
|
+
import { gateDecisionsToSamples, summarizeCalibration } from "./improve/calibration.js";
|
|
13
16
|
const DEFAULT_SINCE_MS = 24 * 60 * 60 * 1000;
|
|
14
17
|
const IMPROVE_COMPLETED_EVENT = "improve_completed";
|
|
15
18
|
const HEALTH_PROBE_EVENT = "health_probe";
|
|
@@ -70,6 +73,7 @@ function createUnknownImproveMetrics() {
|
|
|
70
73
|
error: 0,
|
|
71
74
|
},
|
|
72
75
|
autoAccept: { promoted: 0, validationFailed: 0 },
|
|
76
|
+
calibration: summarizeCalibration([]),
|
|
73
77
|
reflectsWithErrorContext: 0,
|
|
74
78
|
coverageGapCount: 0,
|
|
75
79
|
evalCasesWritten: 0,
|
|
@@ -153,6 +157,22 @@ function createUnknownImproveMetrics() {
|
|
|
153
157
|
graphExtraction: { count: 0, totalMs: 0, medianMs: 0, p95Ms: 0 },
|
|
154
158
|
},
|
|
155
159
|
},
|
|
160
|
+
perfTelemetry: {
|
|
161
|
+
dedupPoolSize: 0,
|
|
162
|
+
llmPoolSize: 0,
|
|
163
|
+
judgedCacheSkipped: 0,
|
|
164
|
+
embedMs: 0,
|
|
165
|
+
embedCacheHits: 0,
|
|
166
|
+
embedCacheMisses: 0,
|
|
167
|
+
overBudgetRuns: 0,
|
|
168
|
+
runsWithTelemetry: 0,
|
|
169
|
+
},
|
|
170
|
+
coverage: {
|
|
171
|
+
rate: Number.NaN,
|
|
172
|
+
eligibleFraction: Number.NaN,
|
|
173
|
+
acceptedProposals: 0,
|
|
174
|
+
totalAssets: 0,
|
|
175
|
+
},
|
|
156
176
|
};
|
|
157
177
|
}
|
|
158
178
|
function toFiniteNumber(value) {
|
|
@@ -361,6 +381,21 @@ function projectRunMetrics(result) {
|
|
|
361
381
|
}
|
|
362
382
|
}
|
|
363
383
|
}
|
|
384
|
+
// WS-5: extract perf telemetry from the consolidation envelope.
|
|
385
|
+
// Pre-WS-5 envelopes lack `perfTelemetry`; be defensive.
|
|
386
|
+
const perf = consolidation.perfTelemetry;
|
|
387
|
+
if (perf) {
|
|
388
|
+
metrics.perfTelemetry.runsWithTelemetry += 1;
|
|
389
|
+
metrics.perfTelemetry.dedupPoolSize += toFiniteNumber(perf.dedupPoolSize);
|
|
390
|
+
metrics.perfTelemetry.llmPoolSize += toFiniteNumber(perf.llmPoolSize);
|
|
391
|
+
metrics.perfTelemetry.judgedCacheSkipped += toFiniteNumber(perf.judgedCacheSkipped);
|
|
392
|
+
metrics.perfTelemetry.embedMs += toFiniteNumber(perf.embedMs);
|
|
393
|
+
metrics.perfTelemetry.embedCacheHits += toFiniteNumber(perf.embedCacheHits);
|
|
394
|
+
metrics.perfTelemetry.embedCacheMisses += toFiniteNumber(perf.embedCacheMisses);
|
|
395
|
+
const budgetFrac = toFiniteNumber(perf.estimatedBudgetFractionUsed);
|
|
396
|
+
if (budgetFrac > 1.0)
|
|
397
|
+
metrics.perfTelemetry.overBudgetRuns += 1;
|
|
398
|
+
}
|
|
364
399
|
}
|
|
365
400
|
const memoryInference = result.memoryInference;
|
|
366
401
|
if (memoryInference) {
|
|
@@ -471,7 +506,10 @@ function finalizeImproveMetrics(metrics) {
|
|
|
471
506
|
*/
|
|
472
507
|
function mergeImproveMetrics(dst, src) {
|
|
473
508
|
dst.plannedRefs += src.plannedRefs;
|
|
474
|
-
|
|
509
|
+
// profileFilteredRefs is the count of refs the planner drops up-front for the
|
|
510
|
+
// active profile — recomputed against the (stable) stash every run, so it is a
|
|
511
|
+
// snapshot, NOT a per-run increment. Summing it re-counts the same refs each
|
|
512
|
+
// run (the ~2.4M bug). Set from the most recent run in summarizeImproveRuns.
|
|
475
513
|
dst.actions.reflect.ok += src.actions.reflect.ok;
|
|
476
514
|
dst.actions.reflect.failed += src.actions.reflect.failed;
|
|
477
515
|
dst.actions.reflect.cooldown += src.actions.reflect.cooldown;
|
|
@@ -504,8 +542,10 @@ function mergeImproveMetrics(dst, src) {
|
|
|
504
542
|
dst.coverageGapCount += src.coverageGapCount;
|
|
505
543
|
dst.evalCasesWritten += src.evalCasesWritten;
|
|
506
544
|
dst.deadUrlCount += src.deadUrlCount;
|
|
507
|
-
|
|
508
|
-
|
|
545
|
+
// NOTE: memorySummary (derived/eligible) is a WHOLE-STASH snapshot recorded on
|
|
546
|
+
// every run, NOT a per-run increment — summing it across the window inflates
|
|
547
|
+
// it ~N× (the 1.2M-eligible bug). It is set from the most recent run in
|
|
548
|
+
// summarizeImproveRuns instead, so it is intentionally not merged here.
|
|
509
549
|
dst.memoryCleanup.pruneCandidates += src.memoryCleanup.pruneCandidates;
|
|
510
550
|
dst.memoryCleanup.contradictionCandidates += src.memoryCleanup.contradictionCandidates;
|
|
511
551
|
dst.memoryCleanup.beliefStateTransitions += src.memoryCleanup.beliefStateTransitions;
|
|
@@ -554,6 +594,18 @@ function mergeImproveMetrics(dst, src) {
|
|
|
554
594
|
dst.sessionExtraction.proposalsCreated += src.sessionExtraction.proposalsCreated;
|
|
555
595
|
dst.sessionExtraction.warnings += src.sessionExtraction.warnings;
|
|
556
596
|
dst.sessionExtraction.durationMs += src.sessionExtraction.durationMs;
|
|
597
|
+
// WS-5: merge perf telemetry (additive sums).
|
|
598
|
+
dst.perfTelemetry.dedupPoolSize += src.perfTelemetry.dedupPoolSize;
|
|
599
|
+
dst.perfTelemetry.llmPoolSize += src.perfTelemetry.llmPoolSize;
|
|
600
|
+
dst.perfTelemetry.judgedCacheSkipped += src.perfTelemetry.judgedCacheSkipped;
|
|
601
|
+
dst.perfTelemetry.embedMs += src.perfTelemetry.embedMs;
|
|
602
|
+
dst.perfTelemetry.embedCacheHits += src.perfTelemetry.embedCacheHits;
|
|
603
|
+
dst.perfTelemetry.embedCacheMisses += src.perfTelemetry.embedCacheMisses;
|
|
604
|
+
dst.perfTelemetry.overBudgetRuns += src.perfTelemetry.overBudgetRuns;
|
|
605
|
+
dst.perfTelemetry.runsWithTelemetry += src.perfTelemetry.runsWithTelemetry;
|
|
606
|
+
// coverage: acceptedProposals is additive; totalAssets is a snapshot (like memorySummary).
|
|
607
|
+
// totalAssets is intentionally NOT merged here — set from the most recent run in summarizeImproveRuns.
|
|
608
|
+
dst.coverage.acceptedProposals += src.coverage.acceptedProposals;
|
|
557
609
|
}
|
|
558
610
|
function summarizeImproveRuns(db, since, until) {
|
|
559
611
|
const accum = createUnknownImproveMetrics();
|
|
@@ -566,6 +618,11 @@ function summarizeImproveRuns(db, since, until) {
|
|
|
566
618
|
memoryInference: [],
|
|
567
619
|
graphExtraction: [],
|
|
568
620
|
};
|
|
621
|
+
// memorySummary is a whole-stash snapshot per run, so the window value is the
|
|
622
|
+
// MOST RECENT run's snapshot (current state) — not a sum across runs.
|
|
623
|
+
let latestStartMs = Number.NEGATIVE_INFINITY;
|
|
624
|
+
let latestMemorySummary;
|
|
625
|
+
let latestProfileFilteredRefs = 0;
|
|
569
626
|
for (const row of rows) {
|
|
570
627
|
let result;
|
|
571
628
|
try {
|
|
@@ -576,6 +633,12 @@ function summarizeImproveRuns(db, since, until) {
|
|
|
576
633
|
}
|
|
577
634
|
const perRow = projectRunMetrics(result);
|
|
578
635
|
mergeImproveMetrics(accum, perRow);
|
|
636
|
+
const startMs = new Date(row.started_at).getTime();
|
|
637
|
+
if (Number.isFinite(startMs) && startMs >= latestStartMs) {
|
|
638
|
+
latestStartMs = startMs;
|
|
639
|
+
latestMemorySummary = perRow.memorySummary;
|
|
640
|
+
latestProfileFilteredRefs = perRow.profileFilteredRefs;
|
|
641
|
+
}
|
|
579
642
|
// Collect per-phase durations directly off the envelope. consolidation's
|
|
580
643
|
// duration lives inside the sub-object; memoryInference and graphExtraction
|
|
581
644
|
// expose top-level *DurationMs keys (`memoryInferenceDurationMs`,
|
|
@@ -592,6 +655,9 @@ function summarizeImproveRuns(db, since, until) {
|
|
|
592
655
|
phaseDurations.graphExtraction.push(graphMs);
|
|
593
656
|
}
|
|
594
657
|
finalizeImproveMetrics(accum);
|
|
658
|
+
if (latestMemorySummary)
|
|
659
|
+
accum.memorySummary = latestMemorySummary;
|
|
660
|
+
accum.profileFilteredRefs = latestProfileFilteredRefs;
|
|
595
661
|
accum.wallTime.byPhase = {
|
|
596
662
|
consolidation: summarizePhaseDurations(phaseDurations.consolidation),
|
|
597
663
|
memoryInference: summarizePhaseDurations(phaseDurations.memoryInference),
|
|
@@ -622,7 +688,7 @@ function summarizePhaseDurations(samples) {
|
|
|
622
688
|
* Project an improve_runs row + wall-time lookup into a single ImproveRunSummary.
|
|
623
689
|
* Used by `akm health --detail per-run`.
|
|
624
690
|
*/
|
|
625
|
-
function projectImproveRunSummary(row, wallTimeMs) {
|
|
691
|
+
function projectImproveRunSummary(row, wallTimeMs, taskId) {
|
|
626
692
|
let result = {};
|
|
627
693
|
try {
|
|
628
694
|
result = JSON.parse(row.result_json);
|
|
@@ -646,6 +712,7 @@ function projectImproveRunSummary(row, wallTimeMs) {
|
|
|
646
712
|
mode: row.scope_mode,
|
|
647
713
|
...(row.scope_value ? { value: row.scope_value } : {}),
|
|
648
714
|
},
|
|
715
|
+
taskId,
|
|
649
716
|
actions: perRow.actions,
|
|
650
717
|
memorySummary: perRow.memorySummary,
|
|
651
718
|
memoryCleanup: perRow.memoryCleanup,
|
|
@@ -708,9 +775,62 @@ function findContainingTaskInterval(timestampMs, intervals) {
|
|
|
708
775
|
}
|
|
709
776
|
return undefined;
|
|
710
777
|
}
|
|
778
|
+
/**
|
|
779
|
+
* Load `task_history` rows whose `task_id` begins `akm-improve` (the scheduled
|
|
780
|
+
* improve tasks: `akm-improve-frequent`, `akm-improve-proactive-weekly`, …) in
|
|
781
|
+
* the window, widened ±5 min so a task that fired just before the window opened
|
|
782
|
+
* still matches a run inside it. Used to attribute each improve run to the task
|
|
783
|
+
* that launched it.
|
|
784
|
+
*/
|
|
785
|
+
function loadImproveTaskRuns(db, since, until) {
|
|
786
|
+
const sinceMs = new Date(since).getTime();
|
|
787
|
+
const untilMs = until ? new Date(until).getTime() : undefined;
|
|
788
|
+
const widenedSince = new Date(sinceMs - 5 * 60 * 1000).toISOString();
|
|
789
|
+
const widenedUntil = untilMs !== undefined ? new Date(untilMs + 5 * 60 * 1000).toISOString() : undefined;
|
|
790
|
+
const runs = [];
|
|
791
|
+
for (const row of queryTaskHistory(db, { since: widenedSince, until: widenedUntil })) {
|
|
792
|
+
if (!row.task_id.startsWith("akm-improve"))
|
|
793
|
+
continue;
|
|
794
|
+
const startMs = new Date(row.started_at).getTime();
|
|
795
|
+
if (!Number.isFinite(startMs))
|
|
796
|
+
continue;
|
|
797
|
+
const endIso = row.completed_at ?? row.failed_at;
|
|
798
|
+
const endMs = endIso ? new Date(endIso).getTime() : Number.NaN;
|
|
799
|
+
runs.push({ taskId: row.task_id, startMs, endMs });
|
|
800
|
+
}
|
|
801
|
+
return runs;
|
|
802
|
+
}
|
|
803
|
+
/**
|
|
804
|
+
* Attribute an improve run to the scheduled task that launched it by matching
|
|
805
|
+
* start times within ±5 min, scored by start delta (plus end delta when both
|
|
806
|
+
* ends are known). Port of the health-report skill's `match_task_id`. Returns
|
|
807
|
+
* `"manual"` when no scheduled improve task matches.
|
|
808
|
+
*/
|
|
809
|
+
function matchImproveTaskId(startedAt, completedAt, taskRuns) {
|
|
810
|
+
const startMs = new Date(startedAt).getTime();
|
|
811
|
+
if (!Number.isFinite(startMs))
|
|
812
|
+
return "manual";
|
|
813
|
+
const endMs = completedAt ? new Date(completedAt).getTime() : Number.NaN;
|
|
814
|
+
let best;
|
|
815
|
+
let bestScore = Number.POSITIVE_INFINITY;
|
|
816
|
+
for (const task of taskRuns) {
|
|
817
|
+
const startDelta = Math.abs(task.startMs - startMs);
|
|
818
|
+
if (startDelta > 5 * 60 * 1000)
|
|
819
|
+
continue;
|
|
820
|
+
let score = startDelta;
|
|
821
|
+
if (Number.isFinite(endMs) && Number.isFinite(task.endMs))
|
|
822
|
+
score += Math.abs(task.endMs - endMs);
|
|
823
|
+
if (score < bestScore) {
|
|
824
|
+
bestScore = score;
|
|
825
|
+
best = task.taskId;
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
return best ?? "manual";
|
|
829
|
+
}
|
|
711
830
|
function buildPerRunSummaries(db, since, until) {
|
|
712
831
|
const rows = queryImproveRuns(db, since, until);
|
|
713
832
|
const taskIntervals = loadTaskIntervals(db, since, until);
|
|
833
|
+
const improveTaskRuns = loadImproveTaskRuns(db, since, until);
|
|
714
834
|
const summaries = [];
|
|
715
835
|
for (const row of rows) {
|
|
716
836
|
const startMs = new Date(row.started_at).getTime();
|
|
@@ -730,7 +850,8 @@ function buildPerRunSummaries(db, since, until) {
|
|
|
730
850
|
const interval = Number.isFinite(startMs) ? findContainingTaskInterval(startMs, taskIntervals) : undefined;
|
|
731
851
|
wallTimeMs = interval?.durationMs ?? 0;
|
|
732
852
|
}
|
|
733
|
-
|
|
853
|
+
const taskId = matchImproveTaskId(row.started_at, row.completed_at, improveTaskRuns);
|
|
854
|
+
summaries.push(projectImproveRunSummary(row, wallTimeMs, taskId));
|
|
734
855
|
}
|
|
735
856
|
return summaries;
|
|
736
857
|
}
|
|
@@ -757,12 +878,34 @@ function computeWallTimeStats(durationsMs, byPhase) {
|
|
|
757
878
|
};
|
|
758
879
|
}
|
|
759
880
|
function buildImproveSkipSummary(events) {
|
|
760
|
-
|
|
881
|
+
// Two kinds of skip events:
|
|
882
|
+
// - Per-occurrence (no `count`): one event per skipped ref → SUM is correct.
|
|
883
|
+
// - Aggregated snapshot (carries `count`): a single per-run event whose count
|
|
884
|
+
// is the number of refs that hit a STABLE, whole-stash condition that run
|
|
885
|
+
// (`no_new_signal`, `profile_filtered_all_passes`). Each run re-counts the
|
|
886
|
+
// same stable set, so summing across the window re-counts it N times (the
|
|
887
|
+
// 2.7M / 3M inflation). For these we keep the MOST RECENT run's count — the
|
|
888
|
+
// current snapshot — matching how memorySummary/profileFilteredRefs are
|
|
889
|
+
// handled. Events arrive in chronological (offset) order, so the last
|
|
890
|
+
// count-bearing event per reason is the latest run's value.
|
|
891
|
+
const summed = {};
|
|
892
|
+
const latestSnapshot = {};
|
|
761
893
|
for (const event of events) {
|
|
762
894
|
const reason = typeof event.metadata?.reason === "string" && event.metadata.reason.trim() ? event.metadata.reason : "unknown";
|
|
763
|
-
|
|
895
|
+
const rawCount = event.metadata?.count;
|
|
896
|
+
if (typeof rawCount === "number" && Number.isFinite(rawCount) && rawCount > 0) {
|
|
897
|
+
latestSnapshot[reason] = rawCount; // overwrite → keeps the latest run's snapshot
|
|
898
|
+
}
|
|
899
|
+
else {
|
|
900
|
+
summed[reason] = (summed[reason] ?? 0) + 1;
|
|
901
|
+
}
|
|
764
902
|
}
|
|
765
|
-
|
|
903
|
+
const skipReasons = { ...summed };
|
|
904
|
+
for (const [reason, count] of Object.entries(latestSnapshot)) {
|
|
905
|
+
skipReasons[reason] = (skipReasons[reason] ?? 0) + count;
|
|
906
|
+
}
|
|
907
|
+
const skipped = Object.values(skipReasons).reduce((a, b) => a + b, 0);
|
|
908
|
+
return { skipped, skipReasons };
|
|
766
909
|
}
|
|
767
910
|
function probeStateDbRoundTrip(stateDbPath) {
|
|
768
911
|
const before = readEvents({}, { dbPath: stateDbPath }).nextOffset;
|
|
@@ -882,14 +1025,275 @@ function computeDeltas(first, last) {
|
|
|
882
1025
|
}
|
|
883
1026
|
return out;
|
|
884
1027
|
}
|
|
885
|
-
|
|
1028
|
+
/**
|
|
1029
|
+
* Partition task_history rows into "should have a log" (non-null log_path) and
|
|
1030
|
+
* "log is actually backed". A run counts as backed when logs.db holds rows for
|
|
1031
|
+
* its run_id (#579 — the DB is the primary record); rows written before logs.db
|
|
1032
|
+
* existed fall back to the transitional on-disk file check. `logsDb` may be
|
|
1033
|
+
* undefined when logs.db could not be opened — then only the file check runs.
|
|
1034
|
+
*/
|
|
1035
|
+
function partitionLogBackedRows(taskRows, logsDb) {
|
|
1036
|
+
const withLogs = taskRows.filter((row) => row.log_path !== null);
|
|
1037
|
+
const loggedRunIds = logsDb
|
|
1038
|
+
? getLoggedRunIds(logsDb, withLogs.map((row) => buildTaskRunId(row.task_id, row.started_at)))
|
|
1039
|
+
: new Set();
|
|
1040
|
+
const backed = withLogs.filter((row) => loggedRunIds.has(buildTaskRunId(row.task_id, row.started_at)) ||
|
|
1041
|
+
(row.log_path !== null && fs.existsSync(row.log_path)));
|
|
1042
|
+
return { withLogs, backed };
|
|
1043
|
+
}
|
|
1044
|
+
/** Stage key used for `llm_usage` events recorded outside any stage scope. */
|
|
1045
|
+
const UNATTRIBUTED_STAGE = "unattributed";
|
|
1046
|
+
function emptyLlmUsageStageAggregate() {
|
|
1047
|
+
return {
|
|
1048
|
+
calls: 0,
|
|
1049
|
+
totalDurationMs: 0,
|
|
1050
|
+
promptTokens: 0,
|
|
1051
|
+
completionTokens: 0,
|
|
1052
|
+
totalTokens: 0,
|
|
1053
|
+
reasoningTokens: 0,
|
|
1054
|
+
};
|
|
1055
|
+
}
|
|
1056
|
+
function emptyLlmUsageAggregate() {
|
|
1057
|
+
return { ...emptyLlmUsageStageAggregate(), byStage: {} };
|
|
1058
|
+
}
|
|
1059
|
+
/**
|
|
1060
|
+
* Aggregate `llm_usage` events (#576) into a window total plus a per-stage
|
|
1061
|
+
* breakdown of call count, wall-time, and token usage. Token fields absent from
|
|
1062
|
+
* a best-effort record contribute 0. Calls with no `stage` land under
|
|
1063
|
+
* {@link UNATTRIBUTED_STAGE}.
|
|
1064
|
+
*/
|
|
1065
|
+
function summarizeLlmUsage(events) {
|
|
1066
|
+
const aggregate = emptyLlmUsageAggregate();
|
|
1067
|
+
for (const event of events) {
|
|
1068
|
+
const meta = event.metadata ?? {};
|
|
1069
|
+
const stageKey = typeof meta.stage === "string" && meta.stage ? meta.stage : UNATTRIBUTED_STAGE;
|
|
1070
|
+
let stage = aggregate.byStage[stageKey];
|
|
1071
|
+
if (!stage) {
|
|
1072
|
+
stage = emptyLlmUsageStageAggregate();
|
|
1073
|
+
aggregate.byStage[stageKey] = stage;
|
|
1074
|
+
}
|
|
1075
|
+
const durationMs = toFiniteNumber(meta.durationMs);
|
|
1076
|
+
const promptTokens = toFiniteNumber(meta.promptTokens);
|
|
1077
|
+
const completionTokens = toFiniteNumber(meta.completionTokens);
|
|
1078
|
+
const totalTokens = toFiniteNumber(meta.totalTokens);
|
|
1079
|
+
const reasoningTokens = toFiniteNumber(meta.reasoningTokens);
|
|
1080
|
+
for (const target of [aggregate, stage]) {
|
|
1081
|
+
target.calls += 1;
|
|
1082
|
+
target.totalDurationMs += durationMs;
|
|
1083
|
+
target.promptTokens += promptTokens;
|
|
1084
|
+
target.completionTokens += completionTokens;
|
|
1085
|
+
target.totalTokens += totalTokens;
|
|
1086
|
+
target.reasoningTokens += reasoningTokens;
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
return aggregate;
|
|
1090
|
+
}
|
|
1091
|
+
function readLlmUsageAggregate(stateDbPath, since, until) {
|
|
1092
|
+
const events = readEvents({ since, type: LLM_USAGE_EVENT }, { dbPath: stateDbPath }).events.filter((event) => {
|
|
1093
|
+
if (until === undefined)
|
|
1094
|
+
return true;
|
|
1095
|
+
return new Date(event.ts ?? since).getTime() < new Date(until).getTime();
|
|
1096
|
+
});
|
|
1097
|
+
return summarizeLlmUsage(events);
|
|
1098
|
+
}
|
|
1099
|
+
/**
|
|
1100
|
+
* Read the auto-accept gate calibration summary (#612) over `[since, until)`.
|
|
1101
|
+
* Reads every proposal's `gateDecision` from the open state.db, projects the
|
|
1102
|
+
* acted-on (auto-accepted / auto-rejected) decisions into calibration samples
|
|
1103
|
+
* within the window, and aggregates them deterministically.
|
|
1104
|
+
*/
|
|
1105
|
+
function readCalibration(db, since, until) {
|
|
1106
|
+
const decisions = listProposalGateDecisions(db);
|
|
1107
|
+
const samples = gateDecisionsToSamples(decisions, { since, ...(until !== undefined ? { until } : {}) });
|
|
1108
|
+
return summarizeCalibration(samples);
|
|
1109
|
+
}
|
|
1110
|
+
// ── WS-5 Observability helpers ───────────────────────────────────────────────
|
|
1111
|
+
/**
|
|
1112
|
+
* Compute WS-5 denominator-fixed coverage metrics.
|
|
1113
|
+
*
|
|
1114
|
+
* `coverage = accepted_proposals / total_assets` (Part V §3).
|
|
1115
|
+
* The denominator is the TOTAL stash size (not the moving eligible set) so
|
|
1116
|
+
* more-inclusive WS-1 ranking cannot spuriously inflate coverage.
|
|
1117
|
+
* `eligibleFraction = eligible_assets / total_assets` is reported separately.
|
|
1118
|
+
*
|
|
1119
|
+
* Proposals are counted only when their `updatedAt` falls within `[since, until)`
|
|
1120
|
+
* so the rate is genuinely window-scoped (matching the JSDoc on the type).
|
|
1121
|
+
*
|
|
1122
|
+
* @param db - Open state.db connection.
|
|
1123
|
+
* @param totalAssets - Total stash asset count (eligible + derived) from the
|
|
1124
|
+
* most recent run's memorySummary. 0 = denominator unknown, returns NaN rates.
|
|
1125
|
+
* @param eligibleAssets - Eligible (non-derived) asset count from the most recent run.
|
|
1126
|
+
* @param since - Window start (ISO-8601). Proposals accepted before this are excluded.
|
|
1127
|
+
* @param until - Window end (ISO-8601, exclusive). Absent = open-ended (up to now).
|
|
1128
|
+
* @param stashDir - Optional: scope accepted proposals to one stash. Absent = all stashes.
|
|
1129
|
+
*/
|
|
1130
|
+
function computeDenominatorFixedCoverage(db, totalAssets, eligibleAssets, since, until, stashDir) {
|
|
1131
|
+
let acceptedProposals = 0;
|
|
1132
|
+
try {
|
|
1133
|
+
const proposals = listStateProposals(db, {
|
|
1134
|
+
status: "accepted",
|
|
1135
|
+
...(stashDir ? { stashDir } : {}),
|
|
1136
|
+
}).filter((p) => {
|
|
1137
|
+
const updatedAt = p.updatedAt ?? "";
|
|
1138
|
+
if (updatedAt < since)
|
|
1139
|
+
return false;
|
|
1140
|
+
if (until !== undefined && updatedAt >= until)
|
|
1141
|
+
return false;
|
|
1142
|
+
return true;
|
|
1143
|
+
});
|
|
1144
|
+
acceptedProposals = proposals.length;
|
|
1145
|
+
}
|
|
1146
|
+
catch {
|
|
1147
|
+
// Fail open: table may not exist on older installs.
|
|
1148
|
+
}
|
|
1149
|
+
if (totalAssets === 0) {
|
|
1150
|
+
return {
|
|
1151
|
+
rate: Number.NaN,
|
|
1152
|
+
eligibleFraction: Number.NaN,
|
|
1153
|
+
acceptedProposals,
|
|
1154
|
+
totalAssets: 0,
|
|
1155
|
+
};
|
|
1156
|
+
}
|
|
1157
|
+
return {
|
|
1158
|
+
rate: roundRate(acceptedProposals / totalAssets),
|
|
1159
|
+
eligibleFraction: roundRate(eligibleAssets / totalAssets),
|
|
1160
|
+
acceptedProposals,
|
|
1161
|
+
totalAssets,
|
|
1162
|
+
};
|
|
1163
|
+
}
|
|
1164
|
+
/**
|
|
1165
|
+
* Compute WS-5 per-run degradation metrics (Part V §4).
|
|
1166
|
+
*
|
|
1167
|
+
* Health VIEWS only — reads from state.db tables populated by prior improve
|
|
1168
|
+
* runs. Gracefully returns partial data when tables are absent (pre-WS-1/2).
|
|
1169
|
+
*
|
|
1170
|
+
* @param db - Open state.db connection.
|
|
1171
|
+
* @param since - Window start (ISO-8601).
|
|
1172
|
+
* @param until - Window end (ISO-8601).
|
|
1173
|
+
*/
|
|
1174
|
+
function computeDegradationMetrics(db, since, until) {
|
|
1175
|
+
// (a) Corpus diversity — salience rank distribution of the top-100 assets.
|
|
1176
|
+
// We use the Gini coefficient of retrieval_salience scores as an intra-corpus
|
|
1177
|
+
// diversity proxy. A Gini close to 1 = highly concentrated (entrenched top
|
|
1178
|
+
// assets), Gini near 0 = flat/diverse. This is a single-snapshot metric;
|
|
1179
|
+
// consecutive-run centroid distance requires cross-run history not yet stored.
|
|
1180
|
+
let corpusCentroidDistance = Number.NaN;
|
|
1181
|
+
let entrenchmentFlagged;
|
|
1182
|
+
try {
|
|
1183
|
+
const rows = db
|
|
1184
|
+
.prepare(`SELECT retrieval_salience FROM asset_salience
|
|
1185
|
+
ORDER BY rank_score DESC LIMIT 100`)
|
|
1186
|
+
.all();
|
|
1187
|
+
if (rows.length >= 5) {
|
|
1188
|
+
const vals = rows.map((r) => r.retrieval_salience).sort((a, b) => a - b);
|
|
1189
|
+
const n = vals.length;
|
|
1190
|
+
const sumAbsDiff = vals.reduce((acc, xi, i) => {
|
|
1191
|
+
return acc + vals.slice(i + 1).reduce((a, xj) => a + Math.abs(xi - xj), 0);
|
|
1192
|
+
}, 0);
|
|
1193
|
+
const mean = vals.reduce((a, b) => a + b, 0) / n;
|
|
1194
|
+
// Gini = (sum |xi - xj|) / (2 n^2 mean); 0 = perfect equality, 1 = perfect inequality.
|
|
1195
|
+
const gini = mean > 0 ? sumAbsDiff / (2 * n * n * mean) : 0;
|
|
1196
|
+
// Re-express as a diversity proxy in [0,1]: high gini = low diversity.
|
|
1197
|
+
// corpusCentroidDistance approximation: gini is "distance from uniform".
|
|
1198
|
+
// Note: retrieval_salience values are in [0,1], so the max achievable Gini
|
|
1199
|
+
// with this formula is ~0.5 (when one asset dominates and others are near 0).
|
|
1200
|
+
// Threshold: >0.35 flags entrenchment (robustly above the ~0.1 uniform baseline).
|
|
1201
|
+
corpusCentroidDistance = roundRate(gini);
|
|
1202
|
+
entrenchmentFlagged = gini > 0.35;
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
catch {
|
|
1206
|
+
// Table not present (pre-WS-1 install) — leave NaN.
|
|
1207
|
+
}
|
|
1208
|
+
// (b) Merge fidelity — fraction of consolidate accepted proposals in the window
|
|
1209
|
+
// whose ref also has a consolidate skip-reason of "contradict_target_missing"
|
|
1210
|
+
// or an event indicating contradiction. Uses the improve_runs result_json
|
|
1211
|
+
// consolidation.contradicted count as a proxy.
|
|
1212
|
+
// Simple implementation: contradictionRate = total_contradicted / max(1, total_processed)
|
|
1213
|
+
// sourced from the window's consolidation envelope.
|
|
1214
|
+
// (The full "merge proposal → later contradiction" correlation requires cross-run
|
|
1215
|
+
// history; this is the available proxy.)
|
|
1216
|
+
let mergeFidelityContradictionRate = 0;
|
|
1217
|
+
try {
|
|
1218
|
+
const runs = queryImproveRuns(db, since, until);
|
|
1219
|
+
let totalContradicted = 0;
|
|
1220
|
+
let totalProcessed = 0;
|
|
1221
|
+
for (const row of runs) {
|
|
1222
|
+
try {
|
|
1223
|
+
const result = JSON.parse(row.result_json);
|
|
1224
|
+
const cons = result.consolidation;
|
|
1225
|
+
if (cons) {
|
|
1226
|
+
totalContradicted += toFiniteNumber(cons.contradicted);
|
|
1227
|
+
totalProcessed += toFiniteNumber(cons.processed);
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
catch {
|
|
1231
|
+
// Skip malformed rows.
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
if (totalProcessed > 0) {
|
|
1235
|
+
mergeFidelityContradictionRate = roundRate(totalContradicted / totalProcessed);
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
catch {
|
|
1239
|
+
// Fail open.
|
|
1240
|
+
}
|
|
1241
|
+
// (c) Generation distribution — fraction of asset_salience rows with
|
|
1242
|
+
// generation >= 2. Generation is NOT currently stored in asset_salience
|
|
1243
|
+
// (it's in frontmatter). We approximate using consecutive_no_ops as a
|
|
1244
|
+
// maturity proxy: assets that have never been no-op'd are "fresh".
|
|
1245
|
+
// TODO(0.10+): store generation in asset_salience for proper tracking.
|
|
1246
|
+
let highGenerationFraction = Number.NaN;
|
|
1247
|
+
try {
|
|
1248
|
+
const genRows = db.prepare("SELECT consecutive_no_ops FROM asset_salience").all();
|
|
1249
|
+
if (genRows.length > 0) {
|
|
1250
|
+
// Use consecutive_no_ops >= 2 as a proxy for "has been through merge cycles".
|
|
1251
|
+
const highGen = genRows.filter((r) => r.consecutive_no_ops >= 2).length;
|
|
1252
|
+
highGenerationFraction = roundRate(highGen / genRows.length);
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
catch {
|
|
1256
|
+
// Table not present.
|
|
1257
|
+
}
|
|
1258
|
+
// (d) Oracle spot-check — up to 5 recently accepted proposals in the window.
|
|
1259
|
+
const oracleSpotCheck = [];
|
|
1260
|
+
try {
|
|
1261
|
+
const accepted = listStateProposals(db, { status: "accepted" }).filter((p) => {
|
|
1262
|
+
const updatedAt = p.updatedAt ?? "";
|
|
1263
|
+
return updatedAt >= since && updatedAt < until;
|
|
1264
|
+
});
|
|
1265
|
+
// Sample up to 5: pick evenly spaced (not just the first 5).
|
|
1266
|
+
const step = Math.max(1, Math.floor(accepted.length / 5));
|
|
1267
|
+
for (let i = 0; i < accepted.length && oracleSpotCheck.length < 5; i += step) {
|
|
1268
|
+
const p = accepted[i];
|
|
1269
|
+
if (p) {
|
|
1270
|
+
oracleSpotCheck.push({
|
|
1271
|
+
proposalId: p.id,
|
|
1272
|
+
ref: p.ref,
|
|
1273
|
+
source: p.source ?? "unknown",
|
|
1274
|
+
acceptedAt: p.updatedAt ?? p.createdAt ?? "",
|
|
1275
|
+
});
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
catch {
|
|
1280
|
+
// Fail open.
|
|
1281
|
+
}
|
|
1282
|
+
return {
|
|
1283
|
+
corpusCentroidDistance,
|
|
1284
|
+
entrenchmentFlagged,
|
|
1285
|
+
mergeFidelityContradictionRate,
|
|
1286
|
+
highGenerationFraction,
|
|
1287
|
+
oracleSpotCheck,
|
|
1288
|
+
};
|
|
1289
|
+
}
|
|
1290
|
+
function buildWindowMetrics(db, stateDbPath, since, until, now = () => Date.now(), logsDb) {
|
|
886
1291
|
const taskRows = queryTaskHistory(db, { since }).filter((row) => {
|
|
887
1292
|
const startMs = new Date(row.started_at).getTime();
|
|
888
1293
|
const untilMs = new Date(until).getTime();
|
|
889
1294
|
return !Number.isFinite(untilMs) || startMs < untilMs;
|
|
890
1295
|
});
|
|
891
|
-
const taskRowsWithLogs
|
|
892
|
-
const existingLogRows = taskRowsWithLogs.filter((row) => row.log_path && fs.existsSync(row.log_path));
|
|
1296
|
+
const { withLogs: taskRowsWithLogs, backed: existingLogRows } = partitionLogBackedRows(taskRows, logsDb);
|
|
893
1297
|
const failedTaskRows = taskRows.filter((row) => row.status === "failed");
|
|
894
1298
|
const activeRows = taskRows.filter((row) => row.status === "active");
|
|
895
1299
|
const stuckActiveRuns = activeRows.filter((row) => now() - new Date(row.started_at).getTime() > ACTIVE_RUN_WARN_MS).length;
|
|
@@ -917,12 +1321,24 @@ function buildWindowMetrics(db, stateDbPath, since, until, now = () => Date.now(
|
|
|
917
1321
|
const perRunSummaries = buildPerRunSummaries(db, since, until);
|
|
918
1322
|
const wallTimes = perRunSummaries.map((run) => run.wallTimeMs).filter((ms) => Number.isFinite(ms) && ms > 0);
|
|
919
1323
|
improveSummary.wallTime = computeWallTimeStats(wallTimes, improveSummary.wallTime.byPhase);
|
|
1324
|
+
improveSummary.calibration = readCalibration(db, since, until);
|
|
1325
|
+
// WS-5: Compute denominator-fixed coverage from the most recent run's
|
|
1326
|
+
// memorySummary (totalAssets = eligible + derived — the fixed denominator).
|
|
1327
|
+
const totalAssets = improveSummary.memorySummary.eligible + improveSummary.memorySummary.derived;
|
|
1328
|
+
improveSummary.coverage = computeDenominatorFixedCoverage(db, totalAssets, improveSummary.memorySummary.eligible, since, until);
|
|
1329
|
+
// WS-5: Compute per-run degradation metrics (corpus diversity, merge fidelity,
|
|
1330
|
+
// generation distribution, oracle spot-check). Health VIEWS only.
|
|
1331
|
+
const degradation = computeDegradationMetrics(db, since, until);
|
|
1332
|
+
if (degradation) {
|
|
1333
|
+
improveSummary.degradation = degradation;
|
|
1334
|
+
}
|
|
920
1335
|
const metrics = {
|
|
921
1336
|
taskFailRate: roundRate(taskFailRate),
|
|
922
1337
|
agentFailureRate: roundRate(agentFailureRate),
|
|
923
1338
|
stuckActiveRuns,
|
|
924
1339
|
logBackingRate: roundRate(logBackingRate),
|
|
925
1340
|
probeRoundTripMs: null,
|
|
1341
|
+
llmUsage: readLlmUsageAggregate(stateDbPath, since, until),
|
|
926
1342
|
};
|
|
927
1343
|
return { improve: improveSummary, metrics, runs: runCount };
|
|
928
1344
|
}
|
|
@@ -961,6 +1377,16 @@ export function akmHealth(options = {}) {
|
|
|
961
1377
|
catch (error) {
|
|
962
1378
|
throw new ConfigError(`Unable to open state.db: ${error instanceof Error ? error.message : String(error)}`, "INVALID_CONFIG_FILE");
|
|
963
1379
|
}
|
|
1380
|
+
// logs.db backs the log-backing metric (#579). Best-effort: when it cannot
|
|
1381
|
+
// be opened, partitionLogBackedRows falls back to the on-disk file check, so
|
|
1382
|
+
// health never hard-fails on a missing/locked logs database.
|
|
1383
|
+
let logsDb;
|
|
1384
|
+
try {
|
|
1385
|
+
logsDb = openLogsDatabase(options.logsDbPath);
|
|
1386
|
+
}
|
|
1387
|
+
catch {
|
|
1388
|
+
logsDb = undefined;
|
|
1389
|
+
}
|
|
964
1390
|
try {
|
|
965
1391
|
const tables = listExistingTableNames(db, ["events", "task_history", "proposals", "schema_migrations"]);
|
|
966
1392
|
const tableNames = tables.map((row) => row.name).sort();
|
|
@@ -968,8 +1394,7 @@ export function akmHealth(options = {}) {
|
|
|
968
1394
|
const missingTables = requiredTables.filter((name) => !tableNames.includes(name));
|
|
969
1395
|
const probe = probeStateDbRoundTrip(stateDbPath);
|
|
970
1396
|
const taskRows = queryTaskHistory(db, { since });
|
|
971
|
-
const taskRowsWithLogs
|
|
972
|
-
const existingLogRows = taskRowsWithLogs.filter((row) => row.log_path && fs.existsSync(row.log_path));
|
|
1397
|
+
const { withLogs: taskRowsWithLogs, backed: existingLogRows } = partitionLogBackedRows(taskRows, logsDb);
|
|
973
1398
|
const failedTaskRows = taskRows.filter((row) => row.status === "failed");
|
|
974
1399
|
const activeRows = taskRows.filter((row) => row.status === "active");
|
|
975
1400
|
const stuckActiveRuns = activeRows.filter((row) => now() - new Date(row.started_at).getTime() > ACTIVE_RUN_WARN_MS).length;
|
|
@@ -995,6 +1420,35 @@ export function akmHealth(options = {}) {
|
|
|
995
1420
|
const perRunSummaries = buildPerRunSummaries(db, since);
|
|
996
1421
|
const wallTimes = perRunSummaries.map((run) => run.wallTimeMs).filter((ms) => Number.isFinite(ms) && ms > 0);
|
|
997
1422
|
improveSummary.wallTime = computeWallTimeStats(wallTimes, improveSummary.wallTime.byPhase);
|
|
1423
|
+
improveSummary.calibration = readCalibration(db, since);
|
|
1424
|
+
// WS-5: Compute denominator-fixed coverage and per-run degradation metrics
|
|
1425
|
+
// for the main health path (not just window-compare mode).
|
|
1426
|
+
const until = new Date(now()).toISOString();
|
|
1427
|
+
const totalAssetsMain = improveSummary.memorySummary.eligible + improveSummary.memorySummary.derived;
|
|
1428
|
+
improveSummary.coverage = computeDenominatorFixedCoverage(db, totalAssetsMain, improveSummary.memorySummary.eligible, since, until);
|
|
1429
|
+
const degradationMain = computeDegradationMetrics(db, since, until);
|
|
1430
|
+
if (degradationMain) {
|
|
1431
|
+
improveSummary.degradation = degradationMain;
|
|
1432
|
+
}
|
|
1433
|
+
// WS-2 proxy-adequacy tripwire: surface any outcome_proxy_inverted events
|
|
1434
|
+
// in the health window as an advisory so operators know when the 0.10+
|
|
1435
|
+
// rich in-session signal is no longer deferrable.
|
|
1436
|
+
const proxyInvertedEvents = readEvents({ since, type: "outcome_proxy_inverted" }, { dbPath: stateDbPath }).events;
|
|
1437
|
+
if (proxyInvertedEvents.length > 0) {
|
|
1438
|
+
const lastEvent = proxyInvertedEvents[proxyInvertedEvents.length - 1];
|
|
1439
|
+
const correlation = typeof lastEvent.metadata?.correlation === "number" ? lastEvent.metadata.correlation.toFixed(3) : "unknown";
|
|
1440
|
+
advisories.push({
|
|
1441
|
+
name: "outcome-proxy-adequacy",
|
|
1442
|
+
status: "warn",
|
|
1443
|
+
kind: "deterministic",
|
|
1444
|
+
confidence: "high",
|
|
1445
|
+
message: `WS-2 outcome proxy inverted (${proxyInvertedEvents.length} event(s) in window). ` +
|
|
1446
|
+
`corr(outcome_score, accepted_change_rate) = ${correlation} < −0.3. ` +
|
|
1447
|
+
"Popular assets are also the most-needing-improvement assets — " +
|
|
1448
|
+
"the retrieval-based proxy is inverted. " +
|
|
1449
|
+
"The 0.10+ rich in-session outcome signal is no longer deferrable. See plan §WS-2.",
|
|
1450
|
+
});
|
|
1451
|
+
}
|
|
998
1452
|
let sessionLogEntries = [];
|
|
999
1453
|
try {
|
|
1000
1454
|
const sinceDays = Math.max(0, Math.ceil((now() - new Date(since).getTime()) / (24 * 60 * 60 * 1000)));
|
|
@@ -1041,6 +1495,7 @@ export function akmHealth(options = {}) {
|
|
|
1041
1495
|
stuckActiveRuns,
|
|
1042
1496
|
logBackingRate: roundRate(logBackingRate),
|
|
1043
1497
|
probeRoundTripMs: probe.durationMs,
|
|
1498
|
+
llmUsage: readLlmUsageAggregate(stateDbPath, since),
|
|
1044
1499
|
};
|
|
1045
1500
|
const hardFailure = hardChecks.some((check) => check.status === "fail");
|
|
1046
1501
|
const deterministicWarnings = [...hardChecks, ...advisories].some((check) => check.status === "warn" && check.kind === "deterministic");
|
|
@@ -1062,7 +1517,7 @@ export function akmHealth(options = {}) {
|
|
|
1062
1517
|
windowResults = windowSpecs.map((spec) => {
|
|
1063
1518
|
const winSince = parseHealthSince(spec.since);
|
|
1064
1519
|
const winUntil = spec.until ? parseHealthSince(spec.until) : new Date(now()).toISOString();
|
|
1065
|
-
const bundle = buildWindowMetrics(db, stateDbPath, winSince, winUntil, now);
|
|
1520
|
+
const bundle = buildWindowMetrics(db, stateDbPath, winSince, winUntil, now, logsDb);
|
|
1066
1521
|
return {
|
|
1067
1522
|
name: spec.name,
|
|
1068
1523
|
since: winSince,
|
|
@@ -1112,6 +1567,14 @@ export function akmHealth(options = {}) {
|
|
|
1112
1567
|
}
|
|
1113
1568
|
finally {
|
|
1114
1569
|
db.close();
|
|
1570
|
+
if (logsDb) {
|
|
1571
|
+
try {
|
|
1572
|
+
logsDb.close();
|
|
1573
|
+
}
|
|
1574
|
+
catch {
|
|
1575
|
+
// best-effort
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1115
1578
|
}
|
|
1116
1579
|
}
|
|
1117
1580
|
// ── Markdown renderers ───────────────────────────────────────────────────────
|