akm-cli 0.9.0-beta.9 → 0.9.0-rc.0
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 +592 -0
- package/README.md +12 -4
- package/dist/akm +38 -0
- package/dist/akm-migrate-storage +38 -0
- package/dist/assets/help/help-improve.md +9 -6
- package/dist/assets/hints/cli-hints-full.md +6 -5
- package/dist/assets/profiles/default.json +9 -4
- package/dist/assets/profiles/frequent.json +1 -1
- package/dist/assets/profiles/memory-focus.json +1 -1
- package/dist/assets/profiles/proactive-maintenance.json +25 -0
- package/dist/assets/profiles/quick.json +1 -1
- package/dist/assets/profiles/recombine-only.json +21 -0
- package/dist/assets/profiles/reflect-distill.json +30 -0
- package/dist/assets/profiles/synthesize.json +15 -0
- package/dist/assets/profiles/thorough.json +1 -1
- 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 +11 -3
- package/dist/assets/prompts/graph-extract-system.md +1 -0
- package/dist/assets/prompts/graph-extract-user-prompt.md +1 -1
- 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/stash-skeleton/facts/conventions/assets/agent.md +38 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/command.md +38 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/fact.md +39 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/knowledge.md +40 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/lesson.md +43 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/memory.md +38 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/script.md +43 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/skill.md +40 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/workflow.md +43 -0
- package/dist/assets/templates/html/health.html +281 -111
- package/dist/assets/wiki/ingest-workflow-template.md +45 -16
- package/dist/assets/wiki/schema-template.md +4 -4
- package/dist/cli/clack.js +56 -0
- package/dist/cli/config-migrate.js +7 -1
- package/dist/cli/confirm.js +1 -1
- package/dist/cli/parse-args.js +46 -1
- package/dist/cli/shared.js +28 -0
- package/dist/cli.js +25 -21
- package/dist/commands/agent/agent-dispatch.js +3 -2
- package/dist/commands/agent/agent-support.js +0 -7
- package/dist/commands/agent/contribute-cli.js +26 -7
- package/dist/commands/config-cli.js +26 -13
- package/dist/commands/env/child-env.js +47 -0
- package/dist/commands/env/env-cli.js +220 -227
- package/dist/commands/env/env.js +14 -67
- package/dist/commands/env/secret-cli.js +140 -138
- package/dist/commands/feedback-cli.js +156 -155
- package/dist/commands/graph/graph-cli.js +5 -13
- package/dist/commands/graph/graph.js +3 -3
- package/dist/commands/health/advisories.js +151 -0
- package/dist/commands/health/checks.js +103 -16
- package/dist/commands/health/html-report.js +447 -81
- package/dist/commands/health/improve-metrics.js +771 -0
- package/dist/commands/health/llm-usage.js +65 -0
- package/dist/commands/health/md-report.js +103 -0
- package/dist/commands/health/metrics.js +278 -0
- package/dist/commands/health/stash-exposure.js +46 -0
- package/dist/commands/health/surfaces.js +216 -0
- package/dist/commands/health/task-runs.js +135 -0
- package/dist/commands/health/types.js +26 -0
- package/dist/commands/health/windows.js +195 -0
- package/dist/commands/health.js +91 -1091
- package/dist/commands/improve/anti-collapse.js +170 -0
- package/dist/commands/improve/calibration.js +161 -0
- package/dist/commands/improve/collapse-detector.js +421 -0
- package/dist/commands/improve/consolidate/chunking.js +141 -0
- package/dist/commands/improve/consolidate/eligibility.js +64 -0
- package/dist/commands/improve/consolidate/merge.js +145 -0
- package/dist/commands/improve/consolidate/sanitize.js +231 -0
- package/dist/commands/{lint.js → improve/consolidate/types.js} +1 -1
- package/dist/commands/improve/consolidate.js +1295 -1277
- package/dist/commands/improve/dedup.js +482 -0
- package/dist/commands/improve/distill/content-repair.js +202 -0
- package/dist/commands/improve/distill/promote-memory.js +229 -0
- package/dist/commands/improve/distill/quality-gate.js +236 -0
- package/dist/commands/improve/distill-guards.js +127 -0
- package/dist/commands/improve/distill-promotion-policy.js +826 -167
- package/dist/commands/improve/distill.js +228 -605
- package/dist/commands/improve/eligibility.js +434 -0
- package/dist/commands/improve/encoding-salience.js +205 -0
- package/dist/commands/improve/extract-cli.js +179 -59
- package/dist/commands/improve/extract-prompt.js +54 -3
- package/dist/commands/improve/extract-watch.js +140 -0
- package/dist/commands/improve/extract.js +409 -43
- package/dist/commands/improve/feedback-valence.js +54 -0
- package/dist/commands/improve/hot-probation.js +45 -0
- package/dist/commands/improve/improve-auto-accept.js +157 -10
- package/dist/commands/improve/improve-cli.js +115 -73
- package/dist/commands/improve/improve-profiles.js +28 -8
- package/dist/commands/improve/improve-result-file.js +15 -25
- package/dist/commands/improve/improve-session.js +58 -0
- package/dist/commands/improve/improve.js +485 -2764
- package/dist/commands/improve/locks.js +154 -0
- package/dist/commands/improve/loop-stages.js +1100 -0
- package/dist/commands/improve/memory/memory-belief.js +14 -15
- package/dist/commands/improve/memory/memory-contradiction-detect.js +83 -60
- package/dist/commands/improve/memory/memory-improve.js +27 -27
- package/dist/commands/improve/outcome-loop.js +270 -0
- package/dist/commands/improve/preparation.js +2002 -0
- package/dist/commands/improve/proactive-maintenance.js +37 -35
- package/dist/commands/improve/procedural.js +398 -0
- package/dist/commands/improve/recombine.js +818 -0
- package/dist/commands/improve/reflect-noise.js +0 -0
- package/dist/commands/improve/reflect.js +206 -45
- package/dist/commands/improve/salience.js +455 -0
- package/dist/commands/improve/schema-similarity-gate.js +168 -0
- package/dist/commands/improve/shared.js +51 -0
- package/dist/commands/improve/triage.js +93 -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 +38 -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/observability-cli.js +4 -4
- package/dist/commands/proposal/drain-policies.js +13 -4
- package/dist/commands/proposal/drain.js +45 -51
- package/dist/commands/proposal/legacy-import.js +115 -0
- package/dist/commands/proposal/proposal-cli.js +24 -34
- package/dist/commands/proposal/proposal.js +2 -1
- package/dist/commands/proposal/propose.js +8 -3
- package/dist/commands/proposal/repository.js +829 -0
- package/dist/commands/proposal/validators/proposal-quality-validators.js +9 -8
- package/dist/commands/proposal/validators/proposals.js +93 -895
- package/dist/commands/read/curate.js +410 -111
- package/dist/commands/read/knowledge.js +10 -3
- package/dist/commands/read/remember-cli.js +133 -138
- package/dist/commands/read/search-cli.js +15 -8
- package/dist/commands/read/search.js +22 -11
- package/dist/commands/read/show.js +106 -14
- package/dist/commands/registry-cli.js +76 -87
- package/dist/commands/remember.js +11 -12
- package/dist/commands/sources/add-cli.js +91 -95
- package/dist/commands/sources/history.js +1 -1
- package/dist/commands/sources/init.js +66 -18
- package/dist/commands/sources/installed-stashes.js +11 -3
- package/dist/commands/sources/schema-repair.js +44 -46
- package/dist/commands/sources/self-update.js +2 -2
- package/dist/commands/sources/source-add.js +7 -3
- package/dist/commands/sources/sources-cli.js +3 -3
- package/dist/commands/sources/stash-cli.js +19 -39
- package/dist/commands/sources/stash-skeleton.js +57 -8
- package/dist/commands/tasks/default-tasks.js +15 -2
- package/dist/commands/tasks/tasks-cli.js +20 -29
- package/dist/commands/tasks/tasks.js +39 -11
- package/dist/commands/wiki-cli.js +23 -38
- package/dist/commands/workflow-cli.js +15 -1
- package/dist/core/asset/asset-registry.js +3 -1
- package/dist/core/asset/asset-spec.js +21 -4
- package/dist/core/asset/frontmatter.js +188 -167
- package/dist/core/asset/markdown.js +8 -0
- package/dist/core/authoring-rules.js +92 -0
- package/dist/core/common.js +4 -23
- package/dist/core/concurrent.js +10 -1
- package/dist/core/config/config-io.js +10 -1
- package/dist/core/config/config-migration.js +18 -40
- package/dist/core/config/config-schema.js +382 -62
- package/dist/core/config/config-types.js +3 -3
- package/dist/core/config/config.js +67 -22
- package/dist/core/deep-merge.js +38 -0
- package/dist/core/errors.js +1 -0
- package/dist/core/eval/rank-metrics.js +113 -0
- package/dist/core/events.js +4 -7
- package/dist/core/improve-types.js +47 -8
- package/dist/core/logs-db.js +14 -75
- package/dist/core/parse.js +36 -16
- package/dist/core/paths.js +18 -18
- package/dist/core/standards/resolve-standards-context.js +87 -0
- package/dist/core/standards/resolve-stash-standards.js +99 -0
- package/dist/core/standards/resolve-type-conventions.js +66 -0
- package/dist/core/state/migrations.js +770 -0
- package/dist/core/state-db.js +132 -1126
- package/dist/core/structured.js +69 -0
- package/dist/core/time.js +53 -0
- package/dist/core/warn.js +21 -0
- package/dist/core/write-source.js +37 -0
- package/dist/indexer/db/db.js +259 -769
- package/dist/indexer/db/entry-mapper.js +41 -0
- package/dist/indexer/db/graph-db.js +129 -86
- package/dist/indexer/db/llm-cache.js +2 -2
- package/dist/indexer/db/schema.js +516 -0
- package/dist/indexer/ensure-index.js +36 -92
- package/dist/indexer/feedback/utility-policy.js +75 -0
- package/dist/indexer/graph/graph-boost.js +51 -41
- package/dist/indexer/graph/graph-extraction.js +207 -4
- package/dist/indexer/index-writer-lock.js +18 -11
- package/dist/indexer/index-written-assets.js +105 -0
- package/dist/indexer/indexer.js +182 -204
- package/dist/indexer/passes/dir-staleness.js +114 -0
- package/dist/indexer/passes/memory-inference.js +13 -5
- package/dist/indexer/passes/metadata.js +20 -0
- package/dist/indexer/read-preflight.js +23 -0
- package/dist/indexer/search/db-search.js +89 -13
- package/dist/indexer/search/fts-query.js +51 -0
- package/dist/indexer/search/ranking-contributors.js +95 -9
- package/dist/indexer/search/ranking.js +79 -3
- package/dist/indexer/search/search-fields.js +6 -0
- package/dist/indexer/search/search-source.js +32 -21
- package/dist/indexer/search/semantic-status.js +4 -0
- package/dist/indexer/walk/matchers.js +9 -0
- package/dist/indexer/walk/walker.js +21 -13
- package/dist/integrations/agent/builders.js +39 -13
- package/dist/integrations/agent/config.js +20 -59
- package/dist/integrations/agent/detect.js +9 -0
- package/dist/integrations/agent/index.js +3 -19
- package/dist/integrations/agent/model-aliases.js +7 -2
- package/dist/integrations/agent/profiles.js +7 -1
- package/dist/integrations/agent/prompts.js +75 -9
- package/dist/integrations/agent/runner-dispatch.js +59 -0
- package/dist/integrations/agent/runner.js +13 -9
- package/dist/integrations/agent/spawn.js +69 -67
- package/dist/integrations/harnesses/claude/agent-builder.js +1 -1
- package/dist/integrations/harnesses/claude/index.js +2 -0
- package/dist/integrations/harnesses/claude/session-log.js +10 -0
- package/dist/integrations/harnesses/index.js +2 -3
- package/dist/integrations/harnesses/opencode/agent-builder.js +1 -1
- package/dist/integrations/harnesses/opencode/index.js +2 -0
- package/dist/integrations/harnesses/opencode/session-log.js +173 -3
- package/dist/integrations/harnesses/opencode-sdk/index.js +2 -2
- package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +98 -17
- package/dist/integrations/harnesses/types.js +1 -0
- package/dist/integrations/session-logs/index.js +16 -0
- package/dist/llm/call-ai.js +2 -2
- package/dist/llm/client.js +34 -11
- package/dist/llm/embedder.js +67 -4
- package/dist/llm/embedders/cache.js +3 -1
- package/dist/llm/embedders/deterministic.js +66 -0
- package/dist/llm/embedders/local.js +73 -3
- package/dist/llm/feature-gate.js +16 -15
- package/dist/llm/graph-extract.js +67 -44
- package/dist/llm/memory-infer-impl.js +138 -0
- package/dist/llm/memory-infer.js +1 -127
- package/dist/llm/metadata-enhance.js +44 -31
- package/dist/llm/structured-call.js +49 -0
- package/dist/migrate-storage-node.mjs +8 -0
- package/dist/output/context.js +5 -5
- package/dist/output/renderers.js +85 -14
- package/dist/output/shapes/curate.js +14 -2
- package/dist/output/shapes/helpers.js +0 -3
- package/dist/output/shapes/passthrough.js +2 -1
- package/dist/output/text/helpers.js +29 -1
- package/dist/output/text/workflow.js +1 -0
- package/dist/registry/providers/skills-sh.js +21 -147
- package/dist/registry/providers/static-index.js +15 -157
- package/dist/registry/resolve.js +27 -9
- package/dist/runtime.js +25 -1
- package/dist/scripts/migrate-storage.js +2661 -2369
- package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +883 -596
- package/dist/setup/detect.js +9 -0
- package/dist/setup/legacy-config.js +106 -0
- package/dist/setup/prompt.js +57 -0
- package/dist/setup/providers.js +14 -0
- package/dist/setup/registry-stash-loader.js +12 -0
- package/dist/setup/semantic-assets.js +124 -0
- package/dist/setup/setup.js +52 -1614
- package/dist/setup/steps/connection.js +734 -0
- package/dist/setup/steps/output.js +31 -0
- package/dist/setup/steps/platforms.js +124 -0
- package/dist/setup/steps/semantic.js +27 -0
- package/dist/setup/steps/sources.js +222 -0
- package/dist/setup/steps/stashdir.js +42 -0
- package/dist/setup/steps/tasks.js +152 -0
- package/dist/sources/include.js +6 -2
- package/dist/sources/providers/filesystem.js +0 -1
- package/dist/sources/providers/git-install.js +210 -0
- package/dist/sources/providers/git-provider.js +234 -0
- package/dist/sources/providers/git-stash.js +248 -0
- package/dist/sources/providers/git.js +10 -661
- package/dist/sources/providers/npm.js +2 -6
- package/dist/sources/providers/provider-utils.js +13 -7
- package/dist/sources/providers/sync-from-ref.js +9 -1
- package/dist/sources/providers/website.js +9 -5
- package/dist/sources/website-ingest.js +187 -29
- package/dist/sources/wiki-fetchers/registry.js +53 -0
- package/dist/sources/wiki-fetchers/youtube.js +239 -0
- package/dist/storage/database.js +45 -10
- package/dist/storage/managed-db.js +82 -0
- package/dist/storage/repositories/canaries-repository.js +107 -0
- package/dist/storage/repositories/consolidation-repository.js +38 -0
- package/dist/storage/repositories/embeddings-repository.js +72 -0
- package/dist/storage/repositories/events-repository.js +187 -0
- package/dist/storage/repositories/extract-sessions-repository.js +96 -0
- package/dist/storage/repositories/improve-runs-repository.js +146 -0
- package/dist/storage/repositories/index-db.js +14 -8
- package/dist/storage/repositories/proposals-repository.js +220 -0
- package/dist/storage/repositories/recombine-repository.js +213 -0
- package/dist/storage/repositories/registry-cache.js +93 -0
- package/dist/storage/repositories/registry-index-cache-repository.js +46 -0
- package/dist/storage/repositories/task-history-repository.js +93 -0
- package/dist/storage/sqlite-pragmas.js +146 -0
- package/dist/tasks/backends/cron.js +1 -1
- package/dist/tasks/backends/index.js +9 -0
- package/dist/tasks/backends/launchd.js +1 -1
- package/dist/tasks/backends/schtasks.js +1 -1
- package/dist/tasks/{resolveAkmBin.js → resolve-akm-bin.js} +2 -2
- package/dist/tasks/runner.js +15 -13
- package/dist/text-import-hook.mjs +0 -0
- package/dist/wiki/wiki.js +52 -11
- package/dist/workflows/cli.js +1 -0
- package/dist/workflows/db.js +3 -4
- package/dist/workflows/runtime/runs.js +43 -118
- package/dist/workflows/runtime/workflow-asset-loader.js +125 -0
- package/dist/workflows/validate-summary.js +2 -7
- package/docs/README.md +69 -18
- package/docs/data-and-telemetry.md +5 -4
- package/docs/migration/release-notes/0.7.0.md +1 -1
- package/docs/migration/release-notes/0.9.0.md +39 -0
- package/package.json +10 -10
- package/dist/assets/tasks/core/update-stashes.yml +0 -4
- package/dist/commands/db-cli.js +0 -23
- package/dist/indexer/db/db-backup.js +0 -376
- package/dist/indexer/passes/staleness-detect.js +0 -488
|
@@ -0,0 +1,1100 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { parseAssetRef } from "../../core/asset/asset-ref.js";
|
|
6
|
+
import { daysToMs } from "../../core/common.js";
|
|
7
|
+
import { loadConfig } from "../../core/config/config.js";
|
|
8
|
+
import { UsageError } from "../../core/errors.js";
|
|
9
|
+
import { appendEvent } from "../../core/events.js";
|
|
10
|
+
import { openLogsDatabase, purgeOldTaskLogs } from "../../core/logs-db.js";
|
|
11
|
+
import { getDbPath } from "../../core/paths.js";
|
|
12
|
+
import { withStateDb } from "../../core/state-db.js";
|
|
13
|
+
import { info, warn } from "../../core/warn.js";
|
|
14
|
+
import { closeDatabase, openIndexDatabase } from "../../indexer/db/db.js";
|
|
15
|
+
import { runGraphExtractionPass } from "../../indexer/graph/graph-extraction.js";
|
|
16
|
+
import { withIndexWriterLease } from "../../indexer/index-writer-lock.js";
|
|
17
|
+
import { collectPendingMemories, runMemoryInferencePass, } from "../../indexer/passes/memory-inference.js";
|
|
18
|
+
import { getWritableStashDirs, resolveSourceEntries } from "../../indexer/search/search-source.js";
|
|
19
|
+
import { resolveImproveProcessRunnerFromProfile } from "../../integrations/agent/runner.js";
|
|
20
|
+
import { isProcessEnabled } from "../../llm/feature-gate.js";
|
|
21
|
+
import { withLlmStage } from "../../llm/usage-telemetry.js";
|
|
22
|
+
import { purgeOldCycleMetrics } from "../../storage/repositories/canaries-repository.js";
|
|
23
|
+
import { purgeOldEvents } from "../../storage/repositories/events-repository.js";
|
|
24
|
+
import { purgeOldImproveRuns } from "../../storage/repositories/improve-runs-repository.js";
|
|
25
|
+
import { createProposal, expireStaleProposals, isProposalSkipped, listProposals, purgeOrphanProposals, } from "../proposal/repository.js";
|
|
26
|
+
import { checkDeadUrls } from "../url-checker.js";
|
|
27
|
+
import { DEFAULT_RETENTION_DAYS as CYCLE_METRICS_RETENTION_DAYS, runCollapseDetector } from "./collapse-detector.js";
|
|
28
|
+
import { deriveLessonRef } from "./distill.js";
|
|
29
|
+
import { deriveKnowledgeRef } from "./distill-promotion-policy.js";
|
|
30
|
+
// Eligibility / candidate-selection predicates live in ./eligibility.
|
|
31
|
+
import { findAssetFilePath, isDistillCandidateRef } from "./eligibility.js";
|
|
32
|
+
import { writeEvalCase } from "./eval-cases.js";
|
|
33
|
+
import { makeGateConfig, runAutoAcceptGate } from "./improve-auto-accept.js";
|
|
34
|
+
import { resolveProcessEnabled, shouldSkipRef } from "./improve-profiles.js";
|
|
35
|
+
// The pre-loop preparation pipeline lives in ./preparation.
|
|
36
|
+
import { maybeAutoTuneThreshold } from "./preparation.js";
|
|
37
|
+
import { akmProcedural } from "./procedural.js";
|
|
38
|
+
import { akmRecombine } from "./recombine.js";
|
|
39
|
+
import { recordNoOp, resetConsecutiveNoOps } from "./salience.js";
|
|
40
|
+
import { errMessage, refSlug } from "./shared.js";
|
|
41
|
+
// ── improve loop / post-loop / maintenance stages ───────────────────
|
|
42
|
+
// The cycle stages run by akmImprove, extracted from improve.ts.
|
|
43
|
+
export async function runImproveLoopStage(args) {
|
|
44
|
+
const { scope, options, primaryStashDir, reflectFn, distillFn, loopRefs, actions, signalBearingSet, distillCooledRefs, distillOnlyRefs, recentErrors, rejectedProposalsByRef, utilityMap, startMs, budgetMs, eventsCtx, improveProfile, } = args;
|
|
45
|
+
// O-1 (#364): compute remaining budget at call time so each sub-call
|
|
46
|
+
// receives only its fair share of the wall-clock budget.
|
|
47
|
+
const remainingBudgetMs = () => Math.max(0, budgetMs - (Date.now() - startMs));
|
|
48
|
+
const RECENT_ERRORS_CAP = 3;
|
|
49
|
+
// requirePlannedRefs guard: when the distill profile sets this flag, skip
|
|
50
|
+
// distill for distill-only refs if the reflect phase produced no planned refs.
|
|
51
|
+
// Prevents the distill loop from generating hundreds of distill-skipped events
|
|
52
|
+
// on quiet passes (all refs on reflect cooldown, no new signal to distill).
|
|
53
|
+
const requirePlannedRefs = improveProfile?.processes?.distill?.requirePlannedRefs === true;
|
|
54
|
+
const _distillOnlyRefNames = new Set(distillOnlyRefs.map((r) => r.ref));
|
|
55
|
+
const hasReflectEligibleRefs = loopRefs.some((r) => !_distillOnlyRefNames.has(r.ref));
|
|
56
|
+
const skipDistillDueToRequirePlannedRefs = requirePlannedRefs && !hasReflectEligibleRefs;
|
|
57
|
+
// R-2 / #389: Self-Consistency multi-sample voting helpers.
|
|
58
|
+
// Wang et al. arXiv:2203.11171 — N=3 samples beat single-shot on reasoning tasks.
|
|
59
|
+
const SC_THRESHOLD = options.selfConsistencyThreshold ?? 0.7;
|
|
60
|
+
const SC_N = Math.min(Math.max(2, options.selfConsistencyN ?? 3), 5);
|
|
61
|
+
/**
|
|
62
|
+
* Compute Jaccard token overlap between two strings.
|
|
63
|
+
* Tokenizes by whitespace; returns 0 when both are empty.
|
|
64
|
+
*/
|
|
65
|
+
function jaccardSimilarity(a, b) {
|
|
66
|
+
const tokensA = new Set(a.split(/\s+/).filter(Boolean));
|
|
67
|
+
const tokensB = new Set(b.split(/\s+/).filter(Boolean));
|
|
68
|
+
if (tokensA.size === 0 && tokensB.size === 0)
|
|
69
|
+
return 1;
|
|
70
|
+
let intersection = 0;
|
|
71
|
+
for (const t of tokensA) {
|
|
72
|
+
if (tokensB.has(t))
|
|
73
|
+
intersection++;
|
|
74
|
+
}
|
|
75
|
+
const union = tokensA.size + tokensB.size - intersection;
|
|
76
|
+
return union > 0 ? intersection / union : 0;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Given N reflect results, return the one with the highest average Jaccard
|
|
80
|
+
* similarity to all other successful results (majority-vote winner).
|
|
81
|
+
* Falls back to the first successful result when N < 2.
|
|
82
|
+
*/
|
|
83
|
+
function pickMajorityVote(results) {
|
|
84
|
+
const successful = results.filter((r) => r.ok);
|
|
85
|
+
if (successful.length === 0)
|
|
86
|
+
return (results[0] ?? {
|
|
87
|
+
schemaVersion: 1,
|
|
88
|
+
ok: false,
|
|
89
|
+
reason: "non_zero_exit",
|
|
90
|
+
error: "all samples failed",
|
|
91
|
+
exitCode: null,
|
|
92
|
+
});
|
|
93
|
+
if (successful.length === 1)
|
|
94
|
+
return successful[0];
|
|
95
|
+
let bestIdx = 0;
|
|
96
|
+
let bestScore = -1;
|
|
97
|
+
for (let i = 0; i < successful.length; i++) {
|
|
98
|
+
let totalSim = 0;
|
|
99
|
+
for (let j = 0; j < successful.length; j++) {
|
|
100
|
+
if (i === j)
|
|
101
|
+
continue;
|
|
102
|
+
totalSim += jaccardSimilarity(successful[i].proposal.payload.content ?? "", successful[j].proposal.payload.content ?? "");
|
|
103
|
+
}
|
|
104
|
+
const avgSim = totalSim / (successful.length - 1);
|
|
105
|
+
if (avgSim > bestScore) {
|
|
106
|
+
bestScore = avgSim;
|
|
107
|
+
bestIdx = i;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return successful[bestIdx] ?? successful[0];
|
|
111
|
+
}
|
|
112
|
+
// O-5 / #378: helper to push per-originator errors into the rolling window.
|
|
113
|
+
function pushRecentError(originator, msg) {
|
|
114
|
+
if (!recentErrors[originator])
|
|
115
|
+
recentErrors[originator] = [];
|
|
116
|
+
recentErrors[originator].push(msg);
|
|
117
|
+
if (recentErrors[originator].length > RECENT_ERRORS_CAP)
|
|
118
|
+
recentErrors[originator].shift();
|
|
119
|
+
}
|
|
120
|
+
// Build a Set for O(1) membership test — these refs skip the reflect call (Bug D2).
|
|
121
|
+
const distillOnlyRefSet = new Set(distillOnlyRefs.map((r) => r.ref));
|
|
122
|
+
let completedCount = 0;
|
|
123
|
+
let reflectsWithErrorContext = 0;
|
|
124
|
+
const memoryRefsForInference = new Set();
|
|
125
|
+
// Pre-load all pending proposals once instead of querying per asset in the loop.
|
|
126
|
+
const dedupeStashDirForProposals = primaryStashDir ?? options.stashDir;
|
|
127
|
+
const pendingProposalRefSet = new Set(dedupeStashDirForProposals
|
|
128
|
+
? listProposals(dedupeStashDirForProposals, { status: "pending" }).map((p) => p.ref)
|
|
129
|
+
: []);
|
|
130
|
+
let gateAutoAcceptedCount = 0;
|
|
131
|
+
let gateAutoAcceptFailedCount = 0;
|
|
132
|
+
const reflectGateCfg = makeGateConfig("reflect", {
|
|
133
|
+
globalThreshold: options.autoAccept,
|
|
134
|
+
dryRun: options.dryRun ?? false,
|
|
135
|
+
stashDir: primaryStashDir,
|
|
136
|
+
config: options.config ?? loadConfig(),
|
|
137
|
+
eventsCtx,
|
|
138
|
+
stateDbPath: eventsCtx?.dbPath,
|
|
139
|
+
// candidateCount drives the exploration budget. loopRefs is the per-phase
|
|
140
|
+
// set for reflect/distill; pass it so exploration budget is proportional.
|
|
141
|
+
candidateCount: loopRefs.length,
|
|
142
|
+
});
|
|
143
|
+
const distillGateCfg = makeGateConfig("distill", {
|
|
144
|
+
globalThreshold: options.autoAccept,
|
|
145
|
+
dryRun: options.dryRun ?? false,
|
|
146
|
+
stashDir: primaryStashDir,
|
|
147
|
+
config: options.config ?? loadConfig(),
|
|
148
|
+
eventsCtx,
|
|
149
|
+
stateDbPath: eventsCtx?.dbPath,
|
|
150
|
+
candidateCount: loopRefs.length,
|
|
151
|
+
});
|
|
152
|
+
for (const planned of loopRefs) {
|
|
153
|
+
if (Date.now() - startMs >= budgetMs) {
|
|
154
|
+
const remaining = loopRefs.length - completedCount;
|
|
155
|
+
info(`[improve] budget exhausted after ${Math.round((Date.now() - startMs) / 60000)}min — ${remaining} assets skipped`);
|
|
156
|
+
appendEvent({
|
|
157
|
+
eventType: "improve_skipped",
|
|
158
|
+
ref: planned.ref,
|
|
159
|
+
metadata: {
|
|
160
|
+
reason: "budget_exhausted",
|
|
161
|
+
remaining,
|
|
162
|
+
},
|
|
163
|
+
}, eventsCtx);
|
|
164
|
+
// B11: Emit improve_skipped for all remaining assets that will not be processed.
|
|
165
|
+
for (const remainingRef of loopRefs.slice(completedCount + 1)) {
|
|
166
|
+
appendEvent({
|
|
167
|
+
eventType: "improve_skipped",
|
|
168
|
+
ref: remainingRef.ref,
|
|
169
|
+
metadata: { reason: "budget_exhausted_batch", remaining: loopRefs.length - completedCount - 1 },
|
|
170
|
+
}, eventsCtx);
|
|
171
|
+
}
|
|
172
|
+
actions.push({
|
|
173
|
+
ref: planned.ref,
|
|
174
|
+
mode: "error",
|
|
175
|
+
result: { ok: false, error: "timeout: improve wall-clock budget exhausted" },
|
|
176
|
+
});
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
try {
|
|
180
|
+
// Bug D2: distillOnlyRefs skip the reflect call but still run the distill path.
|
|
181
|
+
// Bug D1: in-loop distill-cooldown check removed — distill-cooled candidates
|
|
182
|
+
// have their synthetic actions emitted in runImprovePreparationStage.
|
|
183
|
+
const isDistillOnly = distillOnlyRefSet.has(planned.ref);
|
|
184
|
+
const parsedPlannedRef = parseAssetRef(planned.ref);
|
|
185
|
+
// B6: derived memories are machine-generated; skip reflect to avoid noisy proposals.
|
|
186
|
+
// shouldDistillMemoryRef already returns false for .derived refs, so the distill
|
|
187
|
+
// path is also a no-op for them — we just avoid unnecessary agent spawns.
|
|
188
|
+
// D2: distillOnlyRefs also skip the reflect call (reflect-cooled, distill path only).
|
|
189
|
+
if (!isDistillOnly && !planned.ref.endsWith(".derived")) {
|
|
190
|
+
// Type guard: skip reflect for unsupported types (script, env, task, etc.)
|
|
191
|
+
// and raw wiki directories, driven by the active improve profile.
|
|
192
|
+
const reflectSkip = shouldSkipRef(planned.ref, "reflect", improveProfile);
|
|
193
|
+
if (reflectSkip.skip) {
|
|
194
|
+
actions.push({
|
|
195
|
+
ref: planned.ref,
|
|
196
|
+
mode: "reflect-skipped",
|
|
197
|
+
result: { ok: true, reason: reflectSkip.reason },
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
// O-5 / #378: only inject reflect-originator errors into the reflect call.
|
|
202
|
+
// Cross-task errors (e.g. schema-repair) must NOT contaminate reflect prompts.
|
|
203
|
+
const reflectErrors = recentErrors.reflect ?? [];
|
|
204
|
+
if (reflectErrors.length > 0)
|
|
205
|
+
reflectsWithErrorContext++;
|
|
206
|
+
// O-1 (#364): pass remaining budget as timeoutMs so the agent spawn is
|
|
207
|
+
// bounded by the wall-clock deadline rather than the default per-profile timeout.
|
|
208
|
+
const reflectBudgetMs = remainingBudgetMs();
|
|
209
|
+
// Wire profile.processes.reflect.{mode, profile, timeoutMs} into the reflect
|
|
210
|
+
// dispatch when present. Falls back to akmReflect's own config-based resolution
|
|
211
|
+
// (profiles.improve.<name>.processes.reflect → defaults.llm) when the profile
|
|
212
|
+
// does not specify.
|
|
213
|
+
const reflectProfileRunner = resolveImproveProcessRunnerFromProfile(improveProfile.processes?.reflect, options.config ?? loadConfig());
|
|
214
|
+
const reflectCallArgs = {
|
|
215
|
+
ref: planned.ref,
|
|
216
|
+
task: options.task,
|
|
217
|
+
// Active profile so reflect's per-process reads honor `--profile`.
|
|
218
|
+
...(improveProfile ? { improveProfile } : {}),
|
|
219
|
+
...(options.stashDir ? { stashDir: options.stashDir } : {}),
|
|
220
|
+
...(reflectErrors.length > 0 ? { avoidPatterns: [...reflectErrors] } : {}),
|
|
221
|
+
agentProcess: options.agentProcess ?? "reflect",
|
|
222
|
+
eventSource: "improve",
|
|
223
|
+
// #639 — resolve the low-value filter from the ACTIVE improve profile
|
|
224
|
+
// (default off when unset), so the running profile decides instead of
|
|
225
|
+
// a hardcoded profiles.improve.default path.
|
|
226
|
+
lowValueFilter: improveProfile.processes?.reflect?.lowValueFilter?.enabled === true,
|
|
227
|
+
...(reflectBudgetMs > 0 ? { timeoutMs: reflectBudgetMs } : {}),
|
|
228
|
+
...(reflectProfileRunner ? { runner: reflectProfileRunner } : {}),
|
|
229
|
+
// Attribution: carry the eligibility lane so reflect stamps it on
|
|
230
|
+
// the reflect_invoked event and the persisted proposal.
|
|
231
|
+
...(planned.eligibilitySource ? { eligibilitySource: planned.eligibilitySource } : {}),
|
|
232
|
+
};
|
|
233
|
+
// R-2 / #389: Self-consistency multi-sample voting for high-utility refs.
|
|
234
|
+
// Self-Consistency arXiv:2203.11171 — N=3 samples beat single-shot quality.
|
|
235
|
+
const refUtility = utilityMap.get(planned.ref) ?? 0;
|
|
236
|
+
const useConsistency = refUtility >= SC_THRESHOLD && SC_N >= 2;
|
|
237
|
+
let reflectResult;
|
|
238
|
+
if (useConsistency) {
|
|
239
|
+
const samples = [];
|
|
240
|
+
for (let s = 0; s < SC_N; s++) {
|
|
241
|
+
if (remainingBudgetMs() <= 0)
|
|
242
|
+
break;
|
|
243
|
+
// draftMode: skip DB write so each sample doesn't create a proposal.
|
|
244
|
+
samples.push(await withLlmStage("reflect", () => reflectFn({ ...reflectCallArgs, draftMode: true })));
|
|
245
|
+
}
|
|
246
|
+
const winner = pickMajorityVote(samples.length > 0
|
|
247
|
+
? samples
|
|
248
|
+
: [await withLlmStage("reflect", () => reflectFn({ ...reflectCallArgs, draftMode: true }))]);
|
|
249
|
+
// Persist only the majority-vote winner as a single real proposal.
|
|
250
|
+
if (winner.ok && primaryStashDir) {
|
|
251
|
+
const persistResult = createProposal(primaryStashDir, {
|
|
252
|
+
ref: winner.proposal.ref,
|
|
253
|
+
source: "reflect",
|
|
254
|
+
sourceRun: `reflect-sc-${Date.now()}`,
|
|
255
|
+
payload: winner.proposal.payload,
|
|
256
|
+
// Attribution: the self-consistency path persists the winner here
|
|
257
|
+
// (draftMode skips reflect's own createProposal), so stamp the lane.
|
|
258
|
+
...(planned.eligibilitySource ? { eligibilitySource: planned.eligibilitySource } : {}),
|
|
259
|
+
});
|
|
260
|
+
reflectResult = isProposalSkipped(persistResult)
|
|
261
|
+
? {
|
|
262
|
+
schemaVersion: 1,
|
|
263
|
+
ok: false,
|
|
264
|
+
reason: "cooldown",
|
|
265
|
+
error: `SC proposal skipped: ${persistResult.message}`,
|
|
266
|
+
ref: winner.ref,
|
|
267
|
+
exitCode: null,
|
|
268
|
+
}
|
|
269
|
+
: { ...winner, proposal: persistResult };
|
|
270
|
+
}
|
|
271
|
+
else {
|
|
272
|
+
reflectResult = winner;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
else {
|
|
276
|
+
reflectResult = await withLlmStage("reflect", () => reflectFn(reflectCallArgs));
|
|
277
|
+
}
|
|
278
|
+
const isCooldown = !reflectResult.ok && reflectResult.reason === "cooldown";
|
|
279
|
+
// Content-policy guard hits (reflect size-rail rejections) are NOT
|
|
280
|
+
// LLM faults — the agent responded fine, the downstream guard
|
|
281
|
+
// blocked the output. Route them to a distinct `reflect-guard-rejected`
|
|
282
|
+
// mode so health metrics can split deterministic guard hits out of
|
|
283
|
+
// true LLM failures. See
|
|
284
|
+
// `/tmp/akm-health-investigations/metrics-taxonomy-review.md` §1a.
|
|
285
|
+
const isGuardReject = !reflectResult.ok && reflectResult.reason === "content_policy_reject";
|
|
286
|
+
// Type-guard rejection (reflect refused a script/env/task ref) is
|
|
287
|
+
// also NOT an LLM failure — the LLM is never invoked. Route to the
|
|
288
|
+
// existing `reflect-skipped` bucket so it does not inflate the
|
|
289
|
+
// failure-rate numerator. ~9% of `reflect-failed` events in the
|
|
290
|
+
// user's stack were this case; see review §1a row "Reflect refused
|
|
291
|
+
// asset type".
|
|
292
|
+
const isTypeRefused = !reflectResult.ok && reflectResult.reason === "unsupported_type";
|
|
293
|
+
// Noise-gate suppression (#580): the candidate edit was an empty
|
|
294
|
+
// diff or a cosmetic-only reformat of the current asset. Like
|
|
295
|
+
// `unsupported_type`, this is a deterministic skip — not an LLM
|
|
296
|
+
// fault — so it routes to the `reflect-skipped` bucket and stays
|
|
297
|
+
// out of recentErrors/avoidPatterns.
|
|
298
|
+
const isNoChange = !reflectResult.ok && reflectResult.reason === "no_change";
|
|
299
|
+
actions.push({
|
|
300
|
+
ref: planned.ref,
|
|
301
|
+
mode: reflectResult.ok
|
|
302
|
+
? "reflect"
|
|
303
|
+
: isCooldown
|
|
304
|
+
? "reflect-cooldown"
|
|
305
|
+
: isGuardReject
|
|
306
|
+
? "reflect-guard-rejected"
|
|
307
|
+
: isTypeRefused || isNoChange
|
|
308
|
+
? "reflect-skipped"
|
|
309
|
+
: "reflect-failed",
|
|
310
|
+
result: reflectResult,
|
|
311
|
+
});
|
|
312
|
+
// Cooldown skips, guard rejects, type-refused skips, and noise-gate
|
|
313
|
+
// skips are not failures — do not pollute recentErrors with them
|
|
314
|
+
// (those get injected as `avoidPatterns` into the next reflect
|
|
315
|
+
// prompt). Guard rejects ARE worth showing the LLM as a learn-signal
|
|
316
|
+
// so the next iteration sees "your last expansion was too large";
|
|
317
|
+
// type-refused and no-change are deterministic and add no learning
|
|
318
|
+
// signal.
|
|
319
|
+
if (!reflectResult.ok && !isCooldown && !isTypeRefused && !isNoChange) {
|
|
320
|
+
const errMsg = reflectResult.error ?? reflectResult.reason ?? "unknown reflect error";
|
|
321
|
+
pushRecentError("reflect", errMsg);
|
|
322
|
+
}
|
|
323
|
+
// improve_reflect_outcome — per-asset metric for tuning the reflect path.
|
|
324
|
+
appendEvent({
|
|
325
|
+
eventType: "improve_reflect_outcome",
|
|
326
|
+
ref: planned.ref,
|
|
327
|
+
metadata: {
|
|
328
|
+
ok: reflectResult.ok,
|
|
329
|
+
durationMs: reflectResult.ok ? reflectResult.durationMs : undefined,
|
|
330
|
+
agentProfile: reflectResult.ok ? reflectResult.agentProfile : undefined,
|
|
331
|
+
reason: reflectResult.ok ? undefined : reflectResult.reason,
|
|
332
|
+
},
|
|
333
|
+
}, eventsCtx);
|
|
334
|
+
// Plasticity counter (plan §WS-1 step 8): record no-ops so the
|
|
335
|
+
// WS-1 selection comparator (effectiveScore, ~line 3073) can dampen
|
|
336
|
+
// repeatedly-silent assets during consolidation-selection.
|
|
337
|
+
// A no_change reflect means the LLM was invoked but found nothing to
|
|
338
|
+
// improve — the asset is stable. Track it. A successful reflect means
|
|
339
|
+
// the asset changed; reset the counter so the dampener lifts.
|
|
340
|
+
if (isNoChange && eventsCtx?.db) {
|
|
341
|
+
try {
|
|
342
|
+
recordNoOp(eventsCtx.db, planned.ref);
|
|
343
|
+
}
|
|
344
|
+
catch {
|
|
345
|
+
// best-effort: plasticity counter failure never blocks the run
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
else if (reflectResult.ok && eventsCtx?.db) {
|
|
349
|
+
try {
|
|
350
|
+
resetConsecutiveNoOps(eventsCtx.db, planned.ref);
|
|
351
|
+
}
|
|
352
|
+
catch {
|
|
353
|
+
// best-effort
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
if (reflectResult.ok) {
|
|
357
|
+
const reflectGr = await runAutoAcceptGate([{ proposalId: reflectResult.proposal.id, confidence: reflectResult.proposal.confidence }], reflectGateCfg);
|
|
358
|
+
gateAutoAcceptedCount += reflectGr.promoted.length;
|
|
359
|
+
gateAutoAcceptFailedCount += reflectGr.failed.length;
|
|
360
|
+
}
|
|
361
|
+
} // end else (reflect type/profile check)
|
|
362
|
+
}
|
|
363
|
+
else if (!isDistillOnly && planned.ref.endsWith(".derived")) {
|
|
364
|
+
// B6: .derived refs skip reflect; record synthetic skip action.
|
|
365
|
+
actions.push({
|
|
366
|
+
ref: planned.ref,
|
|
367
|
+
mode: "distill-skipped",
|
|
368
|
+
result: { ok: true, reason: "derived-memory-reflect-skipped" },
|
|
369
|
+
});
|
|
370
|
+
appendEvent({
|
|
371
|
+
eventType: "improve_skipped",
|
|
372
|
+
ref: planned.ref,
|
|
373
|
+
metadata: { reason: "derived_memory_reflect_skipped" },
|
|
374
|
+
}, eventsCtx);
|
|
375
|
+
}
|
|
376
|
+
// isDistillOnly refs: no reflect action emitted — proceed directly to distill path below.
|
|
377
|
+
const hasRecentFeedbackSignal = signalBearingSet.has(planned.ref);
|
|
378
|
+
const explicitRefScope = scope.mode === "ref";
|
|
379
|
+
// Profile gate: apply the full type-filter / raw-wiki / disabled rules to
|
|
380
|
+
// distill so callers who configure `profile.processes.distill.allowedTypes`
|
|
381
|
+
// or land on raw-wiki refs get a recorded skip action instead of silently
|
|
382
|
+
// proceeding.
|
|
383
|
+
const distillSkip = shouldSkipRef(planned.ref, "distill", improveProfile);
|
|
384
|
+
if (distillSkip.skip) {
|
|
385
|
+
actions.push({
|
|
386
|
+
ref: planned.ref,
|
|
387
|
+
mode: "distill-skipped",
|
|
388
|
+
result: { ok: true, reason: distillSkip.reason },
|
|
389
|
+
});
|
|
390
|
+
completedCount++;
|
|
391
|
+
info(`[improve] ${completedCount}/${loopRefs.length} ${planned.ref}`);
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
394
|
+
// requirePlannedRefs guard: skip distill for distill-only refs when no
|
|
395
|
+
// reflect-eligible refs were planned this run, preventing mass skip events.
|
|
396
|
+
if (skipDistillDueToRequirePlannedRefs && isDistillOnly) {
|
|
397
|
+
actions.push({
|
|
398
|
+
ref: planned.ref,
|
|
399
|
+
mode: "distill-skipped",
|
|
400
|
+
result: { ok: true, reason: "require_planned_refs" },
|
|
401
|
+
});
|
|
402
|
+
completedCount++;
|
|
403
|
+
info(`[improve] ${completedCount}/${loopRefs.length} ${planned.ref}`);
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
406
|
+
// See `isDistillCandidateRef` — excludes `lesson:*` (and anything else in
|
|
407
|
+
// DISTILL_REFUSED_INPUT_TYPES) so distill never gets queued for an input
|
|
408
|
+
// it will refuse.
|
|
409
|
+
const shouldAttemptDistill = isDistillCandidateRef(planned.ref, options.stashDir);
|
|
410
|
+
const skipMemoryDistillForWeakSignal = !isDistillOnly && parsedPlannedRef.type === "memory" && !hasRecentFeedbackSignal && !explicitRefScope;
|
|
411
|
+
// distillCooledRefs guard: pre-filter emitted synthetic actions for distill-candidate
|
|
412
|
+
// refs; non-candidate refs in the set are blocked here.
|
|
413
|
+
// O-2 (#365): bypass the distill cooldown when the user explicitly targeted
|
|
414
|
+
// this ref via --scope — their intent overrides unattended-run policies.
|
|
415
|
+
if (shouldAttemptDistill &&
|
|
416
|
+
!skipMemoryDistillForWeakSignal &&
|
|
417
|
+
(!distillCooledRefs.has(planned.ref) || explicitRefScope)) {
|
|
418
|
+
// TODO(refactor): single call site needs both lesson+knowledge refs for proposal dedup. If a third target ref type is added, extract deriveAllTargetRefs(inputRef): string[].
|
|
419
|
+
const lessonRef = deriveLessonRef(planned.ref);
|
|
420
|
+
const knowledgeRef = deriveKnowledgeRef(planned.ref);
|
|
421
|
+
const dedupeStashDir = primaryStashDir ?? options.stashDir;
|
|
422
|
+
if (dedupeStashDir) {
|
|
423
|
+
// B2: check both lesson ref and knowledge ref since auto-promoted memories
|
|
424
|
+
// create knowledge: proposals, not lesson: proposals.
|
|
425
|
+
const hasExistingPending = pendingProposalRefSet.has(lessonRef) || pendingProposalRefSet.has(knowledgeRef);
|
|
426
|
+
if (hasExistingPending) {
|
|
427
|
+
actions.push({
|
|
428
|
+
ref: planned.ref,
|
|
429
|
+
mode: "distill-skipped",
|
|
430
|
+
result: { ok: true, reason: "pending proposal exists" },
|
|
431
|
+
});
|
|
432
|
+
appendEvent({
|
|
433
|
+
eventType: "improve_skipped",
|
|
434
|
+
ref: planned.ref,
|
|
435
|
+
metadata: { reason: "pending_proposal_exists" },
|
|
436
|
+
}, eventsCtx);
|
|
437
|
+
completedCount++;
|
|
438
|
+
info(`[improve] ${completedCount}/${loopRefs.length} ${planned.ref}`);
|
|
439
|
+
continue;
|
|
440
|
+
}
|
|
441
|
+
// D-2 (#370): reject-aware cooldown for distill. When the reviewer
|
|
442
|
+
// recently rejected a distilled lesson or knowledge proposal for this
|
|
443
|
+
// asset, skip re-distillation for a 1-day grace window. Prevents the
|
|
444
|
+
// same rejected proposal from being regenerated immediately. The
|
|
445
|
+
// window is fixed (the 0.8.0 redesign moved per-ref cooldowns to
|
|
446
|
+
// signal-delta gates and dropped --distill-cooldown-days; a short
|
|
447
|
+
// reject grace is preserved here so a fresh rejection isn't
|
|
448
|
+
// overridden by the same run).
|
|
449
|
+
// References: ExpeL arXiv:2308.10144, STaR arXiv:2203.14465.
|
|
450
|
+
const DISTILL_REJECT_COOLDOWN_MS = daysToMs(1);
|
|
451
|
+
const recentlyRejectedLesson = !explicitRefScope && // O-2: bypass when --scope <ref> is explicit
|
|
452
|
+
(rejectedProposalsByRef.has(lessonRef) || rejectedProposalsByRef.has(knowledgeRef));
|
|
453
|
+
if (recentlyRejectedLesson) {
|
|
454
|
+
const rejectedEntry = rejectedProposalsByRef.get(lessonRef) ?? rejectedProposalsByRef.get(knowledgeRef);
|
|
455
|
+
const rejectedAgeMs = rejectedEntry ? Date.now() - new Date(rejectedEntry.ts).getTime() : 0;
|
|
456
|
+
if (rejectedAgeMs < DISTILL_REJECT_COOLDOWN_MS) {
|
|
457
|
+
actions.push({
|
|
458
|
+
ref: planned.ref,
|
|
459
|
+
mode: "distill-skipped",
|
|
460
|
+
result: { ok: true, reason: "distill reject grace window" },
|
|
461
|
+
});
|
|
462
|
+
appendEvent({
|
|
463
|
+
eventType: "improve_skipped",
|
|
464
|
+
ref: planned.ref,
|
|
465
|
+
metadata: {
|
|
466
|
+
reason: "distill_reject_grace_window",
|
|
467
|
+
},
|
|
468
|
+
}, eventsCtx);
|
|
469
|
+
completedCount++;
|
|
470
|
+
info(`[improve] ${completedCount}/${loopRefs.length} ${planned.ref}`);
|
|
471
|
+
continue;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
const distillResult = await withLlmStage("distill", () => distillFn({
|
|
476
|
+
ref: planned.ref,
|
|
477
|
+
...(parsedPlannedRef.type === "memory" ? { proposalKind: "auto" } : {}),
|
|
478
|
+
...(options.stashDir ? { stashDir: options.stashDir } : {}),
|
|
479
|
+
// Active profile so distill's per-process reads honor `--profile`.
|
|
480
|
+
...(improveProfile ? { improveProfile } : {}),
|
|
481
|
+
// Attribution: carry the eligibility lane so distill stamps it on the
|
|
482
|
+
// distill_invoked event and the persisted proposal.
|
|
483
|
+
...(planned.eligibilitySource ? { eligibilitySource: planned.eligibilitySource } : {}),
|
|
484
|
+
}));
|
|
485
|
+
actions.push({ ref: planned.ref, mode: "distill", result: distillResult });
|
|
486
|
+
if (distillResult.outcome === "queued" && distillResult.proposal) {
|
|
487
|
+
const distillGr = await runAutoAcceptGate([{ proposalId: distillResult.proposal.id, confidence: distillResult.proposal.confidence }], distillGateCfg);
|
|
488
|
+
gateAutoAcceptedCount += distillGr.promoted.length;
|
|
489
|
+
gateAutoAcceptFailedCount += distillGr.failed.length;
|
|
490
|
+
}
|
|
491
|
+
if (parsedPlannedRef.type === "memory") {
|
|
492
|
+
const promotedToKnowledge = distillResult.outcome === "queued" && distillResult.proposalKind === "knowledge";
|
|
493
|
+
if (!promotedToKnowledge)
|
|
494
|
+
memoryRefsForInference.add(planned.ref);
|
|
495
|
+
}
|
|
496
|
+
// Plasticity counter (plan §WS-1 step 8) for the distill path.
|
|
497
|
+
// quality_rejected: the LLM ran but produced output that didn't pass the
|
|
498
|
+
// quality gate — the asset is not yielding useful distill output.
|
|
499
|
+
// queued: a proposal was produced; reset the no-op counter.
|
|
500
|
+
if (eventsCtx?.db) {
|
|
501
|
+
try {
|
|
502
|
+
if (distillResult.outcome === "quality_rejected" || distillResult.outcome === "skipped") {
|
|
503
|
+
recordNoOp(eventsCtx.db, planned.ref);
|
|
504
|
+
}
|
|
505
|
+
else if (distillResult.outcome === "queued") {
|
|
506
|
+
resetConsecutiveNoOps(eventsCtx.db, planned.ref);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
catch {
|
|
510
|
+
// best-effort: plasticity counter failure never blocks the run
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
if (distillResult.outcome === "quality_rejected" && primaryStashDir) {
|
|
514
|
+
const slug = refSlug(planned.ref);
|
|
515
|
+
writeEvalCase(primaryStashDir, {
|
|
516
|
+
ref: planned.ref,
|
|
517
|
+
failureReason: distillResult.reason ?? "quality gate rejected",
|
|
518
|
+
assetType: parseAssetRef(planned.ref).type ?? "unknown",
|
|
519
|
+
rejectedAt: Date.now(),
|
|
520
|
+
source: "distill_quality_rejected",
|
|
521
|
+
slug: `${slug}-${Date.now()}`,
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
// D6: use pre-loaded map instead of per-iteration DB query
|
|
525
|
+
const rejectedProposalEvent = rejectedProposalsByRef.get(planned.ref);
|
|
526
|
+
if (rejectedProposalEvent && primaryStashDir) {
|
|
527
|
+
const slug = refSlug(planned.ref);
|
|
528
|
+
writeEvalCase(primaryStashDir, {
|
|
529
|
+
ref: planned.ref,
|
|
530
|
+
failureReason: rejectedProposalEvent.metadata?.reason ?? "proposal rejected",
|
|
531
|
+
assetType: parseAssetRef(planned.ref).type ?? "unknown",
|
|
532
|
+
rejectedAt: new Date(rejectedProposalEvent.ts).getTime(),
|
|
533
|
+
source: "proposal_rejected",
|
|
534
|
+
slug: `${slug}-rejected`,
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
else if (skipMemoryDistillForWeakSignal) {
|
|
539
|
+
actions.push({
|
|
540
|
+
ref: planned.ref,
|
|
541
|
+
mode: "distill-skipped",
|
|
542
|
+
result: { ok: true, reason: "memory requires recent feedback signal" },
|
|
543
|
+
});
|
|
544
|
+
appendEvent({
|
|
545
|
+
eventType: "improve_skipped",
|
|
546
|
+
ref: planned.ref,
|
|
547
|
+
metadata: { reason: "memory_distill_requires_feedback" },
|
|
548
|
+
}, eventsCtx);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
catch (err) {
|
|
552
|
+
// B7: UsageError thrown by akmDistill on validation_failed should be recorded
|
|
553
|
+
// as mode:"distill" with outcome:"validation_failed", NOT as a generic error.
|
|
554
|
+
// The distill_invoked event was already emitted inside akmDistill before the throw.
|
|
555
|
+
if (err instanceof UsageError) {
|
|
556
|
+
actions.push({
|
|
557
|
+
ref: planned.ref,
|
|
558
|
+
mode: "distill",
|
|
559
|
+
result: { ok: false, outcome: "validation_failed", error: err.message },
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
else {
|
|
563
|
+
actions.push({
|
|
564
|
+
ref: planned.ref,
|
|
565
|
+
mode: "error",
|
|
566
|
+
result: { ok: false, error: errMessage(err) },
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
completedCount++;
|
|
571
|
+
info(`[improve] ${completedCount}/${loopRefs.length} ${planned.ref}`);
|
|
572
|
+
}
|
|
573
|
+
// WS-4: Per-phase threshold auto-tune — runs AFTER the loop so the gate
|
|
574
|
+
// has processed all candidates for this run. Persists each phase's tuned
|
|
575
|
+
// threshold to state.db for the NEXT run's makeGateConfig to read.
|
|
576
|
+
// Best-effort: a tune failure must never fail the improve run.
|
|
577
|
+
const stateDbPathForTune = eventsCtx?.dbPath;
|
|
578
|
+
if (options.autoAccept !== undefined && stateDbPathForTune) {
|
|
579
|
+
const phaseGateCfgMap = {
|
|
580
|
+
reflect: reflectGateCfg,
|
|
581
|
+
distill: distillGateCfg,
|
|
582
|
+
};
|
|
583
|
+
for (const phase of ["reflect", "distill"]) {
|
|
584
|
+
const phaseCfg = phaseGateCfgMap[phase];
|
|
585
|
+
try {
|
|
586
|
+
maybeAutoTuneThreshold(phaseCfg.phaseThreshold ?? options.autoAccept, options.config ?? loadConfig(), stateDbPathForTune, undefined, phase);
|
|
587
|
+
}
|
|
588
|
+
catch (err) {
|
|
589
|
+
warn(`[improve] calibration auto-tune (${phase}) skipped: ${errMessage(err)}`);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
return { reflectsWithErrorContext, memoryRefsForInference, gateAutoAcceptedCount, gateAutoAcceptFailedCount };
|
|
594
|
+
}
|
|
595
|
+
export async function runImprovePostLoopStage(args) {
|
|
596
|
+
const { scope, options, primaryStashDir, actionableRefs, appliedCleanup, cleanupWarnings, memoryRefsForInference, reindexFn, eventsCtx, budgetSignal, improveProfile, consolidationRan, } = args;
|
|
597
|
+
const allWarnings = [...cleanupWarnings, ...(appliedCleanup?.warnings ?? [])];
|
|
598
|
+
info("[improve] post-loop maintenance starting");
|
|
599
|
+
const maintenanceResult = await runImproveMaintenancePasses({
|
|
600
|
+
options,
|
|
601
|
+
primaryStashDir,
|
|
602
|
+
actionableRefs,
|
|
603
|
+
memoryRefsForInference,
|
|
604
|
+
allWarnings,
|
|
605
|
+
reindexFn,
|
|
606
|
+
consolidationRan,
|
|
607
|
+
// O-1 (#364): forward the budget signal to memory inference + graph extraction.
|
|
608
|
+
budgetSignal,
|
|
609
|
+
eventsCtx,
|
|
610
|
+
improveProfile,
|
|
611
|
+
});
|
|
612
|
+
let deadUrls;
|
|
613
|
+
if (scope.mode === "all" && primaryStashDir && actionableRefs.length > 0) {
|
|
614
|
+
try {
|
|
615
|
+
const knowledgeEntries = actionableRefs
|
|
616
|
+
.filter((r) => {
|
|
617
|
+
try {
|
|
618
|
+
return parseAssetRef(r.ref).type === "knowledge";
|
|
619
|
+
}
|
|
620
|
+
catch {
|
|
621
|
+
return false;
|
|
622
|
+
}
|
|
623
|
+
})
|
|
624
|
+
.slice(0, 10)
|
|
625
|
+
.map((r) => ({ ref: r.ref, body: "" }));
|
|
626
|
+
if (knowledgeEntries.length > 0) {
|
|
627
|
+
info(`[improve] checking URLs in ${knowledgeEntries.length} knowledge refs`);
|
|
628
|
+
deadUrls = await checkDeadUrls(primaryStashDir, knowledgeEntries);
|
|
629
|
+
info(`[improve] URL check complete (${deadUrls.length} dead/timeout URLs)`);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
catch {
|
|
633
|
+
// best-effort
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
// #609 — recombine / synthesize pass. Whole-corpus cross-episodic
|
|
637
|
+
// generalization. Runs in the post-loop stage under consolidate.lock (it
|
|
638
|
+
// reads the consolidated corpus and writes proposals). Opt-in: gated on the
|
|
639
|
+
// `recombine` process being enabled, whole-stash / type scope (never `ref`),
|
|
640
|
+
// and not a dry run. Mirrors the proactiveMaintenance opt-in wiring.
|
|
641
|
+
let recombination;
|
|
642
|
+
if (primaryStashDir &&
|
|
643
|
+
improveProfile &&
|
|
644
|
+
resolveProcessEnabled("recombine", improveProfile) &&
|
|
645
|
+
scope.mode !== "ref" &&
|
|
646
|
+
!options.dryRun) {
|
|
647
|
+
const recombineFn = options.recombineFn ?? akmRecombine;
|
|
648
|
+
try {
|
|
649
|
+
recombination = await recombineFn({
|
|
650
|
+
stashDir: primaryStashDir,
|
|
651
|
+
config: options.config ?? loadConfig(),
|
|
652
|
+
improveProfile,
|
|
653
|
+
...(options.runId ? { sourceRun: options.runId } : {}),
|
|
654
|
+
...(budgetSignal ? { signal: budgetSignal } : {}),
|
|
655
|
+
eligibilitySource: "recombine",
|
|
656
|
+
...(eventsCtx ? { ctx: eventsCtx } : {}),
|
|
657
|
+
minClusterSize: improveProfile.processes?.recombine?.minClusterSize,
|
|
658
|
+
maxClustersPerRun: improveProfile.processes?.recombine?.maxClustersPerRun,
|
|
659
|
+
relatednessSource: improveProfile.processes?.recombine?.relatednessSource,
|
|
660
|
+
confirmThreshold: improveProfile.processes?.recombine?.confirmThreshold,
|
|
661
|
+
// #632 — clustering-tuning knobs. UNSET = pre-#632 behaviour.
|
|
662
|
+
maxClusterSize: improveProfile.processes?.recombine?.maxClusterSize,
|
|
663
|
+
excludeTags: improveProfile.processes?.recombine?.excludeTags,
|
|
664
|
+
excludeEntities: improveProfile.processes?.recombine?.excludeEntities,
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
catch (e) {
|
|
668
|
+
allWarnings.push(`recombine: ${String(e)}`);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
// #615 — procedural-compilation pass. Detects recurring successful ordered
|
|
672
|
+
// action sequences and compiles them into workflow proposals. Opt-in: gated
|
|
673
|
+
// on the `procedural` process being enabled, whole-stash / type scope (never
|
|
674
|
+
// `ref`), and not a dry run. Mirrors the recombine opt-in wiring.
|
|
675
|
+
let proceduralCompilation;
|
|
676
|
+
if (primaryStashDir &&
|
|
677
|
+
improveProfile &&
|
|
678
|
+
resolveProcessEnabled("procedural", improveProfile) &&
|
|
679
|
+
scope.mode !== "ref" &&
|
|
680
|
+
!options.dryRun) {
|
|
681
|
+
const proceduralFn = options.proceduralFn ?? akmProcedural;
|
|
682
|
+
try {
|
|
683
|
+
proceduralCompilation = await proceduralFn({
|
|
684
|
+
stashDir: primaryStashDir,
|
|
685
|
+
config: options.config ?? loadConfig(),
|
|
686
|
+
...(improveProfile ? { improveProfile } : {}),
|
|
687
|
+
...(options.runId ? { sourceRun: options.runId } : {}),
|
|
688
|
+
...(budgetSignal ? { signal: budgetSignal } : {}),
|
|
689
|
+
eligibilitySource: "procedural",
|
|
690
|
+
...(eventsCtx ? { ctx: eventsCtx } : {}),
|
|
691
|
+
minRecurrence: improveProfile.processes?.procedural?.minRecurrence,
|
|
692
|
+
maxProposalsPerRun: improveProfile.processes?.procedural?.maxProposalsPerRun,
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
catch (e) {
|
|
696
|
+
allWarnings.push(`procedural: ${String(e)}`);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
// ── R5: collapse/churn detector ────────────────────────────────────────────
|
|
700
|
+
// One snapshot per QUALIFYING cycle: consolidate processed work and/or
|
|
701
|
+
// recombine formed clusters. Runs AFTER the maintenance reindex so FTS sees
|
|
702
|
+
// the post-merge index; one call site covers both passes. Deterministic,
|
|
703
|
+
// observe-only, fail-open (the orchestrator catches everything) — and inert
|
|
704
|
+
// on the ~9-in-10 default-profile runs that touch no merges.
|
|
705
|
+
let cycleMetrics;
|
|
706
|
+
const recombineWorked = (recombination?.clustersFormed ?? 0) > 0;
|
|
707
|
+
if (!options.dryRun && (consolidationRan || recombineWorked)) {
|
|
708
|
+
cycleMetrics = runCollapseDetector({
|
|
709
|
+
runId: options.runId ?? "improve-adhoc",
|
|
710
|
+
...(improveProfile ? { improveProfile } : {}),
|
|
711
|
+
pass: consolidationRan && recombineWorked ? "both" : consolidationRan ? "consolidate" : "recombine",
|
|
712
|
+
// prep+loop gate accepts, PLUS recombine's confirmed-lesson promotions —
|
|
713
|
+
// recombine churn is the historically observed failure mode and its
|
|
714
|
+
// promotions never flow through the prep/loop gates.
|
|
715
|
+
acceptedActions: (args.acceptedActions ?? 0) + (recombination?.lessonsPromoted ?? 0),
|
|
716
|
+
mergeFloorViolations: args.consolidationMergeFloorViolations ?? 0,
|
|
717
|
+
config: options.config ?? loadConfig(),
|
|
718
|
+
...(eventsCtx ? { eventsCtx } : {}),
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
return {
|
|
722
|
+
allWarnings,
|
|
723
|
+
deadUrls,
|
|
724
|
+
...(cycleMetrics ? { cycleMetrics } : {}),
|
|
725
|
+
...(recombination ? { recombination } : {}),
|
|
726
|
+
...(proceduralCompilation ? { proceduralCompilation } : {}),
|
|
727
|
+
...(maintenanceResult.memoryInference ? { memoryInference: maintenanceResult.memoryInference } : {}),
|
|
728
|
+
...(maintenanceResult.graphExtraction ? { graphExtraction: maintenanceResult.graphExtraction } : {}),
|
|
729
|
+
...(maintenanceResult.actions && maintenanceResult.actions.length > 0
|
|
730
|
+
? { maintenanceActions: maintenanceResult.actions }
|
|
731
|
+
: {}),
|
|
732
|
+
memoryInferenceDurationMs: maintenanceResult.memoryInferenceDurationMs,
|
|
733
|
+
graphExtractionDurationMs: maintenanceResult.graphExtractionDurationMs,
|
|
734
|
+
orphansPurged: maintenanceResult.orphansPurged,
|
|
735
|
+
proposalsExpired: maintenanceResult.proposalsExpired,
|
|
736
|
+
// Consolidation's auto-accept gate counts now accrue in the preparation
|
|
737
|
+
// stage (#551); post-loop no longer runs an auto-accept gate of its own.
|
|
738
|
+
gateAutoAcceptedCount: 0,
|
|
739
|
+
gateAutoAcceptFailedCount: 0,
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
// TODO(refactor): mutates the passed-in `allWarnings` array as a hidden side channel. Return warnings in ImproveMaintenanceResult and merge in caller — invasive signature change deferred to next refactor pass.
|
|
743
|
+
// Exported for tests (#584/#585 DB-locking regression coverage); production
|
|
744
|
+
// callers reach it only through akmImprove → runImprovePostLoopStage.
|
|
745
|
+
// TODO(refactor): mutates the passed-in `allWarnings` array as a hidden side channel. Return warnings in ImproveMaintenanceResult and merge in caller — invasive signature change deferred to next refactor pass.
|
|
746
|
+
// Exported for tests (#584/#585 DB-locking regression coverage); production
|
|
747
|
+
// callers reach it only through akmImprove → runImprovePostLoopStage.
|
|
748
|
+
export async function runImproveMaintenancePasses(args) {
|
|
749
|
+
const { options, primaryStashDir, memoryRefsForInference, allWarnings, reindexFn, consolidationRan, budgetSignal, eventsCtx, improveProfile, } = args;
|
|
750
|
+
if (!primaryStashDir)
|
|
751
|
+
return { memoryInferenceDurationMs: 0, graphExtractionDurationMs: 0 };
|
|
752
|
+
const config = options.config ?? loadConfig();
|
|
753
|
+
const sources = resolveSourceEntries(options.stashDir, config);
|
|
754
|
+
const memoryInferenceFn = options.memoryInferenceFn ?? runMemoryInferencePass;
|
|
755
|
+
const graphExtractionFn = options.graphExtractionFn ?? runGraphExtractionPass;
|
|
756
|
+
let db;
|
|
757
|
+
let memoryInference;
|
|
758
|
+
let graphExtraction;
|
|
759
|
+
let reindexedAfterInference = false;
|
|
760
|
+
const actions = [];
|
|
761
|
+
let memoryInferenceDurationMs = 0;
|
|
762
|
+
let graphExtractionDurationMs = 0;
|
|
763
|
+
let orphansPurged = 0;
|
|
764
|
+
let proposalsExpired = 0;
|
|
765
|
+
const openIndexDb = () => openIndexDatabase(getDbPath(), config.embedding?.dimension ? { embeddingDim: config.embedding.dimension } : undefined);
|
|
766
|
+
// #584: reindexFn opens its own write handle on the same index.db WAL file.
|
|
767
|
+
// Holding our handle across that call produced SQLITE_BUSY / "database is
|
|
768
|
+
// locked" failures in production, so the handle is closed BEFORE every
|
|
769
|
+
// reindex and reopened after — the fresh handle also sees the post-reindex
|
|
770
|
+
// state that graph extraction below relies on. The
|
|
771
|
+
// reopen runs in `finally` so a failed reindex still leaves a usable handle.
|
|
772
|
+
const reindexWithIndexDbReleased = async (stashDir) => {
|
|
773
|
+
if (db) {
|
|
774
|
+
closeDatabase(db);
|
|
775
|
+
db = undefined;
|
|
776
|
+
}
|
|
777
|
+
try {
|
|
778
|
+
await reindexFn({ stashDir });
|
|
779
|
+
}
|
|
780
|
+
finally {
|
|
781
|
+
db = openIndexDb();
|
|
782
|
+
}
|
|
783
|
+
};
|
|
784
|
+
await withIndexWriterLease({ purpose: "improve-maintenance", signal: budgetSignal }, async () => {
|
|
785
|
+
try {
|
|
786
|
+
db = openIndexDb();
|
|
787
|
+
// Memory inference candidate-discovery (post-Item 9 fix from
|
|
788
|
+
// memory:akm-improve-critical-review-2026-05-20). Previously this pass
|
|
789
|
+
// was gated on memoryRefsForInference.size > 0 AND passed those refs as a
|
|
790
|
+
// candidateRefs filter. But memoryRefsForInference is populated from refs
|
|
791
|
+
// distilled THIS RUN — by the time that happens, those parents are
|
|
792
|
+
// already split (`inferenceProcessed: true`) and `isPendingMemory` excludes
|
|
793
|
+
// them. The genuinely-pending parents in the stash never entered the
|
|
794
|
+
// filter. Result: 0/0/0 for 25 consecutive runs.
|
|
795
|
+
//
|
|
796
|
+
// Fix: always run the pass when the feature is enabled; let the pass's
|
|
797
|
+
// own `collectPendingMemories` + `isPendingMemory` predicate find
|
|
798
|
+
// candidates from the filesystem-of-truth. The this-run set is still
|
|
799
|
+
// logged as a hint but no longer used as a filter.
|
|
800
|
+
const memoryInferenceDisabledByProfile = improveProfile?.processes?.memoryInference?.enabled === false;
|
|
801
|
+
const minPendingCount = improveProfile?.processes?.memoryInference?.minPendingCount;
|
|
802
|
+
const pendingBelowMinCount = (() => {
|
|
803
|
+
if (!primaryStashDir || minPendingCount === undefined || minPendingCount <= 0)
|
|
804
|
+
return false;
|
|
805
|
+
const pending = collectPendingMemories(primaryStashDir).length;
|
|
806
|
+
if (pending < minPendingCount) {
|
|
807
|
+
info(`[improve] memory inference skipped (${pending} pending < minPendingCount ${minPendingCount})`);
|
|
808
|
+
return true;
|
|
809
|
+
}
|
|
810
|
+
return false;
|
|
811
|
+
})();
|
|
812
|
+
if (memoryInferenceDisabledByProfile) {
|
|
813
|
+
info("[improve] memory inference skipped (disabled by improve profile)");
|
|
814
|
+
}
|
|
815
|
+
else if (pendingBelowMinCount) {
|
|
816
|
+
// skipped — message already emitted above
|
|
817
|
+
}
|
|
818
|
+
else {
|
|
819
|
+
const hintRefs = memoryRefsForInference.size;
|
|
820
|
+
info(hintRefs > 0
|
|
821
|
+
? `[improve] memory inference starting (${hintRefs} hint refs touched this run; pass discovers all pending)`
|
|
822
|
+
: "[improve] memory inference starting (discovering pending parents)");
|
|
823
|
+
const inferenceStart = Date.now();
|
|
824
|
+
try {
|
|
825
|
+
// O-1 (#364): pass budget signal so a hung inference call is cancelled.
|
|
826
|
+
memoryInference = await withLlmStage("memory-inference", () => memoryInferenceFn({
|
|
827
|
+
config,
|
|
828
|
+
sources,
|
|
829
|
+
signal: budgetSignal,
|
|
830
|
+
db,
|
|
831
|
+
reEnrich: false,
|
|
832
|
+
onProgress: (event) => {
|
|
833
|
+
const current = event.currentRef ? ` ${event.currentRef}` : "";
|
|
834
|
+
info(`[improve] memory inference ${event.processed}/${event.total}${current} (written ${event.writtenFacts}, skipped ${event.skippedNoFacts})`);
|
|
835
|
+
},
|
|
836
|
+
}));
|
|
837
|
+
memoryInferenceDurationMs = Date.now() - inferenceStart;
|
|
838
|
+
actions.push({ ref: "memory:_inference", mode: "memory-inference", result: memoryInference });
|
|
839
|
+
info(`[improve] memory inference complete (${memoryInference.writtenFacts} facts written from ${memoryInference.splitParents} parents)`);
|
|
840
|
+
}
|
|
841
|
+
catch (err) {
|
|
842
|
+
memoryInferenceDurationMs = Date.now() - inferenceStart;
|
|
843
|
+
allWarnings.push(`memory inference failed: ${errMessage(err)}`);
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
if (memoryInference && (memoryInference.splitParents > 0 || memoryInference.writtenFacts > 0)) {
|
|
847
|
+
info("[improve] reindexing after memory inference writes");
|
|
848
|
+
try {
|
|
849
|
+
await reindexWithIndexDbReleased(primaryStashDir);
|
|
850
|
+
reindexedAfterInference = true;
|
|
851
|
+
info("[improve] reindex after memory inference complete");
|
|
852
|
+
}
|
|
853
|
+
catch (err) {
|
|
854
|
+
allWarnings.push(`reindex after memory inference failed: ${errMessage(err)}`);
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
const graphEnabled = isProcessEnabled("index", "graph_extraction", config);
|
|
858
|
+
const graphExtractionDisabledByProfile = improveProfile?.processes?.graphExtraction?.enabled === false;
|
|
859
|
+
const graphExtractionFullScan = improveProfile?.processes?.graphExtraction?.fullScan === true;
|
|
860
|
+
// #624 P2: optional incremental high-signal-first cap. Unset = process all
|
|
861
|
+
// eligible (byte-identical to today; no ranking/slice).
|
|
862
|
+
const graphExtractionTopN = improveProfile?.processes?.graphExtraction?.topN;
|
|
863
|
+
// Build the set of refs actually touched this run.
|
|
864
|
+
const touchedRefs = new Set();
|
|
865
|
+
for (const r of args.actionableRefs)
|
|
866
|
+
touchedRefs.add(r.ref);
|
|
867
|
+
for (const r of memoryRefsForInference)
|
|
868
|
+
touchedRefs.add(r);
|
|
869
|
+
// INVARIANT: graph extraction normally runs only on files touched by
|
|
870
|
+
// actionable refs (candidatePaths). Full-corpus scans are opt-in via
|
|
871
|
+
// profile.processes.graphExtraction.fullScan = true (used by the
|
|
872
|
+
// `graph-refresh` built-in profile and its weekly scheduled task).
|
|
873
|
+
// The empty-Set fallback is intentional when no refs were touched —
|
|
874
|
+
// the extractor's filter rejects every file and returns empty, keeping
|
|
875
|
+
// the pass invoked so the action is recorded and tests stay exercised.
|
|
876
|
+
if (graphExtractionDisabledByProfile) {
|
|
877
|
+
info("[improve] graph extraction skipped (disabled by improve profile)");
|
|
878
|
+
}
|
|
879
|
+
else if (sources.length > 0 && graphEnabled) {
|
|
880
|
+
info(`[improve] graph extraction starting${graphExtractionFullScan ? " (full-corpus scan)" : ""}`);
|
|
881
|
+
const extractionStart = Date.now();
|
|
882
|
+
try {
|
|
883
|
+
// D9: if consolidation ran but memory inference did not reindex, force a reindex
|
|
884
|
+
// so graph extraction sees current DB state after consolidation writes.
|
|
885
|
+
if (consolidationRan && !reindexedAfterInference) {
|
|
886
|
+
info("[improve] reindexing after consolidation (graph extraction needs current state)");
|
|
887
|
+
try {
|
|
888
|
+
await reindexWithIndexDbReleased(primaryStashDir);
|
|
889
|
+
reindexedAfterInference = true;
|
|
890
|
+
info("[improve] reindex after consolidation complete");
|
|
891
|
+
}
|
|
892
|
+
catch (err) {
|
|
893
|
+
allWarnings.push(`reindex after consolidation failed: ${errMessage(err)}`);
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
// #584: no close/reopen needed here — reindexWithIndexDbReleased
|
|
897
|
+
// already swapped in a fresh post-reindex handle.
|
|
898
|
+
// Resolve touched refs to absolute file paths. Skipped for fullScan
|
|
899
|
+
// (candidatePaths stays undefined → extractor processes all files).
|
|
900
|
+
let candidatePaths;
|
|
901
|
+
if (!graphExtractionFullScan) {
|
|
902
|
+
candidatePaths = new Set();
|
|
903
|
+
if (primaryStashDir && touchedRefs.size > 0) {
|
|
904
|
+
const writableDirSet = new Set(getWritableStashDirs(primaryStashDir).map((d) => path.resolve(d)));
|
|
905
|
+
const resolved = await Promise.all([...touchedRefs].map((ref) => findAssetFilePath(ref, primaryStashDir, writableDirSet).catch(() => null)));
|
|
906
|
+
for (const p of resolved) {
|
|
907
|
+
if (typeof p === "string" && p.length > 0)
|
|
908
|
+
candidatePaths.add(p);
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
const progressHandler = (event) => {
|
|
913
|
+
const current = event.currentPath ? ` ${path.basename(event.currentPath)}` : "";
|
|
914
|
+
info(`[improve] graph extraction ${event.processed}/${event.total}${current} (extracted ${event.extracted}, entities ${event.totalEntities}, relations ${event.totalRelations})`);
|
|
915
|
+
};
|
|
916
|
+
// O-1 (#364): pass budget signal so a hung graph extraction call is cancelled.
|
|
917
|
+
graphExtraction = await withLlmStage("graph-extraction", () => graphExtractionFn({
|
|
918
|
+
config,
|
|
919
|
+
sources,
|
|
920
|
+
signal: budgetSignal,
|
|
921
|
+
db,
|
|
922
|
+
reEnrich: false,
|
|
923
|
+
onProgress: progressHandler,
|
|
924
|
+
options: { candidatePaths, ...(graphExtractionTopN != null ? { topN: graphExtractionTopN } : {}) },
|
|
925
|
+
}));
|
|
926
|
+
graphExtractionDurationMs = Date.now() - extractionStart;
|
|
927
|
+
actions.push({ ref: "graph:_artifact", mode: "graph-extraction", result: graphExtraction });
|
|
928
|
+
info(`[improve] graph extraction complete (${graphExtraction.quality.extractedFiles} files, ${graphExtraction.quality.entityCount} entities, ${graphExtraction.quality.relationCount} relations)`);
|
|
929
|
+
}
|
|
930
|
+
catch (err) {
|
|
931
|
+
graphExtractionDurationMs = Date.now() - extractionStart;
|
|
932
|
+
allWarnings.push(`graph extraction failed: ${errMessage(err)}`);
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
else if (sources.length > 0 && !graphEnabled) {
|
|
936
|
+
info("[improve] graph extraction skipped (features.index.graph_extraction is disabled)");
|
|
937
|
+
}
|
|
938
|
+
// Orphan proposal purge — reject pending reflect proposals whose target
|
|
939
|
+
// asset no longer exists on disk. Runs after graph extraction so newly
|
|
940
|
+
// promoted assets from accept flows during this run are already present.
|
|
941
|
+
if (primaryStashDir) {
|
|
942
|
+
try {
|
|
943
|
+
const purgeResult = purgeOrphanProposals(primaryStashDir, sources.map((s) => s.path));
|
|
944
|
+
orphansPurged = purgeResult.rejected;
|
|
945
|
+
if (purgeResult.rejected > 0) {
|
|
946
|
+
info(`[improve] orphan purge: ${purgeResult.rejected}/${purgeResult.checked} orphaned proposals rejected (${purgeResult.durationMs}ms)`);
|
|
947
|
+
}
|
|
948
|
+
appendEvent({
|
|
949
|
+
eventType: "proposal_orphan_purge",
|
|
950
|
+
ref: "proposals:_orphan-purge",
|
|
951
|
+
metadata: {
|
|
952
|
+
checked: purgeResult.checked,
|
|
953
|
+
rejected: purgeResult.rejected,
|
|
954
|
+
durationMs: purgeResult.durationMs,
|
|
955
|
+
byType: purgeResult.byType,
|
|
956
|
+
orphans: purgeResult.orphans.map((o) => o.ref),
|
|
957
|
+
},
|
|
958
|
+
}, eventsCtx);
|
|
959
|
+
}
|
|
960
|
+
catch (err) {
|
|
961
|
+
allWarnings.push(`orphan purge failed: ${errMessage(err)}`);
|
|
962
|
+
}
|
|
963
|
+
// Phase 6B (Advantage D6b): expire pending proposals that have aged past
|
|
964
|
+
// the retention window. Runs AFTER orphan purge so we never double-archive
|
|
965
|
+
// a proposal that orphan-purge already moved. `expireStaleProposals` emits
|
|
966
|
+
// its own per-proposal `proposal_expired` events; we additionally emit a
|
|
967
|
+
// single roll-up event here for parity with the orphan-purge surface.
|
|
968
|
+
try {
|
|
969
|
+
const expireResult = expireStaleProposals(primaryStashDir, config);
|
|
970
|
+
proposalsExpired = expireResult.expired;
|
|
971
|
+
if (expireResult.expired > 0) {
|
|
972
|
+
info(`[improve] expiration: ${expireResult.expired}/${expireResult.checked} pending proposals expired ` +
|
|
973
|
+
`(retention=${expireResult.retentionDays}d, ${expireResult.durationMs}ms)`);
|
|
974
|
+
}
|
|
975
|
+
appendEvent({
|
|
976
|
+
eventType: "proposal_expiration_pass",
|
|
977
|
+
ref: "proposals:_expiration",
|
|
978
|
+
metadata: {
|
|
979
|
+
checked: expireResult.checked,
|
|
980
|
+
expired: expireResult.expired,
|
|
981
|
+
durationMs: expireResult.durationMs,
|
|
982
|
+
retentionDays: expireResult.retentionDays,
|
|
983
|
+
expiredProposals: expireResult.expiredProposals,
|
|
984
|
+
},
|
|
985
|
+
}, eventsCtx);
|
|
986
|
+
}
|
|
987
|
+
catch (err) {
|
|
988
|
+
allWarnings.push(`proposal expiration failed: ${errMessage(err)}`);
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
// Fix #2 (observability 0.8.0): trim the events table in state.db so it
|
|
992
|
+
// doesn't grow unbounded. `akm health` writes a `health_probe` row on every
|
|
993
|
+
// invocation, and every command surface emits at least one event besides —
|
|
994
|
+
// without this trim, state.db is a permanent append-only log. Config key
|
|
995
|
+
// `improve.eventRetentionDays` (default 90, set 0 to disable) controls the
|
|
996
|
+
// window. The purge runs against state.db (a different SQLite file from
|
|
997
|
+
// the index `db` above).
|
|
998
|
+
{
|
|
999
|
+
const retentionDays = typeof config.improve?.eventRetentionDays === "number" ? config.improve.eventRetentionDays : 90;
|
|
1000
|
+
if (retentionDays > 0) {
|
|
1001
|
+
// #585: reuse the long-lived eventsCtx.db connection when akmImprove
|
|
1002
|
+
// opened one — opening a second state.db write connection while
|
|
1003
|
+
// eventsDb is still live made two simultaneous writers contend on the
|
|
1004
|
+
// same WAL file ("database is locked"). Only the eventsCtx.dbPath
|
|
1005
|
+
// fallback path (state.db failed to open up-front) opens — and then
|
|
1006
|
+
// owns and closes — its own handle. C2 still holds: the fallback uses
|
|
1007
|
+
// the boundary-pinned path, never a live `process.env` re-read.
|
|
1008
|
+
try {
|
|
1009
|
+
withStateDb((stateDb) => {
|
|
1010
|
+
const purgedCount = purgeOldEvents(stateDb, retentionDays);
|
|
1011
|
+
if (purgedCount > 0) {
|
|
1012
|
+
info(`[improve] events purge: ${purgedCount} event(s) older than ${retentionDays}d removed from state.db`);
|
|
1013
|
+
}
|
|
1014
|
+
appendEvent({
|
|
1015
|
+
eventType: "events_purged",
|
|
1016
|
+
ref: "events:_purge",
|
|
1017
|
+
metadata: { purgedCount, retentionDays },
|
|
1018
|
+
}, eventsCtx);
|
|
1019
|
+
// improve_runs uses the same retention window as events — both are
|
|
1020
|
+
// observability/audit data, both grow append-only, both have a
|
|
1021
|
+
// dedicated purge helper. Mirroring the events purge here means a
|
|
1022
|
+
// single retention knob (improve.eventRetentionDays) governs both.
|
|
1023
|
+
const improveRunsPurged = purgeOldImproveRuns(stateDb, retentionDays);
|
|
1024
|
+
if (improveRunsPurged > 0) {
|
|
1025
|
+
info(`[improve] improve_runs purge: ${improveRunsPurged} run(s) older than ${retentionDays}d removed from state.db`);
|
|
1026
|
+
}
|
|
1027
|
+
appendEvent({
|
|
1028
|
+
eventType: "improve_runs_purged",
|
|
1029
|
+
ref: "improve_runs:_purge",
|
|
1030
|
+
metadata: { purgedCount: improveRunsPurged, retentionDays },
|
|
1031
|
+
}, eventsCtx);
|
|
1032
|
+
// R5: improve_cycle_metrics has its OWN retention window
|
|
1033
|
+
// (default 365d — a slow collapse needs a longer trend than
|
|
1034
|
+
// the 90d events window). canary_queries rows are never purged.
|
|
1035
|
+
const cycleRetention = config.improve?.collapseDetector?.retentionDays ?? CYCLE_METRICS_RETENTION_DAYS;
|
|
1036
|
+
const cycleMetricsPurged = purgeOldCycleMetrics(stateDb, cycleRetention);
|
|
1037
|
+
if (cycleMetricsPurged > 0) {
|
|
1038
|
+
info(`[improve] cycle-metrics purge: ${cycleMetricsPurged} row(s) older than ${cycleRetention}d removed from state.db`);
|
|
1039
|
+
appendEvent({
|
|
1040
|
+
// Dedicated type (mirrors improve_runs_purged) so consumers
|
|
1041
|
+
// never have to disambiguate purge targets via the ref string.
|
|
1042
|
+
eventType: "improve_cycle_metrics_purged",
|
|
1043
|
+
ref: "improve_cycle_metrics:_purge",
|
|
1044
|
+
metadata: { purgedCount: cycleMetricsPurged, retentionDays: cycleRetention },
|
|
1045
|
+
}, eventsCtx);
|
|
1046
|
+
}
|
|
1047
|
+
}, { path: eventsCtx?.dbPath, borrowed: eventsCtx?.db });
|
|
1048
|
+
}
|
|
1049
|
+
catch (err) {
|
|
1050
|
+
allWarnings.push(`events purge failed: ${errMessage(err)}`);
|
|
1051
|
+
}
|
|
1052
|
+
// task_logs in logs.db (#579) shares the same retention window as
|
|
1053
|
+
// events/improve_runs — all three are observability data governed by
|
|
1054
|
+
// the single improve.eventRetentionDays knob. Separate try/finally
|
|
1055
|
+
// because logs.db is a different file: a locked/missing logs.db must
|
|
1056
|
+
// not block the state.db purges above.
|
|
1057
|
+
let logsDb;
|
|
1058
|
+
try {
|
|
1059
|
+
logsDb = openLogsDatabase();
|
|
1060
|
+
const taskLogsPurged = purgeOldTaskLogs(logsDb, retentionDays);
|
|
1061
|
+
if (taskLogsPurged > 0) {
|
|
1062
|
+
info(`[improve] task_logs purge: ${taskLogsPurged} log line(s) older than ${retentionDays}d removed from logs.db`);
|
|
1063
|
+
}
|
|
1064
|
+
appendEvent({
|
|
1065
|
+
eventType: "task_logs_purged",
|
|
1066
|
+
ref: "task_logs:_purge",
|
|
1067
|
+
metadata: { purgedCount: taskLogsPurged, retentionDays },
|
|
1068
|
+
}, eventsCtx);
|
|
1069
|
+
}
|
|
1070
|
+
catch (err) {
|
|
1071
|
+
allWarnings.push(`task_logs purge failed: ${errMessage(err)}`);
|
|
1072
|
+
}
|
|
1073
|
+
finally {
|
|
1074
|
+
if (logsDb) {
|
|
1075
|
+
try {
|
|
1076
|
+
logsDb.close();
|
|
1077
|
+
}
|
|
1078
|
+
catch {
|
|
1079
|
+
// best-effort
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
finally {
|
|
1087
|
+
if (db)
|
|
1088
|
+
closeDatabase(db);
|
|
1089
|
+
}
|
|
1090
|
+
});
|
|
1091
|
+
return {
|
|
1092
|
+
...(memoryInference ? { memoryInference } : {}),
|
|
1093
|
+
...(graphExtraction ? { graphExtraction } : {}),
|
|
1094
|
+
...(actions.length > 0 ? { actions } : {}),
|
|
1095
|
+
memoryInferenceDurationMs,
|
|
1096
|
+
graphExtractionDurationMs,
|
|
1097
|
+
orphansPurged,
|
|
1098
|
+
proposalsExpired,
|
|
1099
|
+
};
|
|
1100
|
+
}
|