akm-cli 0.9.16 → 0.9.17-alpha.1
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 +478 -0
- package/dist/assets/prompts/consolidate-system.md +4 -11
- package/dist/assets/prompts/graph-extract-user-prompt.md +5 -5
- package/dist/commands/health/accept-rate.js +6 -0
- package/dist/commands/health/checks.js +54 -0
- package/dist/commands/health/improve-metrics.js +1 -5
- package/dist/commands/health/report-view-model.js +0 -1
- package/dist/commands/health.js +10 -0
- package/dist/commands/improve/consolidate/chunking.js +19 -35
- package/dist/commands/improve/consolidate/merge.js +6 -9
- package/dist/commands/improve/consolidate.js +104 -91
- package/dist/commands/improve/distill/promote-memory.js +40 -2
- package/dist/commands/improve/distill/quality-gate.js +186 -23
- package/dist/commands/improve/distill.js +42 -8
- package/dist/commands/improve/eligibility.js +13 -3
- package/dist/commands/improve/improve-cli.js +32 -9
- package/dist/commands/improve/improve-strategies.js +23 -1
- package/dist/commands/improve/improve.js +121 -84
- package/dist/commands/improve/loop-stages.js +241 -108
- package/dist/commands/improve/preparation.js +50 -17
- package/dist/commands/improve/reflect.js +16 -5
- package/dist/commands/improve/shared.js +0 -10
- package/dist/commands/proposal/drain.js +79 -10
- package/dist/commands/proposal/proposal-types.js +21 -0
- package/dist/commands/proposal/repository.js +108 -29
- package/dist/core/asset/frontmatter.js +106 -1
- package/dist/core/config/schema/improve-processes.js +29 -2
- package/dist/core/improve-result.js +9 -0
- package/dist/core/paths.js +7 -0
- package/dist/indexer/ensure-index.js +52 -7
- package/dist/indexer/graph/graph-extraction.js +82 -8
- package/dist/indexer/passes/memory-inference.js +16 -1
- package/dist/llm/client.js +16 -2
- package/dist/llm/graph-extract.js +162 -18
- package/dist/scripts/akm-migrate-node.js +20 -4
- package/dist/scripts/akm-migrate.js +20 -4
- package/dist/storage/repositories/index-entries-repository.js +43 -0
- package/dist/storage/repositories/proposals-repository.js +4 -1
- package/dist/storage/state-db-integrity.js +123 -0
- package/dist/workflows/program/schema.js +1 -0
- package/docs/reference/cli.md +4 -3
- package/docs/reference/data-and-telemetry.md +1 -0
- package/package.json +1 -1
- package/schemas/akm-config.json +44 -0
- package/schemas/akm-workflow.json +1 -0
- package/dist/commands/improve/eval-cases.js +0 -52
|
@@ -7,12 +7,13 @@ import { parseRefInput } from "../../core/asset/resolve-ref.js";
|
|
|
7
7
|
import { daysToMs } from "../../core/common.js";
|
|
8
8
|
import { DEFAULT_GRAPH_EXTRACTION_BATCH_SIZE, loadConfig } from "../../core/config/config.js";
|
|
9
9
|
import { UsageError } from "../../core/errors.js";
|
|
10
|
-
import { appendEvent } from "../../core/events.js";
|
|
10
|
+
import { appendEvent, readEvents } from "../../core/events.js";
|
|
11
11
|
import { openLogsDatabase, purgeOldTaskLogs } from "../../core/logs-db.js";
|
|
12
12
|
import { getDbPath, getTaskLogDir } from "../../core/paths.js";
|
|
13
13
|
import { withStateDb } from "../../core/state-db.js";
|
|
14
14
|
import { info } from "../../core/warn.js";
|
|
15
15
|
import { DEFAULT_GRAPH_EXTRACTION_INCLUDE_TYPES, runGraphExtractionPass, } from "../../indexer/graph/graph-extraction.js";
|
|
16
|
+
import { indexWrittenAssets } from "../../indexer/index-written-assets.js";
|
|
16
17
|
import { deriveWritableBundleIds } from "../../indexer/installations.js";
|
|
17
18
|
import { collectPendingMemories, runMemoryInferencePass, } from "../../indexer/passes/memory-inference.js";
|
|
18
19
|
import { resolveSourceEntries } from "../../indexer/search/search-source.js";
|
|
@@ -22,21 +23,23 @@ import { purgeOldCycleMetrics } from "../../storage/repositories/canaries-reposi
|
|
|
22
23
|
import { purgeOldEvents } from "../../storage/repositories/events-repository.js";
|
|
23
24
|
import { purgeOldImproveRuns } from "../../storage/repositories/improve-runs-repository.js";
|
|
24
25
|
import { closeDatabase, openIndexDatabase } from "../../storage/repositories/index-connection.js";
|
|
25
|
-
import {
|
|
26
|
+
import { getLiveRefSnapshot, isRefLiveInSnapshot, } from "../../storage/repositories/index-entries-repository.js";
|
|
26
27
|
import { clearAssetOutcomeMissing, countAssetOutcomeMissing, deleteAssetOutcomeMissingBefore, listAssetOutcomeMissingState, stampAssetOutcomeMissing, } from "../../storage/repositories/outcome-repository.js";
|
|
27
28
|
import { clearAssetSalienceMissing, countAssetSalienceMissing, deleteAssetSalienceMissingBefore, listAssetSalienceMissingState, stampAssetSalienceMissing, } from "../../storage/repositories/salience-repository.js";
|
|
29
|
+
import { readFreelistInfo, vacuumStateDbIfReclaimable } from "../../storage/state-db-integrity.js";
|
|
28
30
|
import { purgeOldTaskLogFiles } from "../../tasks/run/task-log.js";
|
|
29
|
-
import { expireStaleProposals, listProposals, purgeOrphanProposals } from "../proposal/repository.js";
|
|
31
|
+
import { checkProposalGuard, expireStaleProposals, listProposals, purgeOrphanProposals } from "../proposal/repository.js";
|
|
30
32
|
import { checkDeadUrls } from "../url-checker.js";
|
|
31
33
|
import { DEFAULT_RETENTION_DAYS as CYCLE_METRICS_RETENTION_DAYS, runCollapseDetector } from "./collapse-detector.js";
|
|
32
|
-
import { deriveLessonRef } from "./distill.js";
|
|
34
|
+
import { defaultLookup, deriveLessonRef } from "./distill.js";
|
|
35
|
+
import { wouldPromoteMemoryToKnowledge } from "./distill/promote-memory.js";
|
|
33
36
|
import { deriveKnowledgeRef } from "./distill-promotion-policy.js";
|
|
34
37
|
// Eligibility / candidate-selection predicates live in ./eligibility.
|
|
35
38
|
import { findAssetFilePath, isDistillCandidateRef } from "./eligibility.js";
|
|
36
|
-
import { writeEvalCase } from "./eval-cases.js";
|
|
37
39
|
import { shouldSkipRef } from "./improve-strategies.js";
|
|
40
|
+
import { readOnlyEventsContext } from "./reflect.js";
|
|
38
41
|
import { recordNoOp, resetConsecutiveNoOps } from "./salience.js";
|
|
39
|
-
import { errMessage
|
|
42
|
+
import { errMessage } from "./shared.js";
|
|
40
43
|
import { bareImproveRef, durableImproveRef } from "./source-identity.js";
|
|
41
44
|
// ── improve loop / post-loop / maintenance stages ───────────────────
|
|
42
45
|
// The cycle stages run by akmImprove, extracted from improve.ts.
|
|
@@ -179,6 +182,8 @@ async function runLoopReflectPass(planned, isDistillOnly, env, tally) {
|
|
|
179
182
|
// O-1 (#364): pass remaining budget as timeoutMs so the agent spawn is
|
|
180
183
|
// bounded by the wall-clock deadline rather than the default per-profile timeout.
|
|
181
184
|
const reflectBudgetMs = env.remainingBudgetMs();
|
|
185
|
+
const reflectEngine = resolvedPlan.processes.reflect.runner?.engine;
|
|
186
|
+
const reflectTarget = options.sourceName && primaryStashDir ? { source: options.sourceName, root: primaryStashDir } : undefined;
|
|
182
187
|
// Re-enter canonical named-engine lowering with the config snapshot and
|
|
183
188
|
// process profile frozen into the invocation plan. The loop never injects
|
|
184
189
|
// a RunnerSpec seam or observes later caller-config mutations.
|
|
@@ -192,9 +197,7 @@ async function runLoopReflectPass(planned, isDistillOnly, env, tally) {
|
|
|
192
197
|
...(improveProfile ? { improveProfile } : {}),
|
|
193
198
|
config: resolvedPlan.config,
|
|
194
199
|
...(primaryStashDir ? { stashDir: primaryStashDir } : {}),
|
|
195
|
-
...(
|
|
196
|
-
? { target: { source: options.sourceName, root: primaryStashDir } }
|
|
197
|
-
: {}),
|
|
200
|
+
...(reflectTarget ? { target: reflectTarget } : {}),
|
|
198
201
|
...(reflectErrors.length > 0 ? { avoidPatterns: [...reflectErrors] } : {}),
|
|
199
202
|
eventSource: "improve",
|
|
200
203
|
// #639 — resolve the low-value filter from the ACTIVE improve profile
|
|
@@ -208,10 +211,73 @@ async function runLoopReflectPass(planned, isDistillOnly, env, tally) {
|
|
|
208
211
|
// the reflect_invoked event and the persisted proposal.
|
|
209
212
|
...(planned.eligibilitySource ? { eligibilitySource: planned.eligibilitySource } : {}),
|
|
210
213
|
};
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
214
|
+
// R9 (tier2-0917): the fingerprint/rejection-backoff guard `createProposal`
|
|
215
|
+
// runs AFTER reflect's ~39s generation + judge is computable from inputs
|
|
216
|
+
// available before dispatch. Check it here first — on a hit, skip the LLM
|
|
217
|
+
// call entirely and synthesize the same "cooldown" envelope reflect.ts's
|
|
218
|
+
// createProposal-skip branch returns, so everything below (mode
|
|
219
|
+
// classification, improve_reflect_outcome, plasticity) is unchanged. This
|
|
220
|
+
// pre-check is an optimisation only — createProposal's post-generation
|
|
221
|
+
// check stays authoritative (see checkProposalGuard's doc comment).
|
|
222
|
+
const guardStash = primaryStashDir ?? options.stashDir;
|
|
223
|
+
const guardSkip = guardStash
|
|
224
|
+
? checkProposalGuard({
|
|
225
|
+
stash: guardStash,
|
|
226
|
+
ref: planned.ref,
|
|
227
|
+
source: "reflect",
|
|
228
|
+
...(reflectTarget ? { target: reflectTarget } : {}),
|
|
229
|
+
...(reflectEngine ? { modelId: reflectEngine } : {}),
|
|
230
|
+
})
|
|
231
|
+
: undefined;
|
|
232
|
+
let reflectResult;
|
|
233
|
+
if (guardSkip) {
|
|
234
|
+
// Mirror reflect.ts's buildReflectEventEmitters().emitInvoked(): the
|
|
235
|
+
// signal-delta cursor (buildLatestProposalTsMap) reads `reflect_invoked`
|
|
236
|
+
// events regardless of outcome, so it must still advance for this ref
|
|
237
|
+
// even though reflectFn was never called.
|
|
238
|
+
appendEvent({
|
|
239
|
+
eventType: "reflect_invoked",
|
|
240
|
+
ref: planned.itemRef ?? durableImproveRef(planned.ref),
|
|
241
|
+
metadata: {
|
|
242
|
+
...(options.task ? { task: options.task } : {}),
|
|
243
|
+
...(reflectEngine ? { engine: reflectEngine } : {}),
|
|
244
|
+
...(planned.eligibilitySource ? { eligibilitySource: planned.eligibilitySource } : {}),
|
|
245
|
+
},
|
|
246
|
+
}, eventsCtx);
|
|
247
|
+
// Mirror reflect.ts's buildReflectEventEmitters().emitFailed(): every
|
|
248
|
+
// reflect_invoked must be paired with a reflect_completed so observers
|
|
249
|
+
// building closed-loop telemetry see balanced invoke/complete pairs.
|
|
250
|
+
// reflectFn is never called on this path, so reflect.ts's own
|
|
251
|
+
// emitFailed (fired from its post-generation cooldown branch) never
|
|
252
|
+
// runs either — this is the pre-generation guard's own pairing.
|
|
253
|
+
appendEvent({
|
|
254
|
+
eventType: "reflect_completed",
|
|
255
|
+
ref: planned.itemRef ?? durableImproveRef(planned.ref),
|
|
256
|
+
metadata: {
|
|
257
|
+
source: "reflect",
|
|
258
|
+
ok: false,
|
|
259
|
+
reason: "cooldown",
|
|
260
|
+
subreason: "pre_generation_guard",
|
|
261
|
+
proposalSkipReason: guardSkip.reason,
|
|
262
|
+
...(guardSkip.existingProposalId ? { existingProposalId: guardSkip.existingProposalId } : {}),
|
|
263
|
+
},
|
|
264
|
+
}, eventsCtx);
|
|
265
|
+
reflectResult = {
|
|
266
|
+
schemaVersion: 2,
|
|
267
|
+
ok: false,
|
|
268
|
+
reason: "cooldown",
|
|
269
|
+
error: `Proposal skipped (${guardSkip.reason}): ${guardSkip.message}`,
|
|
270
|
+
ref: planned.ref,
|
|
271
|
+
...(reflectEngine ? { engine: reflectEngine } : {}),
|
|
272
|
+
exitCode: null,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
else {
|
|
276
|
+
reflectResult = await withLlmStage("reflect", () => reflectFn(reflectCallArgs), {
|
|
277
|
+
engine: reflectEngine,
|
|
278
|
+
process: "reflect",
|
|
279
|
+
});
|
|
280
|
+
}
|
|
215
281
|
const isCooldown = !reflectResult.ok && reflectResult.reason === "cooldown";
|
|
216
282
|
// Content-policy guard hits (reflect size-rail rejections) are NOT
|
|
217
283
|
// LLM faults — the agent responded fine, the downstream guard
|
|
@@ -233,6 +299,13 @@ async function runLoopReflectPass(planned, isDistillOnly, env, tally) {
|
|
|
233
299
|
// fault — so it routes to the `reflect-skipped` bucket and stays
|
|
234
300
|
// out of recentErrors/avoidPatterns.
|
|
235
301
|
const isNoChange = !reflectResult.ok && reflectResult.reason === "no_change";
|
|
302
|
+
// Quality-gate rejection (R3): the judge rejected an otherwise
|
|
303
|
+
// well-parsed proposal. Stays in the `reflect-failed` bucket for
|
|
304
|
+
// metrics continuity, but — like the deterministic skips above —
|
|
305
|
+
// must not be injected into recentErrors/avoidPatterns: the judge's
|
|
306
|
+
// rejection text is not a reusable "avoid this pattern" lesson, and
|
|
307
|
+
// feeding it back in poisoned later prompts in the same run.
|
|
308
|
+
const isQualityRejected = !reflectResult.ok && reflectResult.reason === "quality_rejected";
|
|
236
309
|
tally.actions.push({
|
|
237
310
|
ref: planned.ref,
|
|
238
311
|
mode: reflectResult.ok
|
|
@@ -246,14 +319,15 @@ async function runLoopReflectPass(planned, isDistillOnly, env, tally) {
|
|
|
246
319
|
: "reflect-failed",
|
|
247
320
|
result: reflectResult,
|
|
248
321
|
});
|
|
249
|
-
// Cooldown skips, guard rejects, type-refused skips,
|
|
250
|
-
// skips are not failures — do not
|
|
251
|
-
// (those get injected as
|
|
252
|
-
// prompt). Guard rejects ARE
|
|
253
|
-
//
|
|
254
|
-
// type-refused
|
|
322
|
+
// Cooldown skips, guard rejects, type-refused skips, noise-gate
|
|
323
|
+
// skips, and quality-gate rejections are not failures — do not
|
|
324
|
+
// pollute recentErrors with them (those get injected as
|
|
325
|
+
// `avoidPatterns` into the next reflect prompt). Guard rejects ARE
|
|
326
|
+
// worth showing the LLM as a learn-signal so the next iteration sees
|
|
327
|
+
// "your last expansion was too large"; type-refused, no-change, and
|
|
328
|
+
// quality-rejected are deterministic/judge-side and add no learning
|
|
255
329
|
// signal.
|
|
256
|
-
if (!reflectResult.ok && !isCooldown && !isTypeRefused && !isNoChange) {
|
|
330
|
+
if (!reflectResult.ok && !isCooldown && !isTypeRefused && !isNoChange && !isQualityRejected) {
|
|
257
331
|
const errMsg = reflectResult.error ?? reflectResult.reason ?? "unknown reflect error";
|
|
258
332
|
tally.recentErrorPushes.push({ originator: "reflect", message: errMsg });
|
|
259
333
|
}
|
|
@@ -315,7 +389,7 @@ async function runLoopReflectPass(planned, isDistillOnly, env, tally) {
|
|
|
315
389
|
* that was a `continue` in the old inline loop body is an early `return` here.
|
|
316
390
|
*/
|
|
317
391
|
async function runLoopDistillPass(planned, parsedPlannedRef, isDistillOnly, env, tally) {
|
|
318
|
-
const { options, primaryStashDir, eventsCtx, improveProfile } = env;
|
|
392
|
+
const { options, primaryStashDir, eventsCtx, improveProfile, resolvedPlan } = env;
|
|
319
393
|
const hasRecentFeedbackSignal = env.signalBearingSet.has(planned.ref);
|
|
320
394
|
const explicitRefScope = env.scope.mode === "ref";
|
|
321
395
|
// Profile gate: apply the full type-filter / raw-wiki / disabled rules to
|
|
@@ -405,6 +479,97 @@ async function runLoopDistillPass(planned, parsedPlannedRef, isDistillOnly, env,
|
|
|
405
479
|
return;
|
|
406
480
|
}
|
|
407
481
|
}
|
|
482
|
+
// R9 extension (r2-6, tier2-0917; PRECHECK, tier3-0917): the
|
|
483
|
+
// fingerprint/rejection-backoff guard `createProposal` runs AFTER
|
|
484
|
+
// distill's ~generation + judge is computable from inputs available
|
|
485
|
+
// before dispatch — mirror the reflect pre-check above so a guard hit
|
|
486
|
+
// skips the LLM call entirely. Distill's real `createProposal` call
|
|
487
|
+
// always targets the derived lesson/knowledge ref (`effectiveLessonRef`
|
|
488
|
+
// in distill.ts), never the input ref.
|
|
489
|
+
// Which ref that is: for every non-memory distill-candidate type,
|
|
490
|
+
// `targetKind` defaults to "lesson" (distill.ts ~L882, `invokeDistill
|
|
491
|
+
// AndRecord` above only ever sets `proposalKind: "auto"` for memory
|
|
492
|
+
// refs) and is never overridden to "knowledge", so lessonRef is the
|
|
493
|
+
// ONLY real target. For memory refs (`proposalKind: "auto"`), the
|
|
494
|
+
// target is decided at dispatch by `planMemoryKnowledgePromotion`
|
|
495
|
+
// (knowledgeRef via promotion, lessonRef as fallback) — that decision
|
|
496
|
+
// IS cheap and LLM-free (a deterministic score over the asset content
|
|
497
|
+
// + its feedback history, plus one lookup for an existing knowledge
|
|
498
|
+
// file), so it is pre-checked exactly via `wouldPromoteMemoryToKnowledge`,
|
|
499
|
+
// a thin wrapper that delegates to `planMemoryKnowledgePromotion`
|
|
500
|
+
// itself so this can never drift from distill's real decision. A
|
|
501
|
+
// guard hit on the ref distill would NOT have targeted must never
|
|
502
|
+
// suppress a legitimate dispatch.
|
|
503
|
+
// §23.6 fingerprint model-id term: distill resolves models, not
|
|
504
|
+
// engines (unlike reflect), so this must match `distillRunner?.
|
|
505
|
+
// connection.model` in distill.ts, not the engine name.
|
|
506
|
+
const distillModelId = resolvedPlan.processes.distill.runner?.connection.model;
|
|
507
|
+
let realTargetRef = lessonRef;
|
|
508
|
+
if (parsedPlannedRef.type === "memory") {
|
|
509
|
+
// distill.ts's real dispatch (akmDistill) always derives
|
|
510
|
+
// durableInputRef from options.ref alone (durableImproveRef(inputRef),
|
|
511
|
+
// never itemRef) and reads/scores content via that ref
|
|
512
|
+
// (loadAndScoreInputSalience's `lookup(durableInputRef)`); mirror
|
|
513
|
+
// that here so the pre-check can never read/score a different file
|
|
514
|
+
// than the real dispatch would. itemRef is preferred only for the
|
|
515
|
+
// feedback-events query, matching readDistillFeedback's
|
|
516
|
+
// `ref: options.itemRef ?? durableInputRef`.
|
|
517
|
+
const durableInputRef = durableImproveRef(planned.ref);
|
|
518
|
+
const feedbackRef = planned.itemRef ?? durableInputRef;
|
|
519
|
+
const lookup = (ref) => defaultLookup(ref, dedupeStashDir);
|
|
520
|
+
const filePath = await lookup(durableInputRef);
|
|
521
|
+
const assetContent = filePath && fs.existsSync(filePath) ? fs.readFileSync(filePath, "utf8") : null;
|
|
522
|
+
// PRECHECK (tier3-0917-r3, r3-4): reuse the loop's long-lived
|
|
523
|
+
// eventsCtx.db handle when one is open, instead of opening a fresh
|
|
524
|
+
// read-only state.db connection per memory ref (R25). Degrades to
|
|
525
|
+
// the previous readOnly-open when no live handle is present (e.g.
|
|
526
|
+
// this function invoked without a run-scoped eventsCtx), via the
|
|
527
|
+
// same readOnlyEventsContext helper reflect.ts's read call sites use.
|
|
528
|
+
const { events: feedbackEvents } = readEvents({ ref: feedbackRef, type: "feedback" }, readOnlyEventsContext(eventsCtx));
|
|
529
|
+
const promotesToKnowledge = await wouldPromoteMemoryToKnowledge({
|
|
530
|
+
inputRef: planned.ref,
|
|
531
|
+
durableInputRef,
|
|
532
|
+
assetContent,
|
|
533
|
+
feedbackEvents,
|
|
534
|
+
config: options.config ?? loadConfig(),
|
|
535
|
+
stash: dedupeStashDir,
|
|
536
|
+
lookup,
|
|
537
|
+
});
|
|
538
|
+
if (promotesToKnowledge)
|
|
539
|
+
realTargetRef = knowledgeRef;
|
|
540
|
+
}
|
|
541
|
+
const guardSkip = checkProposalGuard({
|
|
542
|
+
stash: dedupeStashDir,
|
|
543
|
+
ref: realTargetRef,
|
|
544
|
+
source: "distill",
|
|
545
|
+
...(distillModelId ? { modelId: distillModelId } : {}),
|
|
546
|
+
});
|
|
547
|
+
if (guardSkip) {
|
|
548
|
+
tally.actions.push({
|
|
549
|
+
ref: planned.ref,
|
|
550
|
+
mode: "distill-skipped",
|
|
551
|
+
result: { ok: true, reason: guardSkip.reason },
|
|
552
|
+
});
|
|
553
|
+
// Mirror distill.ts's own proposal-skip branch (the post-generation
|
|
554
|
+
// guard `createProposal` hits): emit `distill_invoked` with a
|
|
555
|
+
// `skipped` outcome so the signal-delta cursor
|
|
556
|
+
// (buildLatestProposalTsMap, eligibility.ts) advances for this ref
|
|
557
|
+
// even though distillFn was never called.
|
|
558
|
+
appendEvent({
|
|
559
|
+
eventType: "distill_invoked",
|
|
560
|
+
// Use item_ref when resolved, otherwise the input conceptId —
|
|
561
|
+
// matches distill.ts's own distill_invoked key.
|
|
562
|
+
ref: planned.itemRef ?? durableImproveRef(planned.ref),
|
|
563
|
+
metadata: {
|
|
564
|
+
outcome: "skipped",
|
|
565
|
+
proposalRef: realTargetRef,
|
|
566
|
+
message: guardSkip.message,
|
|
567
|
+
skipReason: guardSkip.reason,
|
|
568
|
+
...(planned.eligibilitySource ? { eligibilitySource: planned.eligibilitySource } : {}),
|
|
569
|
+
},
|
|
570
|
+
}, eventsCtx);
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
408
573
|
}
|
|
409
574
|
await invokeDistillAndRecord(planned, parsedPlannedRef, env, tally);
|
|
410
575
|
}
|
|
@@ -423,8 +588,7 @@ async function runLoopDistillPass(planned, parsedPlannedRef, isDistillOnly, env,
|
|
|
423
588
|
}
|
|
424
589
|
/**
|
|
425
590
|
* The distill invocation for one ref that passed every gate: the `distillFn`
|
|
426
|
-
* call, memory-inference queueing, plasticity counters
|
|
427
|
-
* quality-rejected / proposal-rejected eval-case writes.
|
|
591
|
+
* call, memory-inference queueing, and plasticity counters.
|
|
428
592
|
*/
|
|
429
593
|
async function invokeDistillAndRecord(planned, parsedPlannedRef, env, tally) {
|
|
430
594
|
const { options, primaryStashDir, distillFn, eventsCtx, improveProfile, resolvedPlan, budgetSignal } = env;
|
|
@@ -470,30 +634,6 @@ async function invokeDistillAndRecord(planned, parsedPlannedRef, env, tally) {
|
|
|
470
634
|
// best-effort: plasticity counter failure never blocks the run
|
|
471
635
|
}
|
|
472
636
|
}
|
|
473
|
-
if (distillResult.outcome === "quality_rejected" && primaryStashDir) {
|
|
474
|
-
const slug = refSlug(planned.ref);
|
|
475
|
-
writeEvalCase(primaryStashDir, {
|
|
476
|
-
ref: planned.ref,
|
|
477
|
-
failureReason: distillResult.reason ?? "quality gate rejected",
|
|
478
|
-
assetType: parseRefInput(planned.ref).type ?? "unknown",
|
|
479
|
-
rejectedAt: Date.now(),
|
|
480
|
-
source: "distill_quality_rejected",
|
|
481
|
-
slug: `${slug}-${Date.now()}`,
|
|
482
|
-
});
|
|
483
|
-
}
|
|
484
|
-
// D6: use pre-loaded map instead of per-iteration DB query
|
|
485
|
-
const rejectedProposalEvent = env.rejectedProposalsByRef.get(planned.ref);
|
|
486
|
-
if (rejectedProposalEvent && primaryStashDir) {
|
|
487
|
-
const slug = refSlug(planned.ref);
|
|
488
|
-
writeEvalCase(primaryStashDir, {
|
|
489
|
-
ref: planned.ref,
|
|
490
|
-
failureReason: rejectedProposalEvent.metadata?.reason ?? "proposal rejected",
|
|
491
|
-
assetType: parseRefInput(planned.ref).type ?? "unknown",
|
|
492
|
-
rejectedAt: new Date(rejectedProposalEvent.ts).getTime(),
|
|
493
|
-
source: "proposal_rejected",
|
|
494
|
-
slug: `${slug}-rejected`,
|
|
495
|
-
});
|
|
496
|
-
}
|
|
497
637
|
}
|
|
498
638
|
/**
|
|
499
639
|
* Wall-clock budget exhausted mid-loop (O-1 / #364): emit the improve_skipped
|
|
@@ -552,7 +692,7 @@ export async function runImproveLoopStage(args) {
|
|
|
552
692
|
return { reflectsWithErrorContext, memoryRefsForInference };
|
|
553
693
|
}
|
|
554
694
|
export async function runImprovePostLoopStage(args) {
|
|
555
|
-
const { scope, options, primaryStashDir, actionableRefs, appliedCleanup, cleanupWarnings, memoryRefsForInference,
|
|
695
|
+
const { scope, options, primaryStashDir, actionableRefs, appliedCleanup, cleanupWarnings, memoryRefsForInference, eventsCtx, budgetSignal, improveProfile, resolvedPlan, consolidationRan, } = args;
|
|
556
696
|
const allWarnings = [...cleanupWarnings, ...(appliedCleanup?.warnings ?? [])];
|
|
557
697
|
info("[improve] post-loop maintenance starting");
|
|
558
698
|
const maintenanceResult = await runImproveMaintenancePasses({
|
|
@@ -561,8 +701,6 @@ export async function runImprovePostLoopStage(args) {
|
|
|
561
701
|
actionableRefs,
|
|
562
702
|
memoryRefsForInference,
|
|
563
703
|
allWarnings,
|
|
564
|
-
reindexFn,
|
|
565
|
-
consolidationRan,
|
|
566
704
|
// O-1 (#364): forward the budget signal to memory inference + graph extraction.
|
|
567
705
|
budgetSignal,
|
|
568
706
|
eventsCtx,
|
|
@@ -617,8 +755,7 @@ export async function runImprovePostLoopStage(args) {
|
|
|
617
755
|
}
|
|
618
756
|
}
|
|
619
757
|
// ── R5: collapse/churn detector ────────────────────────────────────────────
|
|
620
|
-
// One snapshot per QUALIFYING cycle: consolidate processed work.
|
|
621
|
-
// the maintenance reindex so FTS sees the post-merge index. Deterministic,
|
|
758
|
+
// One snapshot per QUALIFYING cycle: consolidate processed work. Deterministic,
|
|
622
759
|
// observe-only, fail-open (the orchestrator catches everything) — and inert
|
|
623
760
|
// on the ~9-in-10 default-profile runs that touch no merges.
|
|
624
761
|
let cycleMetrics;
|
|
@@ -651,7 +788,7 @@ export async function runImprovePostLoopStage(args) {
|
|
|
651
788
|
// Exported for tests (#584/#585 DB-locking regression coverage); production
|
|
652
789
|
// callers reach it only through akmImprove → runImprovePostLoopStage.
|
|
653
790
|
export async function runImproveMaintenancePasses(args) {
|
|
654
|
-
const { options, primaryStashDir, memoryRefsForInference, allWarnings,
|
|
791
|
+
const { options, primaryStashDir, memoryRefsForInference, allWarnings, budgetSignal, eventsCtx } = args;
|
|
655
792
|
if (!primaryStashDir)
|
|
656
793
|
return { memoryInferenceDurationMs: 0, graphExtractionDurationMs: 0 };
|
|
657
794
|
if (budgetSignal?.aborted)
|
|
@@ -662,21 +799,6 @@ export async function runImproveMaintenancePasses(args) {
|
|
|
662
799
|
const graphExtractionFn = options.graphExtractionFn ?? runGraphExtractionPass;
|
|
663
800
|
const openIndexDb = () => openIndexDatabase(getDbPath(), config.embedding?.dimension ? { embeddingDim: config.embedding.dimension } : undefined);
|
|
664
801
|
const dbCell = {};
|
|
665
|
-
// #584: see the MaintenanceCtx.reindexWithIndexDbReleased doc — close before
|
|
666
|
-
// every reindex, reopen in `finally` so a failed reindex still leaves a
|
|
667
|
-
// usable handle in the cell.
|
|
668
|
-
const reindexWithIndexDbReleased = async (stashDir) => {
|
|
669
|
-
if (dbCell.current) {
|
|
670
|
-
closeDatabase(dbCell.current);
|
|
671
|
-
dbCell.current = undefined;
|
|
672
|
-
}
|
|
673
|
-
try {
|
|
674
|
-
await reindexFn({ stashDir, signal: budgetSignal });
|
|
675
|
-
}
|
|
676
|
-
finally {
|
|
677
|
-
dbCell.current = openIndexDb();
|
|
678
|
-
}
|
|
679
|
-
};
|
|
680
802
|
const ctx = {
|
|
681
803
|
config,
|
|
682
804
|
sources,
|
|
@@ -687,12 +809,10 @@ export async function runImproveMaintenancePasses(args) {
|
|
|
687
809
|
resolvedPlan: args.resolvedPlan,
|
|
688
810
|
memoryInferenceFn,
|
|
689
811
|
graphExtractionFn,
|
|
690
|
-
reindexWithIndexDbReleased,
|
|
691
812
|
};
|
|
692
813
|
const collected = await runMaintenancePassesUnderLease(ctx, dbCell, {
|
|
693
814
|
actionableRefs: args.actionableRefs,
|
|
694
815
|
memoryRefsForInference,
|
|
695
|
-
consolidationRan: args.consolidationRan,
|
|
696
816
|
allWarnings,
|
|
697
817
|
openIndexDb,
|
|
698
818
|
});
|
|
@@ -717,7 +837,6 @@ export async function runImproveMaintenancePasses(args) {
|
|
|
717
837
|
async function runMaintenancePassesUnderLease(ctx, dbCell, args) {
|
|
718
838
|
const { allWarnings } = args;
|
|
719
839
|
const actions = [];
|
|
720
|
-
let reindexedAfterInference = false;
|
|
721
840
|
try {
|
|
722
841
|
dbCell.current = args.openIndexDb();
|
|
723
842
|
const inference = await runMemoryInferenceMaintenancePass(ctx, dbCell, args.memoryRefsForInference);
|
|
@@ -725,22 +844,34 @@ async function runMaintenancePassesUnderLease(ctx, dbCell, args) {
|
|
|
725
844
|
actions.push(inference.action);
|
|
726
845
|
allWarnings.push(...inference.warnings);
|
|
727
846
|
const memoryInference = inference.memoryInference;
|
|
728
|
-
|
|
729
|
-
|
|
847
|
+
// R78 (tier1-0917): index exactly the files memory inference wrote (derived children
|
|
848
|
+
// + rewritten parents) instead of a full reindex — typically one written
|
|
849
|
+
// fact per run, which used to pay a full-corpus reindex regardless.
|
|
850
|
+
if (memoryInference && memoryInference.writtenPaths.length > 0) {
|
|
851
|
+
info(`[improve] indexing ${memoryInference.writtenPaths.length} file(s) written by memory inference`);
|
|
730
852
|
try {
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
853
|
+
// #584: indexWrittenAssets opens its own write handle on the same
|
|
854
|
+
// index.db WAL file, so the maintenance handle must be closed first
|
|
855
|
+
// and a fresh one reopened after, even on failure.
|
|
856
|
+
if (dbCell.current) {
|
|
857
|
+
closeDatabase(dbCell.current);
|
|
858
|
+
dbCell.current = undefined;
|
|
859
|
+
}
|
|
860
|
+
try {
|
|
861
|
+
await indexWrittenAssets(ctx.primaryStashDir, memoryInference.writtenPaths);
|
|
862
|
+
}
|
|
863
|
+
finally {
|
|
864
|
+
dbCell.current = args.openIndexDb();
|
|
865
|
+
}
|
|
866
|
+
info("[improve] indexing after memory inference complete");
|
|
734
867
|
}
|
|
735
868
|
catch (err) {
|
|
736
|
-
allWarnings.push(`
|
|
869
|
+
allWarnings.push(`indexing after memory inference failed: ${errMessage(err)}`);
|
|
737
870
|
}
|
|
738
871
|
}
|
|
739
872
|
const graph = await runGraphExtractionMaintenancePass(ctx, dbCell, {
|
|
740
873
|
actionableRefs: args.actionableRefs,
|
|
741
874
|
memoryRefsForInference: args.memoryRefsForInference,
|
|
742
|
-
consolidationRan: args.consolidationRan,
|
|
743
|
-
reindexedAfterInference,
|
|
744
875
|
});
|
|
745
876
|
if (graph.action)
|
|
746
877
|
actions.push(graph.action);
|
|
@@ -868,7 +999,6 @@ export async function runGraphExtractionMaintenancePass(ctx, dbCell, args) {
|
|
|
868
999
|
let graphExtraction;
|
|
869
1000
|
let durationMs = 0;
|
|
870
1001
|
let action;
|
|
871
|
-
let reindexedAfterInference = args.reindexedAfterInference;
|
|
872
1002
|
const graphEnabled = resolvedPlan ? true : isProcessEnabled("index", "graph_extraction", config);
|
|
873
1003
|
const graphExtractionDisabledByProfile = improveProfile?.processes?.graphExtraction?.enabled === false;
|
|
874
1004
|
const graphExtractionFullScan = improveProfile?.processes?.graphExtraction?.fullScan === true;
|
|
@@ -879,6 +1009,7 @@ export async function runGraphExtractionMaintenancePass(ctx, dbCell, args) {
|
|
|
879
1009
|
...DEFAULT_GRAPH_EXTRACTION_INCLUDE_TYPES,
|
|
880
1010
|
];
|
|
881
1011
|
const graphExtractionBatchSize = improveProfile?.processes?.graphExtraction?.batchSize ?? DEFAULT_GRAPH_EXTRACTION_BATCH_SIZE;
|
|
1012
|
+
const graphExtractionMaxChunksPerAsset = improveProfile?.processes?.graphExtraction?.maxChunksPerAsset;
|
|
882
1013
|
// Build the set of refs actually touched this run.
|
|
883
1014
|
const touchedRefs = new Set();
|
|
884
1015
|
for (const r of args.actionableRefs)
|
|
@@ -892,21 +1023,6 @@ export async function runGraphExtractionMaintenancePass(ctx, dbCell, args) {
|
|
|
892
1023
|
info(`[improve] graph extraction starting${graphExtractionFullScan ? " (full-corpus scan)" : ""}`);
|
|
893
1024
|
const extractionStart = Date.now();
|
|
894
1025
|
try {
|
|
895
|
-
// D9: if consolidation ran but memory inference did not reindex, force a reindex
|
|
896
|
-
// so graph extraction sees current DB state after consolidation writes.
|
|
897
|
-
if (args.consolidationRan && !reindexedAfterInference) {
|
|
898
|
-
info("[improve] reindexing after consolidation (graph extraction needs current state)");
|
|
899
|
-
try {
|
|
900
|
-
await ctx.reindexWithIndexDbReleased(primaryStashDir);
|
|
901
|
-
reindexedAfterInference = true;
|
|
902
|
-
info("[improve] reindex after consolidation complete");
|
|
903
|
-
}
|
|
904
|
-
catch (err) {
|
|
905
|
-
warnings.push(`reindex after consolidation failed: ${errMessage(err)}`);
|
|
906
|
-
}
|
|
907
|
-
}
|
|
908
|
-
// #584: no close/reopen needed here — reindexWithIndexDbReleased
|
|
909
|
-
// already swapped in a fresh post-reindex handle.
|
|
910
1026
|
// Resolve touched refs to absolute file paths. Skipped for fullScan
|
|
911
1027
|
// (candidatePaths stays undefined → extractor processes all files).
|
|
912
1028
|
let candidatePaths;
|
|
@@ -943,6 +1059,9 @@ export async function runGraphExtractionMaintenancePass(ctx, dbCell, args) {
|
|
|
943
1059
|
includeTypes: graphExtractionIncludeTypes,
|
|
944
1060
|
batchSize: graphExtractionBatchSize,
|
|
945
1061
|
...(graphExtractionTopN != null ? { topN: graphExtractionTopN } : {}),
|
|
1062
|
+
...(graphExtractionMaxChunksPerAsset != null
|
|
1063
|
+
? { maxChunksPerAsset: graphExtractionMaxChunksPerAsset }
|
|
1064
|
+
: {}),
|
|
946
1065
|
},
|
|
947
1066
|
}), { engine: resolvedPlan?.processes.graphExtraction.runner?.engine, process: "graphExtraction" });
|
|
948
1067
|
durationMs = Date.now() - extractionStart;
|
|
@@ -1089,6 +1208,13 @@ export function runRetentionPurgePass(ctx) {
|
|
|
1089
1208
|
metadata: { purgedCount: cycleMetricsPurged, retentionDays: cycleRetention },
|
|
1090
1209
|
}, eventsCtx);
|
|
1091
1210
|
}
|
|
1211
|
+
// R0 step 3: opportunistic post-purge VACUUM. Reads the freelist
|
|
1212
|
+
// off this same connection (no second state.db handle) and only
|
|
1213
|
+
// runs when reclaimable space crosses STATE_DB_FREELIST_WARN_RATIO.
|
|
1214
|
+
const vacuumOutcome = vacuumStateDbIfReclaimable(stateDb, readFreelistInfo(stateDb), eventsCtx);
|
|
1215
|
+
if (vacuumOutcome.ran) {
|
|
1216
|
+
info(`[improve] state.db vacuum: ${vacuumOutcome.pagesBefore} -> ${vacuumOutcome.pagesAfter} pages`);
|
|
1217
|
+
}
|
|
1092
1218
|
}, { path: eventsCtx?.dbPath, borrowed: eventsCtx?.db });
|
|
1093
1219
|
}
|
|
1094
1220
|
catch (err) {
|
|
@@ -1165,12 +1291,13 @@ export const STATE_GC_GRACE_MS = daysToMs(7);
|
|
|
1165
1291
|
* Resolve one state-table's stored `asset_ref` against the live index.
|
|
1166
1292
|
*
|
|
1167
1293
|
* "ref not present in entries.item_ref" is the authoritative-deletion
|
|
1168
|
-
* predicate (see the pass doc comment below), so this
|
|
1169
|
-
*
|
|
1170
|
-
*
|
|
1171
|
-
*
|
|
1172
|
-
* `
|
|
1173
|
-
*
|
|
1294
|
+
* predicate (see the pass doc comment below), so this checks the same two
|
|
1295
|
+
* spellings `getEntryByRef` (index-entries-repository.ts) resolves — an exact
|
|
1296
|
+
* bundle-qualified item_ref, or a bare conceptId matched by suffix across all
|
|
1297
|
+
* bundles — but against a prebuilt {@link LiveRefSnapshot}
|
|
1298
|
+
* (`getLiveRefSnapshot`) instead of a database round trip per row: with up to
|
|
1299
|
+
* a few thousand pending rows per run, one probe per row was the dominant
|
|
1300
|
+
* cost R78 (tier1-0917).
|
|
1174
1301
|
*
|
|
1175
1302
|
* On top of that, falls back to the BARE conceptId form (`bareImproveRef` —
|
|
1176
1303
|
* the same primitive `preparation.ts`'s `normalizeStoredKey` map is built
|
|
@@ -1182,11 +1309,11 @@ export const STATE_GC_GRACE_MS = daysToMs(7);
|
|
|
1182
1309
|
* "never delete a live row" over "never miss a genuinely dead one" mirrors
|
|
1183
1310
|
* `getEntryByRef`'s own bare-conceptId suffix-match trade-off.
|
|
1184
1311
|
*/
|
|
1185
|
-
function isStateRefLive(
|
|
1186
|
-
if (
|
|
1312
|
+
function isStateRefLive(snapshot, storedRef) {
|
|
1313
|
+
if (isRefLiveInSnapshot(snapshot, storedRef))
|
|
1187
1314
|
return true;
|
|
1188
1315
|
const bare = bareImproveRef(storedRef);
|
|
1189
|
-
return bare !== storedRef &&
|
|
1316
|
+
return bare !== storedRef && isRefLiveInSnapshot(snapshot, bare);
|
|
1190
1317
|
}
|
|
1191
1318
|
/**
|
|
1192
1319
|
* Sweep ONE state table: stamp refs that just went unresolved, clear refs
|
|
@@ -1198,11 +1325,11 @@ function isStateRefLive(indexDb, storedRef) {
|
|
|
1198
1325
|
* proof" (close-out plan, Workstream C).
|
|
1199
1326
|
*/
|
|
1200
1327
|
function gcOneStateTable(args) {
|
|
1201
|
-
const { refRows,
|
|
1328
|
+
const { refRows, liveRefs, now, collect, stamp, clear, deleteOlderThan, countPending } = args;
|
|
1202
1329
|
const toStamp = [];
|
|
1203
1330
|
const toClear = [];
|
|
1204
1331
|
for (const row of refRows) {
|
|
1205
|
-
const live = isStateRefLive(
|
|
1332
|
+
const live = isStateRefLive(liveRefs, row.asset_ref);
|
|
1206
1333
|
if (!live && row.missing_since == null)
|
|
1207
1334
|
toStamp.push(row.asset_ref);
|
|
1208
1335
|
else if (live && row.missing_since != null)
|
|
@@ -1257,10 +1384,16 @@ export function runOrphanStateGcPass(ctx, dbCell) {
|
|
|
1257
1384
|
let pending = 0;
|
|
1258
1385
|
let collected = 0;
|
|
1259
1386
|
try {
|
|
1387
|
+
// R78 (tier1-0917): one query for every live item_ref, shared by both tables' sweeps
|
|
1388
|
+
// below — replaces a `getEntryByRef` round trip per pending row. Inside
|
|
1389
|
+
// the try so a schema mismatch (e.g. a DB version upgrade that dropped
|
|
1390
|
+
// `entries`) degrades to the "orphan state GC failed" warning below
|
|
1391
|
+
// instead of escaping this pass and failing the whole maintenance run.
|
|
1392
|
+
const liveRefs = getLiveRefSnapshot(indexDb);
|
|
1260
1393
|
withStateDb((stateDb) => {
|
|
1261
1394
|
const salienceResult = gcOneStateTable({
|
|
1262
1395
|
refRows: listAssetSalienceMissingState(stateDb),
|
|
1263
|
-
|
|
1396
|
+
liveRefs,
|
|
1264
1397
|
now,
|
|
1265
1398
|
collect,
|
|
1266
1399
|
stamp: (refs, ts) => stampAssetSalienceMissing(stateDb, refs, ts),
|
|
@@ -1270,7 +1403,7 @@ export function runOrphanStateGcPass(ctx, dbCell) {
|
|
|
1270
1403
|
});
|
|
1271
1404
|
const outcomeResult = gcOneStateTable({
|
|
1272
1405
|
refRows: listAssetOutcomeMissingState(stateDb),
|
|
1273
|
-
|
|
1406
|
+
liveRefs,
|
|
1274
1407
|
now,
|
|
1275
1408
|
collect,
|
|
1276
1409
|
stamp: (refs, ts) => stampAssetOutcomeMissing(stateDb, refs, ts),
|