akm-cli 0.9.0-beta.52 → 0.9.0-beta.54
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/dist/assets/hints/cli-hints-full.md +6 -5
- package/dist/cli/clack.js +56 -0
- package/dist/cli/confirm.js +1 -1
- package/dist/cli.js +0 -7
- package/dist/commands/env/env-cli.js +3 -2
- package/dist/commands/env/env.js +14 -67
- package/dist/commands/health/checks.js +28 -15
- package/dist/commands/health/html-report.js +33 -10
- package/dist/commands/health.js +222 -22
- package/dist/commands/improve/collapse-detector.js +419 -0
- package/dist/commands/improve/consolidate.js +72 -54
- package/dist/commands/improve/distill.js +79 -13
- package/dist/commands/improve/extract.js +13 -6
- package/dist/commands/improve/homeostatic.js +109 -79
- package/dist/commands/improve/improve-cli.js +67 -1
- package/dist/commands/improve/improve.js +10 -0
- package/dist/commands/improve/loop-stages.js +39 -1
- package/dist/commands/improve/outcome-loop.js +33 -19
- package/dist/commands/improve/preparation.js +36 -11
- package/dist/commands/improve/salience.js +49 -32
- package/dist/commands/read/curate.js +9 -13
- package/dist/commands/read/knowledge.js +4 -0
- package/dist/commands/read/search-cli.js +6 -4
- package/dist/commands/read/search.js +12 -5
- package/dist/commands/read/show.js +6 -8
- package/dist/commands/sources/add-cli.js +1 -1
- package/dist/commands/sources/init.js +12 -0
- package/dist/commands/sources/stash-cli.js +1 -1
- package/dist/commands/tasks/default-tasks.js +12 -0
- package/dist/core/asset/asset-spec.js +3 -2
- package/dist/core/config/config-schema.js +39 -17
- package/dist/core/config/config.js +12 -0
- package/dist/core/eval/rank-metrics.js +113 -0
- package/dist/core/state/migrations.js +56 -0
- package/dist/core/state-db.js +146 -19
- package/dist/core/warn.js +21 -0
- package/dist/indexer/db/db.js +6 -0
- package/dist/indexer/ensure-index.js +36 -92
- package/dist/indexer/index-writer-lock.js +9 -11
- package/dist/indexer/index-written-assets.js +105 -0
- package/dist/indexer/indexer.js +16 -4
- package/dist/indexer/passes/metadata.js +20 -0
- package/dist/indexer/read-preflight.js +23 -0
- package/dist/indexer/search/db-search.js +29 -1
- package/dist/indexer/search/ranking-contributors.js +33 -1
- package/dist/indexer/search/ranking.js +66 -0
- package/dist/indexer/search/search-fields.js +6 -0
- package/dist/indexer/walk/walker.js +21 -13
- package/dist/integrations/agent/detect.js +9 -0
- package/dist/integrations/agent/index.js +1 -1
- package/dist/llm/client.js +12 -0
- package/dist/llm/embedder.js +26 -2
- package/dist/llm/embedders/local.js +7 -1
- package/dist/llm/feature-gate.js +6 -2
- package/dist/output/renderers.js +8 -13
- package/dist/output/shapes/helpers.js +0 -3
- package/dist/output/shapes/passthrough.js +1 -0
- package/dist/scripts/migrate-storage.js +178 -35
- package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +46 -19
- package/dist/setup/detect.js +9 -0
- package/dist/setup/registry-stash-loader.js +12 -0
- package/dist/setup/setup.js +1 -1
- package/dist/storage/repositories/index-db.js +10 -1
- package/dist/tasks/backends/index.js +9 -0
- package/dist/tasks/runner.js +9 -0
- package/package.json +2 -4
package/dist/commands/health.js
CHANGED
|
@@ -2,17 +2,29 @@
|
|
|
2
2
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
3
|
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
4
|
import fs from "node:fs";
|
|
5
|
+
import { loadConfig } from "../core/config/config.js";
|
|
5
6
|
import { ConfigError, UsageError } from "../core/errors.js";
|
|
6
7
|
import { appendEvent, readEvents } from "../core/events.js";
|
|
7
8
|
import { buildTaskRunId, getLoggedRunIds, openLogsDatabase } from "../core/logs-db.js";
|
|
8
9
|
import { getStateDbPathInDataDir } from "../core/paths.js";
|
|
9
|
-
import { listExistingTableNames, listProposalGateDecisions, listStateProposals, openStateDatabase, queryCompletedTaskIntervals, queryImproveRuns, queryTaskHistory, } from "../core/state-db.js";
|
|
10
|
+
import { getLatestCycleMetrics, listExistingTableNames, listProposalGateDecisions, listStateProposals, openStateDatabase, queryCompletedTaskIntervals, queryImproveRuns, queryTaskHistory, } from "../core/state-db.js";
|
|
10
11
|
import { parseSinceToIso } from "../core/time.js";
|
|
11
12
|
import { readSemanticStatus } from "../indexer/search/semantic-status.js";
|
|
12
13
|
import { getExecutionLogCandidates } from "../integrations/session-logs/index.js";
|
|
13
14
|
import { LLM_USAGE_EVENT } from "../llm/usage-persist.js";
|
|
14
15
|
import { HEALTH_CHECKS } from "./health/checks.js";
|
|
15
16
|
import { gateDecisionsToSamples, summarizeCalibration } from "./improve/calibration.js";
|
|
17
|
+
/**
|
|
18
|
+
* Lanes ratified as ENRICHMENT-ONLY: they may propose edits to existing
|
|
19
|
+
* assets (metadata, relations, content refresh) but must not mint new ones.
|
|
20
|
+
* New-asset generation belongs to the signal-gated minting lanes
|
|
21
|
+
* (extract/distill/memory-inference/recombine).
|
|
22
|
+
*/
|
|
23
|
+
export const ENRICHMENT_LANES = ["proactive", "high-salience", "high-retrieval", "signal-delta"];
|
|
24
|
+
/** Minted share of enrichment-lane accepts that triggers a WARN advisory. */
|
|
25
|
+
export const ENRICHMENT_MINTED_WARN_SHARE = 0.05;
|
|
26
|
+
/** Minted share of enrichment-lane accepts that triggers a FAIL advisory. */
|
|
27
|
+
export const ENRICHMENT_MINTED_FAIL_SHARE = 0.15;
|
|
16
28
|
const DEFAULT_SINCE_MS = 24 * 60 * 60 * 1000;
|
|
17
29
|
const IMPROVE_COMPLETED_EVENT = "improve_completed";
|
|
18
30
|
const HEALTH_PROBE_EVENT = "health_probe";
|
|
@@ -172,6 +184,8 @@ function createUnknownImproveMetrics() {
|
|
|
172
184
|
rate: Number.NaN,
|
|
173
185
|
eligibleFraction: Number.NaN,
|
|
174
186
|
acceptedProposals: 0,
|
|
187
|
+
distinctRefs: 0,
|
|
188
|
+
churnRatio: Number.NaN,
|
|
175
189
|
totalAssets: 0,
|
|
176
190
|
},
|
|
177
191
|
};
|
|
@@ -1133,6 +1147,7 @@ function readCalibration(db, since, until) {
|
|
|
1133
1147
|
*/
|
|
1134
1148
|
function computeDenominatorFixedCoverage(db, totalAssets, eligibleAssets, since, until, stashDir) {
|
|
1135
1149
|
let acceptedProposals = 0;
|
|
1150
|
+
let distinctRefs = 0;
|
|
1136
1151
|
try {
|
|
1137
1152
|
const proposals = listStateProposals(db, {
|
|
1138
1153
|
status: "accepted",
|
|
@@ -1146,25 +1161,91 @@ function computeDenominatorFixedCoverage(db, totalAssets, eligibleAssets, since,
|
|
|
1146
1161
|
return true;
|
|
1147
1162
|
});
|
|
1148
1163
|
acceptedProposals = proposals.length;
|
|
1164
|
+
// Coverage counts DISTINCT refs: N accepted rewrites of one asset are
|
|
1165
|
+
// churn, not coverage. The raw proposal count is kept alongside so the
|
|
1166
|
+
// churn ratio (proposals ÷ distinct refs) stays visible.
|
|
1167
|
+
distinctRefs = new Set(proposals.map((p) => p.ref)).size;
|
|
1149
1168
|
}
|
|
1150
1169
|
catch {
|
|
1151
1170
|
// Fail open: table may not exist on older installs.
|
|
1152
1171
|
}
|
|
1172
|
+
const churnRatio = distinctRefs > 0 ? roundRate(acceptedProposals / distinctRefs) : Number.NaN;
|
|
1153
1173
|
if (totalAssets === 0) {
|
|
1154
1174
|
return {
|
|
1155
1175
|
rate: Number.NaN,
|
|
1156
1176
|
eligibleFraction: Number.NaN,
|
|
1157
1177
|
acceptedProposals,
|
|
1178
|
+
distinctRefs,
|
|
1179
|
+
churnRatio,
|
|
1158
1180
|
totalAssets: 0,
|
|
1159
1181
|
};
|
|
1160
1182
|
}
|
|
1161
1183
|
return {
|
|
1162
|
-
rate: roundRate(
|
|
1184
|
+
rate: roundRate(distinctRefs / totalAssets),
|
|
1163
1185
|
eligibleFraction: roundRate(eligibleAssets / totalAssets),
|
|
1164
1186
|
acceptedProposals,
|
|
1187
|
+
distinctRefs,
|
|
1188
|
+
churnRatio,
|
|
1165
1189
|
totalAssets,
|
|
1166
1190
|
};
|
|
1167
1191
|
}
|
|
1192
|
+
/**
|
|
1193
|
+
* Compute the enrichment-vs-minting rollup over the window's accepted,
|
|
1194
|
+
* lane-attributed proposals (reporting-only; see {@link EnrichmentMintingRollup}).
|
|
1195
|
+
*
|
|
1196
|
+
* SQL-side `json_extract` keeps the (potentially large) `backupContent` blobs
|
|
1197
|
+
* out of process memory. Pre-Phase-6C rows without an `eligibilitySource`
|
|
1198
|
+
* cannot be lane-classified and are excluded. Fails open (undefined) when the
|
|
1199
|
+
* proposals table is absent.
|
|
1200
|
+
*/
|
|
1201
|
+
export function computeEnrichmentMintingRollup(db, since, until) {
|
|
1202
|
+
try {
|
|
1203
|
+
const rows = db
|
|
1204
|
+
.prepare(`SELECT
|
|
1205
|
+
json_extract(metadata_json, '$.eligibilitySource') AS lane,
|
|
1206
|
+
CASE WHEN json_extract(metadata_json, '$.backupContent') IS NULL THEN 1 ELSE 0 END AS is_minted,
|
|
1207
|
+
COUNT(*) AS cnt
|
|
1208
|
+
FROM proposals
|
|
1209
|
+
WHERE status = 'accepted'
|
|
1210
|
+
AND updated_at >= ?
|
|
1211
|
+
AND (? IS NULL OR updated_at < ?)
|
|
1212
|
+
AND json_extract(metadata_json, '$.eligibilitySource') IS NOT NULL
|
|
1213
|
+
AND json_extract(metadata_json, '$.eligibilitySource') != ''
|
|
1214
|
+
GROUP BY lane, is_minted`)
|
|
1215
|
+
.all(since, until ?? null, until ?? null);
|
|
1216
|
+
if (rows.length === 0)
|
|
1217
|
+
return undefined;
|
|
1218
|
+
const byLane = {};
|
|
1219
|
+
for (const row of rows) {
|
|
1220
|
+
byLane[row.lane] ??= { minted: 0, updated: 0 };
|
|
1221
|
+
const entry = byLane[row.lane];
|
|
1222
|
+
if (row.is_minted === 1)
|
|
1223
|
+
entry.minted += row.cnt;
|
|
1224
|
+
else
|
|
1225
|
+
entry.updated += row.cnt;
|
|
1226
|
+
}
|
|
1227
|
+
let minted = 0;
|
|
1228
|
+
let updated = 0;
|
|
1229
|
+
for (const lane of ENRICHMENT_LANES) {
|
|
1230
|
+
const entry = byLane[lane];
|
|
1231
|
+
if (!entry)
|
|
1232
|
+
continue;
|
|
1233
|
+
minted += entry.minted;
|
|
1234
|
+
updated += entry.updated;
|
|
1235
|
+
}
|
|
1236
|
+
const decided = minted + updated;
|
|
1237
|
+
return {
|
|
1238
|
+
minted,
|
|
1239
|
+
updated,
|
|
1240
|
+
share: decided > 0 ? roundRate(minted / decided) : Number.NaN,
|
|
1241
|
+
byLane,
|
|
1242
|
+
};
|
|
1243
|
+
}
|
|
1244
|
+
catch {
|
|
1245
|
+
// Fail open: proposals table may not exist on older installs.
|
|
1246
|
+
return undefined;
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1168
1249
|
/**
|
|
1169
1250
|
* Compute WS-5 per-run degradation metrics (Part V §4).
|
|
1170
1251
|
*
|
|
@@ -1175,7 +1256,7 @@ function computeDenominatorFixedCoverage(db, totalAssets, eligibleAssets, since,
|
|
|
1175
1256
|
* @param since - Window start (ISO-8601).
|
|
1176
1257
|
* @param until - Window end (ISO-8601).
|
|
1177
1258
|
*/
|
|
1178
|
-
function computeDegradationMetrics(db, since, until) {
|
|
1259
|
+
export function computeDegradationMetrics(db, since, until) {
|
|
1179
1260
|
// (a) Corpus diversity — salience rank distribution of the top-100 assets.
|
|
1180
1261
|
// We use the Gini coefficient of retrieval_salience scores as an intra-corpus
|
|
1181
1262
|
// diversity proxy. A Gini close to 1 = highly concentrated (entrenched top
|
|
@@ -1183,6 +1264,7 @@ function computeDegradationMetrics(db, since, until) {
|
|
|
1183
1264
|
// consecutive-run centroid distance requires cross-run history not yet stored.
|
|
1184
1265
|
let corpusCentroidDistance = Number.NaN;
|
|
1185
1266
|
let entrenchmentFlagged;
|
|
1267
|
+
let salienceUniformityFlagged;
|
|
1186
1268
|
try {
|
|
1187
1269
|
const rows = db
|
|
1188
1270
|
.prepare(`SELECT retrieval_salience FROM asset_salience
|
|
@@ -1201,9 +1283,13 @@ function computeDegradationMetrics(db, since, until) {
|
|
|
1201
1283
|
// corpusCentroidDistance approximation: gini is "distance from uniform".
|
|
1202
1284
|
// Note: retrieval_salience values are in [0,1], so the max achievable Gini
|
|
1203
1285
|
// with this formula is ~0.5 (when one asset dominates and others are near 0).
|
|
1204
|
-
//
|
|
1286
|
+
// Two-tailed: >0.35 flags entrenchment (robustly above the ~0.1 uniform
|
|
1287
|
+
// baseline); <0.08 flags uniformity collapse — the distribution no longer
|
|
1288
|
+
// discriminates between assets (live 2026-07 value 0.040 sat unflagged
|
|
1289
|
+
// in this tail under the old one-tailed check).
|
|
1205
1290
|
corpusCentroidDistance = roundRate(gini);
|
|
1206
1291
|
entrenchmentFlagged = gini > 0.35;
|
|
1292
|
+
salienceUniformityFlagged = gini < 0.08;
|
|
1207
1293
|
}
|
|
1208
1294
|
}
|
|
1209
1295
|
catch {
|
|
@@ -1242,23 +1328,11 @@ function computeDegradationMetrics(db, since, until) {
|
|
|
1242
1328
|
catch {
|
|
1243
1329
|
// Fail open.
|
|
1244
1330
|
}
|
|
1245
|
-
// (c)
|
|
1246
|
-
//
|
|
1247
|
-
// (
|
|
1248
|
-
//
|
|
1249
|
-
//
|
|
1250
|
-
let highGenerationFraction = Number.NaN;
|
|
1251
|
-
try {
|
|
1252
|
-
const genRows = db.prepare("SELECT consecutive_no_ops FROM asset_salience").all();
|
|
1253
|
-
if (genRows.length > 0) {
|
|
1254
|
-
// Use consecutive_no_ops >= 2 as a proxy for "has been through merge cycles".
|
|
1255
|
-
const highGen = genRows.filter((r) => r.consecutive_no_ops >= 2).length;
|
|
1256
|
-
highGenerationFraction = roundRate(highGen / genRows.length);
|
|
1257
|
-
}
|
|
1258
|
-
}
|
|
1259
|
-
catch {
|
|
1260
|
-
// Table not present.
|
|
1261
|
-
}
|
|
1331
|
+
// (c) highGenerationFraction was DELETED (meta-review 05 DRIFT-3): it
|
|
1332
|
+
// approximated "LLM-merge generations" from consecutive_no_ops — which counts
|
|
1333
|
+
// the opposite condition (cycles where nothing was changed) — and its own
|
|
1334
|
+
// in-code TODO admitted the proxy. Display-only, never actionable; removed
|
|
1335
|
+
// rather than instrumented.
|
|
1262
1336
|
// (d) Oracle spot-check — up to 5 recently accepted proposals in the window.
|
|
1263
1337
|
const oracleSpotCheck = [];
|
|
1264
1338
|
try {
|
|
@@ -1286,8 +1360,8 @@ function computeDegradationMetrics(db, since, until) {
|
|
|
1286
1360
|
return {
|
|
1287
1361
|
corpusCentroidDistance,
|
|
1288
1362
|
entrenchmentFlagged,
|
|
1363
|
+
salienceUniformityFlagged,
|
|
1289
1364
|
mergeFidelityContradictionRate,
|
|
1290
|
-
highGenerationFraction,
|
|
1291
1365
|
oracleSpotCheck,
|
|
1292
1366
|
};
|
|
1293
1367
|
}
|
|
@@ -1411,6 +1485,18 @@ export function akmHealth(options = {}) {
|
|
|
1411
1485
|
const taskFailRate = taskRows.length === 0 ? 0 : failedTaskRows.length / taskRows.length;
|
|
1412
1486
|
const agentFailureRate = promptRows.length === 0 ? 0 : promptFailures.length / promptRows.length;
|
|
1413
1487
|
const semanticStatus = readSemanticStatus();
|
|
1488
|
+
// For the embedding-endpoint advisory. Best-effort: an unloadable config
|
|
1489
|
+
// leaves both undefined and the check falls back to its generic message.
|
|
1490
|
+
let semanticSearchMode;
|
|
1491
|
+
let embeddingEndpoint;
|
|
1492
|
+
try {
|
|
1493
|
+
const config = loadConfig();
|
|
1494
|
+
semanticSearchMode = config.semanticSearchMode;
|
|
1495
|
+
embeddingEndpoint = config.embedding?.endpoint;
|
|
1496
|
+
}
|
|
1497
|
+
catch {
|
|
1498
|
+
// fall through with undefined
|
|
1499
|
+
}
|
|
1414
1500
|
const improveInvoked = readEvents({ since, type: "improve_invoked" }, { dbPath: stateDbPath }).events.length;
|
|
1415
1501
|
const improveCompletedEvents = readEvents({ since, type: IMPROVE_COMPLETED_EVENT }, { dbPath: stateDbPath }).events;
|
|
1416
1502
|
const improveSkippedEvents = readEvents({ since, type: "improve_skipped" }, { dbPath: stateDbPath }).events;
|
|
@@ -1434,6 +1520,7 @@ export function akmHealth(options = {}) {
|
|
|
1434
1520
|
if (degradationMain) {
|
|
1435
1521
|
improveSummary.degradation = degradationMain;
|
|
1436
1522
|
}
|
|
1523
|
+
improveSummary.enrichmentMinting = computeEnrichmentMintingRollup(db, since, until);
|
|
1437
1524
|
// WS-2 proxy-adequacy tripwire: surface any outcome_proxy_inverted events
|
|
1438
1525
|
// in the health window as an advisory so operators know when the 0.10+
|
|
1439
1526
|
// rich in-session signal is no longer deferrable.
|
|
@@ -1453,6 +1540,117 @@ export function akmHealth(options = {}) {
|
|
|
1453
1540
|
"The 0.10+ rich in-session outcome signal is no longer deferrable. See plan §WS-2.",
|
|
1454
1541
|
});
|
|
1455
1542
|
}
|
|
1543
|
+
// Two-tailed companion: a proxy that decays to noise (|corr| < 0.1 at scale)
|
|
1544
|
+
// is as much a failure as an inverted one — it just fails silently.
|
|
1545
|
+
const proxyDeadEvents = readEvents({ since, type: "outcome_proxy_dead" }, { dbPath: stateDbPath, db }).events;
|
|
1546
|
+
if (proxyDeadEvents.length > 0) {
|
|
1547
|
+
const lastEvent = proxyDeadEvents[proxyDeadEvents.length - 1];
|
|
1548
|
+
const correlation = typeof lastEvent.metadata?.correlation === "number" ? lastEvent.metadata.correlation.toFixed(3) : "unknown";
|
|
1549
|
+
advisories.push({
|
|
1550
|
+
name: "outcome-proxy-dead",
|
|
1551
|
+
status: "warn",
|
|
1552
|
+
kind: "deterministic",
|
|
1553
|
+
confidence: "high",
|
|
1554
|
+
message: `WS-2 outcome proxy is DEAD (${proxyDeadEvents.length} event(s) in window). ` +
|
|
1555
|
+
`|corr(outcome_score, accepted_change_rate)| = ${correlation} < 0.1 at n ≥ 500. ` +
|
|
1556
|
+
"outcome_score is statistically unrelated to improvement outcomes — " +
|
|
1557
|
+
"treat outcome-derived rank contributions as noise until a real usage/outcome signal lands.",
|
|
1558
|
+
});
|
|
1559
|
+
}
|
|
1560
|
+
// Salience-distribution collapse: Gini below the uniform baseline means
|
|
1561
|
+
// ranking no longer discriminates between assets.
|
|
1562
|
+
if (improveSummary.degradation?.salienceUniformityFlagged) {
|
|
1563
|
+
advisories.push({
|
|
1564
|
+
name: "salience-uniformity-collapse",
|
|
1565
|
+
status: "warn",
|
|
1566
|
+
kind: "deterministic",
|
|
1567
|
+
confidence: "high",
|
|
1568
|
+
message: `Salience distribution collapsed toward uniform: top-100 retrieval_salience Gini = ` +
|
|
1569
|
+
`${improveSummary.degradation.corpusCentroidDistance} < 0.08 (uniform baseline ≈ 0.1). ` +
|
|
1570
|
+
"Ranking currently carries little to no discrimination between assets.",
|
|
1571
|
+
});
|
|
1572
|
+
}
|
|
1573
|
+
// Enrichment-vs-minting policy: enrichment lanes edit existing assets;
|
|
1574
|
+
// a rising minted share means a lane is generating new content instead.
|
|
1575
|
+
const minting = improveSummary.enrichmentMinting;
|
|
1576
|
+
if (minting && Number.isFinite(minting.share) && minting.share > ENRICHMENT_MINTED_WARN_SHARE) {
|
|
1577
|
+
advisories.push({
|
|
1578
|
+
name: "enrichment-lane-minting",
|
|
1579
|
+
status: minting.share > ENRICHMENT_MINTED_FAIL_SHARE ? "fail" : "warn",
|
|
1580
|
+
kind: "deterministic",
|
|
1581
|
+
confidence: "high",
|
|
1582
|
+
message: `Enrichment lanes minted ${minting.minted} NEW asset(s) vs ${minting.updated} update(s) ` +
|
|
1583
|
+
`(${Math.round(minting.share * 100)}% minted, threshold ${Math.round(ENRICHMENT_MINTED_WARN_SHARE * 100)}%). ` +
|
|
1584
|
+
"Enrichment-classed lanes (proactive/high-salience/high-retrieval/signal-delta) are ratified to edit " +
|
|
1585
|
+
"existing assets only — new-asset generation belongs to the signal-gated minting lanes.",
|
|
1586
|
+
});
|
|
1587
|
+
}
|
|
1588
|
+
// Churn: accepted proposals far exceeding distinct touched refs means the
|
|
1589
|
+
// loop is repeatedly rewriting the same assets, not covering the corpus.
|
|
1590
|
+
if (Number.isFinite(improveSummary.coverage.churnRatio) && improveSummary.coverage.churnRatio > 1.5) {
|
|
1591
|
+
advisories.push({
|
|
1592
|
+
name: "improve-churn-ratio",
|
|
1593
|
+
status: "warn",
|
|
1594
|
+
kind: "deterministic",
|
|
1595
|
+
confidence: "high",
|
|
1596
|
+
message: `Improve churn ratio ${improveSummary.coverage.churnRatio} > 1.5: ` +
|
|
1597
|
+
`${improveSummary.coverage.acceptedProposals} accepted proposals touched only ` +
|
|
1598
|
+
`${improveSummary.coverage.distinctRefs} distinct assets in the window — ` +
|
|
1599
|
+
"repeated rewrites of the same refs count as churn, not coverage.",
|
|
1600
|
+
});
|
|
1601
|
+
}
|
|
1602
|
+
// R5 collapse/churn detector: surface any collapse_detector_alert events
|
|
1603
|
+
// in the health window, plus the latest cycle row's headline numbers so
|
|
1604
|
+
// the operator can act without opening the DB. `unknown` when the detector
|
|
1605
|
+
// has never produced a cycle row (no consolidate/recombine work yet).
|
|
1606
|
+
try {
|
|
1607
|
+
// Reuse the already-open state.db handle (readEvents supports a
|
|
1608
|
+
// borrowed connection) — no extra open/migrate/close per health call.
|
|
1609
|
+
const collapseAlertEvents = readEvents({ since, type: "collapse_detector_alert" }, { dbPath: stateDbPath, db }).events;
|
|
1610
|
+
const latestCycle = getLatestCycleMetrics(db);
|
|
1611
|
+
const cycleSummary = latestCycle
|
|
1612
|
+
? `Latest cycle (${latestCycle.ts}, ${latestCycle.pass}): mean canary recall ${latestCycle.mean_recall.toFixed(3)}, ` +
|
|
1613
|
+
`distinct-content ratio ${latestCycle.distinct_content_ratio.toFixed(3)}, ` +
|
|
1614
|
+
`${latestCycle.accepted_actions} accepted action(s).`
|
|
1615
|
+
: "";
|
|
1616
|
+
if (collapseAlertEvents.length > 0) {
|
|
1617
|
+
const kinds = [...new Set(collapseAlertEvents.map((e) => String(e.metadata?.kind ?? "unknown")))];
|
|
1618
|
+
const collapseKinds = kinds.filter((k) => k.startsWith("collapse"));
|
|
1619
|
+
advisories.push({
|
|
1620
|
+
name: "collapse-churn-detector",
|
|
1621
|
+
status: "warn",
|
|
1622
|
+
kind: "deterministic",
|
|
1623
|
+
// Collapse kinds are measured, not inferred; churn/merge-floor
|
|
1624
|
+
// volume thresholds are still being tuned (design doc §7).
|
|
1625
|
+
confidence: collapseKinds.length > 0 ? "high" : "medium",
|
|
1626
|
+
message: `R5 detector fired ${collapseAlertEvents.length} alert(s) in window (kinds: ${kinds.join(", ")}). ` +
|
|
1627
|
+
`${cycleSummary} See docs/design/improve-collapse-churn-detector-design.md §6.3 runbook queries.`,
|
|
1628
|
+
});
|
|
1629
|
+
}
|
|
1630
|
+
else if (latestCycle) {
|
|
1631
|
+
advisories.push({
|
|
1632
|
+
name: "collapse-churn-detector",
|
|
1633
|
+
status: "pass",
|
|
1634
|
+
kind: "deterministic",
|
|
1635
|
+
confidence: "high",
|
|
1636
|
+
message: `No collapse/churn alerts in window. ${cycleSummary}`,
|
|
1637
|
+
});
|
|
1638
|
+
}
|
|
1639
|
+
else {
|
|
1640
|
+
advisories.push({
|
|
1641
|
+
name: "collapse-churn-detector",
|
|
1642
|
+
status: "unknown",
|
|
1643
|
+
kind: "deterministic",
|
|
1644
|
+
confidence: "high",
|
|
1645
|
+
message: "No detector cycle rows yet — the collapse/churn detector runs only on improve cycles " +
|
|
1646
|
+
"where consolidate/recombine did work (synthesis lanes may be idle).",
|
|
1647
|
+
});
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
catch {
|
|
1651
|
+
// Table may predate migration 016 in odd mixed-version setups — advisory
|
|
1652
|
+
// is best-effort and must never fail the health command.
|
|
1653
|
+
}
|
|
1456
1654
|
let sessionLogEntries = [];
|
|
1457
1655
|
try {
|
|
1458
1656
|
const sinceDays = Math.max(0, Math.ceil((now() - new Date(since).getTime()) / (24 * 60 * 60 * 1000)));
|
|
@@ -1482,6 +1680,8 @@ export function akmHealth(options = {}) {
|
|
|
1482
1680
|
logBackingRate,
|
|
1483
1681
|
stuckActiveRuns,
|
|
1484
1682
|
semanticStatus,
|
|
1683
|
+
semanticSearchMode,
|
|
1684
|
+
embeddingEndpoint,
|
|
1485
1685
|
sessionLogEntries,
|
|
1486
1686
|
sessionExtraction: improveSummary.sessionExtraction,
|
|
1487
1687
|
autoAccept: improveSummary.autoAccept,
|