akm-cli 0.9.17-alpha.3 → 0.9.17-alpha.4
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 +731 -0
- package/dist/akm +94 -196
- package/dist/cli/shared.js +6 -2
- package/dist/cli.js +22 -9
- package/dist/commands/agent/agent-dispatch.js +1 -1
- package/dist/commands/command/command-execution.js +24 -62
- package/dist/commands/feedback-cli.js +0 -1
- package/dist/commands/health/accept-rate.js +2 -2
- package/dist/commands/health/checks.js +30 -75
- package/dist/commands/health/config-skew.js +38 -0
- package/dist/commands/health/egress.js +54 -0
- package/dist/commands/health/html-report.js +0 -38
- package/dist/commands/health/improve-metrics.js +123 -562
- package/dist/commands/health/plugin-staleness.js +53 -3
- package/dist/commands/health/renderers.js +12 -4
- package/dist/commands/health/report-view-model.js +11 -106
- package/dist/commands/health/types-improve.js +4 -19
- package/dist/commands/health/windows.js +64 -73
- package/dist/commands/health.js +122 -143
- package/dist/commands/improve/consolidate/chunking.js +25 -100
- package/dist/commands/improve/consolidate/sanitize.js +54 -149
- package/dist/commands/improve/consolidate.js +538 -1075
- package/dist/commands/improve/content-hash.js +16 -24
- package/dist/commands/improve/distill/content-repair.js +18 -100
- package/dist/commands/improve/distill-guards.js +20 -81
- package/dist/commands/improve/distill-promotion-policy.js +23 -243
- package/dist/commands/improve/distill.js +608 -1075
- package/dist/commands/improve/eligibility.js +126 -400
- package/dist/commands/improve/execution.js +3 -5
- package/dist/commands/improve/extract.js +487 -1046
- package/dist/commands/improve/feedback-valence.js +0 -25
- package/dist/commands/improve/improve-cli.js +29 -166
- package/dist/commands/improve/improve-result-file.js +10 -66
- package/dist/commands/improve/improve-strategies.js +12 -7
- package/dist/commands/improve/improve-usage-report.js +18 -64
- package/dist/commands/improve/improve.js +443 -1063
- package/dist/commands/improve/ledger.js +114 -0
- package/dist/commands/improve/locks.js +2 -8
- package/dist/commands/improve/loop-stages.js +459 -1172
- package/dist/commands/improve/memory/derived-ref.js +12 -77
- package/dist/commands/improve/memory/memory-belief.js +14 -118
- package/dist/commands/improve/memory/memory-improve.js +4 -3
- package/dist/commands/improve/outcome-loop.js +28 -156
- package/dist/commands/improve/planner.js +5 -10
- package/dist/commands/improve/preparation.js +851 -2339
- package/dist/commands/improve/proactive-maintenance.js +34 -101
- package/dist/commands/improve/reflect-noise.js +104 -280
- package/dist/commands/improve/reflect.js +621 -1367
- package/dist/commands/improve/salience.js +46 -232
- package/dist/commands/improve/session-asset.js +19 -100
- package/dist/commands/improve/stage.js +323 -0
- package/dist/commands/proposal/drain.js +251 -644
- package/dist/commands/proposal/proposal-cli.js +3 -18
- package/dist/commands/proposal/proposal-types.js +20 -41
- package/dist/commands/proposal/proposal.js +1 -2
- package/dist/commands/proposal/propose.js +134 -160
- package/dist/commands/proposal/repository.js +502 -1487
- package/dist/commands/proposal/validators/proposal-quality-validators.js +71 -174
- package/dist/commands/proposal/validators/proposal-validators.js +1 -1
- package/dist/commands/proposal/validators/proposals.js +13 -89
- package/dist/commands/read/curate.js +63 -413
- package/dist/commands/read/search-cli.js +16 -33
- package/dist/commands/read/search.js +17 -23
- package/dist/commands/read/show.js +2 -13
- package/dist/commands/sources/bundle-cli.js +25 -2
- package/dist/commands/sources/bundle-config-ops.js +7 -0
- package/dist/commands/sources/dangerous-env-audit.js +1 -2
- package/dist/commands/sources/info.js +2 -11
- package/dist/commands/sources/installed-stashes.js +197 -746
- package/dist/commands/sources/schema-repair.js +98 -129
- package/dist/commands/sources/source-add.js +62 -12
- package/dist/commands/sources/stash-cli.js +1 -1
- package/dist/commands/tasks/explain.js +10 -13
- package/dist/commands/tasks/tasks-cli.js +9 -8
- package/dist/commands/tasks/tasks.js +326 -930
- package/dist/commands/tasks/validate.js +42 -21
- package/dist/commands/workflow/plan.js +22 -29
- package/dist/commands/workflow-cli.js +4 -4
- package/dist/core/adapter/adapters/akm-adapter.js +0 -1
- package/dist/core/adapter/adapters/akm-lint.js +2 -3
- package/dist/core/adapter/adapters/akm-metadata.js +11 -12
- package/dist/core/adapter/adapters/akm-workflow-adapter.js +1 -1
- package/dist/core/adapter/execution-source.js +17 -29
- package/dist/core/asset/resolve-ref.js +1 -1
- package/dist/core/bundle-id.js +42 -5
- package/dist/core/bundle-rename.js +291 -0
- package/dist/core/config/config-io.js +1 -2
- package/dist/core/config/config-schema.js +1 -33
- package/dist/core/config/config-walker.js +1 -1
- package/dist/core/config/config.js +163 -68
- package/dist/core/config/legacy-source-shape-shim.js +38 -9
- package/dist/core/config/schema/embedding.js +20 -5
- package/dist/core/config/schema/engines.js +5 -0
- package/dist/core/config/schema/execution.js +1 -1
- package/dist/core/config/schema/experimental.js +1 -1
- package/dist/core/config/schema/improve-processes.js +21 -95
- package/dist/core/config/schema/improve.js +4 -42
- package/dist/core/config/schema/scheduler.js +12 -12
- package/dist/core/config/schema/search.js +6 -22
- package/dist/core/env-secret-ref.js +0 -1
- package/dist/core/errors.js +8 -9
- package/dist/core/file-lock.js +76 -173
- package/dist/core/logs-db.js +2 -2
- package/dist/core/paths.js +0 -27
- package/dist/core/redaction.js +109 -2
- package/dist/core/run-lock.js +2 -5
- package/dist/core/spawn-env.js +1 -1
- package/dist/core/state/migrations.js +108 -61
- package/dist/core/state-db-scope.js +2 -4
- package/dist/core/state-db.js +126 -692
- package/dist/core/type-presentation.js +1 -9
- package/dist/core/write-source.js +293 -1012
- package/dist/execution/input-contract.js +1 -1
- package/dist/execution/resolved-request.js +135 -689
- package/dist/execution/source.js +63 -257
- package/dist/execution/target-ref.js +1 -1
- package/dist/indexer/bundle-identity-guard.js +2 -2
- package/dist/indexer/db/graph-db.js +106 -46
- package/dist/indexer/ensure-index.js +44 -85
- package/dist/indexer/graph/graph-extraction.js +340 -562
- package/dist/indexer/graph/graph-related.js +130 -0
- package/dist/indexer/index-rebuild-lock.js +3 -11
- package/dist/indexer/index-writer-lock.js +8 -17
- package/dist/indexer/index-written-assets.js +139 -151
- package/dist/indexer/indexer.js +524 -846
- package/dist/indexer/materialize-embeddings.js +60 -397
- package/dist/indexer/passes/memory-inference.js +81 -90
- package/dist/indexer/passes/metadata.js +132 -200
- package/dist/indexer/read-preflight.js +0 -7
- package/dist/indexer/scan/doc-to-entry.js +1 -3
- package/dist/indexer/scan/drain-dir.js +1 -1
- package/dist/indexer/search/db-search.js +181 -590
- package/dist/indexer/search/fts-query.js +30 -41
- package/dist/indexer/search/ranking.js +28 -154
- package/dist/indexer/search/search-attribution.js +12 -32
- package/dist/indexer/search/search-fields.js +11 -15
- package/dist/indexer/search/search-hit-enrichers.js +54 -85
- package/dist/indexer/search/search-source.js +1 -4
- package/dist/indexer/usage/usage-events.js +2 -7
- package/dist/integrations/agent/engine-fallback.js +23 -40
- package/dist/integrations/agent/engine-resolution.js +93 -183
- package/dist/integrations/agent/execution.js +507 -0
- package/dist/integrations/agent/model-map.js +28 -156
- package/dist/integrations/agent/request-lowering.js +66 -141
- package/dist/integrations/agent/runner-dispatch.js +143 -321
- package/dist/integrations/agent/runner.js +54 -14
- package/dist/integrations/lockfile.js +53 -101
- package/dist/llm/embedders/deterministic.js +2 -3
- package/dist/llm/embedders/profile.js +71 -0
- package/dist/llm/embedders/remote.js +10 -15
- package/dist/llm/graph-extract.js +3 -12
- package/dist/llm/index-passes.js +3 -5
- package/dist/llm/memory-infer.js +1 -2
- package/dist/llm/metadata-enhance.js +1 -2
- package/dist/llm/structured-call.js +5 -24
- package/dist/output/generic-render.js +23 -11
- package/dist/output/html-render.js +13 -10
- package/dist/output/render-registry.js +3 -32
- package/dist/output/shapes/helpers.js +2 -34
- package/dist/output/shapes/passthrough.js +1 -9
- package/dist/{indexer/search/ranking-types.js → output/text/bundle-rename.js} +4 -1
- package/dist/output/text/command-format.js +60 -23
- package/dist/output/text/helpers.js +1 -1
- package/dist/output/text/migrate.js +5 -14
- package/dist/output/text/proposal-format.js +1 -2
- package/dist/output/text/workflow-format.js +0 -32
- package/dist/output/text.js +2 -0
- package/dist/registry/factory.js +4 -19
- package/dist/registry/network.js +66 -220
- package/dist/registry/providers/index.js +0 -2
- package/dist/registry/providers/skills-sh.js +3 -14
- package/dist/registry/providers/static-index.js +24 -26
- package/dist/registry/resolve.js +55 -131
- package/dist/scripts/akm-migrate-node.js +43937 -93313
- package/dist/scripts/akm-migrate.js +43697 -93071
- package/dist/setup/registry-stash-loader.js +4 -13
- package/dist/setup/semantic-assets.js +3 -44
- package/dist/setup/setup.js +1 -1
- package/dist/setup/steps/tasks.js +25 -15
- package/dist/sources/provider-factory.js +17 -18
- package/dist/sources/providers/filesystem.js +2 -3
- package/dist/sources/providers/git-install.js +7 -1
- package/dist/sources/providers/git-provider.js +0 -3
- package/dist/sources/providers/git-stash.js +0 -17
- package/dist/sources/providers/npm.js +2 -4
- package/dist/sources/providers/provider-utils.js +5 -10
- package/dist/sources/providers/website.js +0 -2
- package/dist/sources/snapshot-fetchers/website-ingest.js +1 -1
- package/dist/sources/website-url.js +2 -2
- package/dist/storage/database.js +9 -35
- package/dist/storage/repositories/improve-ledger-repository.js +168 -0
- package/dist/storage/repositories/index-connection.js +34 -70
- package/dist/storage/repositories/index-entries-repository.js +69 -111
- package/dist/storage/repositories/index-entry-mapper.js +1 -2
- package/dist/storage/repositories/index-entry-schema.js +83 -269
- package/dist/storage/repositories/index-fts-repository.js +86 -256
- package/dist/storage/repositories/index-llm-cache-repository.js +17 -0
- package/dist/storage/repositories/index-meta-repository.js +6 -4
- package/dist/storage/repositories/index-schema.js +192 -220
- package/dist/storage/repositories/index-utility-repository.js +8 -29
- package/dist/storage/repositories/index-vec-repository.js +133 -414
- package/dist/storage/repositories/outcome-repository.js +2 -1
- package/dist/storage/repositories/proposals-repository.js +35 -0
- package/dist/storage/repositories/registry-index-cache-repository.js +100 -0
- package/dist/storage/repositories/task-history-repository.js +26 -4
- package/dist/storage/repositories/workflow-runs-repository.js +53 -244
- package/dist/storage/sqlite-migrations.js +136 -0
- package/dist/storage/sqlite-pragmas.js +11 -9
- package/dist/storage/sqlite-transaction.js +170 -0
- package/dist/storage/state-db-integrity.js +34 -27
- package/dist/tasks/activation-config.js +134 -62
- package/dist/tasks/backends/cron.js +129 -277
- package/dist/tasks/backends/exec-utils.js +2 -5
- package/dist/tasks/backends/launchd.js +125 -745
- package/dist/tasks/backends/schtasks.js +101 -620
- package/dist/tasks/prepare/prepare-support.js +5 -15
- package/dist/tasks/prepare/prepare.js +0 -2
- package/dist/tasks/resolve-akm-bin.js +20 -79
- package/dist/tasks/run/attempt-lifecycle.js +0 -1
- package/dist/tasks/scheduler-binding.js +18 -238
- package/dist/tasks/scheduler-invocation.js +52 -52
- package/dist/tasks/scheduler-lock.js +53 -0
- package/dist/tasks/scheduler-sync.js +361 -751
- package/dist/tasks/source/parse-task-source.js +160 -10
- package/dist/tasks/source/task-source-v3-frozen.js +3 -4
- package/dist/tasks/source/task-to-v4.js +2 -2
- package/dist/workflows/authoring/authoring.js +3 -12
- package/dist/workflows/compile.js +211 -0
- package/dist/workflows/concurrency-policy.js +13 -74
- package/dist/workflows/exec/child-invocation.js +3 -17
- package/dist/workflows/exec/child-workflow.js +32 -141
- package/dist/workflows/exec/dispatch-redaction.js +13 -53
- package/dist/workflows/exec/environment.js +98 -0
- package/dist/workflows/exec/exec-unit.js +33 -140
- package/dist/workflows/exec/frozen-judge.js +7 -59
- package/dist/workflows/exec/native-executor.js +82 -341
- package/dist/workflows/exec/param-secrets.js +29 -47
- package/dist/workflows/exec/run-workflow.js +154 -387
- package/dist/workflows/exec/scheduler.js +9 -36
- package/dist/workflows/exec/step-work.js +127 -430
- package/dist/workflows/exec/unit-dispatch.js +11 -63
- package/dist/workflows/exec/unit-writer.js +8 -52
- package/dist/workflows/exec/worktree.js +39 -273
- package/dist/workflows/freeze/child-output-references.js +4 -15
- package/dist/workflows/freeze/environment.js +99 -92
- package/dist/workflows/freeze/freeze.js +172 -0
- package/dist/workflows/freeze/step-values.js +19 -21
- package/dist/workflows/freeze/targets/child-workflow.js +23 -92
- package/dist/workflows/freeze/targets/command.js +10 -33
- package/dist/workflows/freeze/targets/script.js +5 -12
- package/dist/workflows/freeze/targets/shell.js +3 -6
- package/dist/workflows/freeze/targets/task.js +25 -80
- package/dist/workflows/freeze/task-bindings.js +20 -67
- package/dist/workflows/{source-ir/github-yaml.js → github-yaml.js} +88 -206
- package/dist/workflows/ir/params.js +6 -51
- package/dist/workflows/ir/plan-hash.js +2 -34
- package/dist/workflows/parser.js +140 -43
- package/dist/{commands/improve/consolidate/types.js → workflows/plan.js} +2 -1
- package/dist/workflows/renderer.js +36 -69
- package/dist/workflows/resource-limits.js +12 -120
- package/dist/workflows/runtime/agent-identity.js +8 -40
- package/dist/workflows/runtime/run-outputs.js +3 -6
- package/dist/workflows/runtime/run-plan.js +316 -0
- package/dist/workflows/runtime/runs.js +48 -200
- package/dist/workflows/runtime/workflow-asset-loader.js +24 -57
- package/dist/workflows/{source-ir/semantics.js → source-semantics.js} +16 -20
- package/dist/workflows/validate-summary.js +2 -7
- package/docs/integration/bundling-akm.md +49 -42
- package/docs/migration/README.md +1 -0
- package/docs/migration/release-notes/0.9.17.md +41 -0
- package/docs/migration/v0.9.1-to-v0.9.2.md +19 -7
- package/docs/reference/cli.md +182 -125
- package/docs/reference/configuration.md +49 -56
- package/docs/reference/data-and-telemetry.md +19 -20
- package/docs/reference/tasks.md +86 -38
- package/docs/reference/workflow-schema.md +14 -18
- package/docs/reference/workflows.md +6 -9
- package/package.json +1 -1
- package/schemas/akm-config.json +87 -406
- package/dist/commands/health/advisories.js +0 -150
- package/dist/commands/health/metrics.js +0 -329
- package/dist/commands/health/surfaces.js +0 -102
- package/dist/commands/improve/anti-collapse.js +0 -83
- package/dist/commands/improve/collapse-detector.js +0 -432
- package/dist/commands/improve/consolidate/eligibility.js +0 -48
- package/dist/commands/improve/consolidate/merge.js +0 -146
- package/dist/commands/improve/distill/promote-memory.js +0 -329
- package/dist/commands/improve/distill/quality-gate.js +0 -500
- package/dist/commands/improve/memory/memory-contradiction-detect.js +0 -291
- package/dist/commands/improve/proposal-envelope.js +0 -31
- package/dist/commands/improve/run-context.js +0 -123
- package/dist/commands/improve/shared.js +0 -21
- package/dist/commands/improve/source-identity.js +0 -28
- package/dist/commands/improve/triage.js +0 -96
- package/dist/commands/proposal/drain-policies.js +0 -151
- package/dist/commands/sources/update-transaction.js +0 -220
- package/dist/core/action-contributors.js +0 -28
- package/dist/core/config/config-version-shim.js +0 -101
- package/dist/core/config/retired-experimental-keys-shim.js +0 -62
- package/dist/core/fs-txn.js +0 -405
- package/dist/core/lexical-score.js +0 -25
- package/dist/core/maintenance-barrier.js +0 -167
- package/dist/execution/executable-identity.js +0 -105
- package/dist/execution/guarded-source.js +0 -441
- package/dist/indexer/graph/graph-boost.js +0 -427
- package/dist/indexer/graph/graph-dedup.js +0 -95
- package/dist/indexer/search/name-match.js +0 -35
- package/dist/indexer/search/ranking-contributors.js +0 -515
- package/dist/indexer/walk/project-context.js +0 -192
- package/dist/integrations/agent/execution-cascade.js +0 -566
- package/dist/integrations/agent/execution-definitions.js +0 -202
- package/dist/integrations/agent/execution-lowering.js +0 -841
- package/dist/integrations/agent/execution-preparation.js +0 -98
- package/dist/integrations/agent/inline-execution.js +0 -74
- package/dist/registry/create-provider-registry.js +0 -29
- package/dist/registry/pinned-request-helper.js +0 -247
- package/dist/registry/pinned-transport.js +0 -717
- package/dist/sources/providers/index.js +0 -14
- package/dist/storage/engines/sqlite-migrations.js +0 -271
- package/dist/storage/repositories/canaries-repository.js +0 -107
- package/dist/storage/repositories/embedding-salvage-repository.js +0 -184
- package/dist/storage/repositories/registry-cache.js +0 -113
- package/dist/tasks/scheduler-sync-preview.js +0 -52
- package/dist/workflows/freeze/resolve-steps.js +0 -86
- package/dist/workflows/freeze/source-freeze.js +0 -64
- package/dist/workflows/ir/compile.js +0 -321
- package/dist/workflows/ir/environment-v4.js +0 -330
- package/dist/workflows/ir/freeze-v4.js +0 -153
- package/dist/workflows/ir/schema-v4.js +0 -745
- package/dist/workflows/ir/schema.js +0 -354
- package/dist/workflows/program/schema.js +0 -78
- package/dist/workflows/runtime/checkin.js +0 -57
- package/dist/workflows/runtime/plan-classifier.js +0 -196
- package/dist/workflows/runtime/unit-checkin.js +0 -45
- package/dist/workflows/runtime/unit-phases.js +0 -20
- package/dist/workflows/schema.js +0 -4
- package/dist/workflows/source-ir/compile.js +0 -200
- package/dist/workflows/source-ir/program.js +0 -50
- package/dist/workflows/source-ir/result.js +0 -26
- package/dist/workflows/source-ir/schema.js +0 -786
- package/dist/workflows/source-ir/triggers.js +0 -79
- package/dist/workflows/source-ir/uses.js +0 -40
- package/dist/workflows/validator.js +0 -60
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
2
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
3
|
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
/**
|
|
5
|
+
* `akm improve`: under one run lock, bootstrap the index, drain the proposal
|
|
6
|
+
* backlog (triage), select candidates, run preparation → loop → post-loop, then
|
|
7
|
+
* commit what the run wrote (auto-sync). A dry run plans on read-only state.
|
|
8
|
+
*/
|
|
4
9
|
import fs from "node:fs";
|
|
5
10
|
import path from "node:path";
|
|
6
|
-
import { assertNever } from "../../core/assert.js";
|
|
7
11
|
import { parseRefInput } from "../../core/asset/resolve-ref.js";
|
|
8
|
-
import {
|
|
9
|
-
import { bundlesToSourceEntries, loadConfig } from "../../core/config/config.js";
|
|
12
|
+
import { bundlesToSourceEntries, loadConfig, } from "../../core/config/config.js";
|
|
10
13
|
import { ConfigError, rethrowIfTestIsolationError, UsageError } from "../../core/errors.js";
|
|
11
14
|
import { appendEvent, readEvents } from "../../core/events.js";
|
|
12
15
|
import { classifyImproveAction, foldDistillSkipped } from "../../core/improve-types.js";
|
|
@@ -31,25 +34,20 @@ import { getEntryCount } from "../../storage/repositories/index-entries-reposito
|
|
|
31
34
|
import { openSqliteReadSnapshot, SqliteReadSnapshotUnavailableError } from "../../storage/sqlite-read-snapshot.js";
|
|
32
35
|
import { summarizeLlmUsageCrossTab } from "../health/llm-usage.js";
|
|
33
36
|
import { drainProposals } from "../proposal/drain.js";
|
|
34
|
-
import { resolveDrainPolicy } from "../proposal/drain-policies.js";
|
|
35
37
|
import { describeGatedLanes, isAutonomyLaneAllowed } from "./autonomy-gate.js";
|
|
36
38
|
import { akmDistill } from "./distill.js";
|
|
37
|
-
|
|
38
|
-
import {
|
|
39
|
-
import { projectResolvedProcessRouting, resolveImprovePlan, resolveImproveStrategy, shouldSkipRef, } from "./improve-strategies.js";
|
|
39
|
+
import { collectEligibleRefs, collectEligibleRefsReadOnly, memoryCleanupParentRef, resolveImproveScope, shouldAnalyzeMemoryCleanup, } from "./eligibility.js";
|
|
40
|
+
import { eligibleRefCount, projectResolvedProcessRouting, resolveImprovePlan, resolveImproveStrategy, } from "./improve-strategies.js";
|
|
40
41
|
import { buildImproveUsageReport } from "./improve-usage-report.js";
|
|
42
|
+
import { lastAttemptByRef, loadLedgerSnapshot } from "./ledger.js";
|
|
41
43
|
import { improveLockPath, releaseImproveLock, tryAcquireImproveLock } from "./locks.js";
|
|
42
|
-
// The cycle loop / post-loop / maintenance stages live in ./loop-stages.
|
|
43
44
|
import { runImproveLoopStage, runImprovePostLoopStage } from "./loop-stages.js";
|
|
44
45
|
import { analyzeMemoryCleanup } from "./memory/memory-improve.js";
|
|
45
46
|
import { buildImproveExecutionPlan } from "./planner.js";
|
|
46
|
-
|
|
47
|
-
import { runImprovePreparationStage } from "./preparation.js";
|
|
47
|
+
import { CONSOLIDATION_CONFIG_KEYS, pickDefined, recordImproveSkip, runImprovePreparationStage } from "./preparation.js";
|
|
48
48
|
import { DEFAULT_DUE_DAYS, filterProactiveDue } from "./proactive-maintenance.js";
|
|
49
49
|
import { akmReflect } from "./reflect.js";
|
|
50
|
-
import {
|
|
51
|
-
import { errMessage } from "./shared.js";
|
|
52
|
-
// Re-exported from ./loop-stages for test importers (improve-db-locking).
|
|
50
|
+
import { errMessage, noticeSet } from "./stage.js";
|
|
53
51
|
export { runImproveMaintenancePasses } from "./loop-stages.js";
|
|
54
52
|
export function renderSyncCommitMessage(template, result, nowMs) {
|
|
55
53
|
const iso = new Date(nowMs).toISOString();
|
|
@@ -67,18 +65,16 @@ export function renderSyncCommitMessage(template, result, nowMs) {
|
|
|
67
65
|
return template.replace(/\{(\w+)\}/g, (match, key) => tokens[key] ?? match);
|
|
68
66
|
}
|
|
69
67
|
/**
|
|
70
|
-
* How long the
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
* finishes and the loop is about to start dispatching engine requests — not
|
|
74
|
-
* at run start — so it measures engine latency, not prepass time. Field
|
|
75
|
-
* re-test (#957): an engine pointed at a dead endpoint produced zero output
|
|
76
|
-
* for minutes, so a genuine hang looked identical to a normal-but-slow run.
|
|
77
|
-
* A few seconds is short enough that an operator watching a scheduled run's
|
|
78
|
-
* live log sees something promptly, long enough that an ordinary fast
|
|
79
|
-
* response never prints it.
|
|
68
|
+
* How long the loop waits for its first engine response before printing one
|
|
69
|
+
* "still waiting" line (#957): a dead endpoint otherwise looks like a slow run.
|
|
70
|
+
* Armed when the loop starts, not at run start.
|
|
80
71
|
*/
|
|
81
72
|
export const FIRST_ENGINE_RESPONSE_HEARTBEAT_MS = 5_000;
|
|
73
|
+
/**
|
|
74
|
+
* Abort the run at the budget; force-exit (0 — budget exhaustion is a normal
|
|
75
|
+
* scheduled-task outcome) only if the drain overruns the grace period.
|
|
76
|
+
* Returns an idempotent disposer.
|
|
77
|
+
*/
|
|
82
78
|
export function armBudgetWatchdog(budgetMs, controller, deps) {
|
|
83
79
|
const setTimeoutFn = deps?.setTimeoutFn ?? setTimeout;
|
|
84
80
|
const clearTimeoutFn = deps?.clearTimeoutFn ?? clearTimeout;
|
|
@@ -86,15 +82,10 @@ export function armBudgetWatchdog(budgetMs, controller, deps) {
|
|
|
86
82
|
const hardKillGraceMs = deps?.hardKillGraceMs ?? 5_000;
|
|
87
83
|
let hardKillTimer;
|
|
88
84
|
const budgetTimer = setTimeoutFn(() => {
|
|
89
|
-
// Cooperative cancellation first: let the run drain.
|
|
90
85
|
controller.abort("improve budget exhausted");
|
|
91
|
-
// Watchdog: only force-exit if the drain itself overruns the grace period.
|
|
92
|
-
// Exit 0: budget exhaustion is a normal scheduled-task condition, not an error.
|
|
93
86
|
hardKillTimer = setTimeoutFn(() => exitFn(0), hardKillGraceMs);
|
|
94
|
-
// Never keep the event loop alive solely for the watchdog.
|
|
95
87
|
hardKillTimer.unref?.();
|
|
96
88
|
}, budgetMs);
|
|
97
|
-
// RAII dispose: clears whichever timer is still pending. Idempotent.
|
|
98
89
|
return () => {
|
|
99
90
|
clearTimeoutFn(budgetTimer);
|
|
100
91
|
if (hardKillTimer !== undefined) {
|
|
@@ -103,62 +94,30 @@ export function armBudgetWatchdog(budgetMs, controller, deps) {
|
|
|
103
94
|
}
|
|
104
95
|
};
|
|
105
96
|
}
|
|
106
|
-
/**
|
|
107
|
-
* The run's write-provenance journal lifecycle as one named unit (#652).
|
|
108
|
-
*
|
|
109
|
-
* The journal is opened once the run owns its lock — so it spans exactly the
|
|
110
|
-
* window in which this run, and only this run, is allowed to write — and closed
|
|
111
|
-
* on every exit path including the crash-safety commit. Keeping the mutable
|
|
112
|
-
* handle behind this factory rather than as a bare `let` in {@link akmImprove}
|
|
113
|
-
* is also what keeps that function under the R31 size gate
|
|
114
|
-
* (`tests/architecture/improve-fn-size-ratchet.test.ts`, absolute 220-line bar
|
|
115
|
-
* with an empty baseline): lifecycle state belongs in a named unit, not in the
|
|
116
|
-
* orchestrator's preamble.
|
|
117
|
-
*/
|
|
118
|
-
function createRunWriteJournal() {
|
|
119
|
-
let journal;
|
|
120
|
-
return {
|
|
121
|
-
open: () => {
|
|
122
|
-
journal = beginWriteProvenance();
|
|
123
|
-
},
|
|
124
|
-
close: () => {
|
|
125
|
-
journal?.end();
|
|
126
|
-
journal = undefined;
|
|
127
|
-
},
|
|
128
|
-
current: () => journal,
|
|
129
|
-
};
|
|
130
|
-
}
|
|
131
97
|
export async function akmImprove(options = {}) {
|
|
132
98
|
const setup = resolveImproveRunSetup(options);
|
|
133
99
|
options = setup.options;
|
|
134
|
-
const { budgetMs, budgetAbortController, scope, selectedStrategy, syncRepoDir, resolvedStateDbPath
|
|
100
|
+
const { budgetMs, budgetAbortController, scope, selectedStrategy, syncRepoDir, resolvedStateDbPath } = setup;
|
|
135
101
|
let clearBudgetTimer = () => { };
|
|
136
102
|
let clearFirstResponseHeartbeat = () => { };
|
|
137
|
-
//
|
|
138
|
-
// call terminates (success or error), including one issued by the prepass
|
|
139
|
-
// itself — makes arming the heartbeat below a no-op when the run is
|
|
140
|
-
// already known not to be silent.
|
|
103
|
+
// Set by the usage sink when any engine call terminates, prepass included.
|
|
141
104
|
let firstEngineResponseSeen = false;
|
|
142
105
|
let initialGitPaths = new Set();
|
|
143
|
-
|
|
106
|
+
// The write-provenance journal (#652) spans exactly the window this run holds the lock.
|
|
107
|
+
let journal;
|
|
108
|
+
const closeJournal = () => {
|
|
109
|
+
journal?.end();
|
|
110
|
+
journal = undefined;
|
|
111
|
+
};
|
|
144
112
|
const preEnsureCleanupWarnings = [];
|
|
145
|
-
let
|
|
146
|
-
let memorySummary = { eligible: 0, derived: 0 };
|
|
147
|
-
let strategyFilteredRefs = [];
|
|
148
|
-
let indexSnapshot;
|
|
149
|
-
let memoryCleanupPlan;
|
|
150
|
-
let autonomyGatedDirectLanes = [];
|
|
151
|
-
let guidance;
|
|
113
|
+
let collected;
|
|
152
114
|
let triageDrain;
|
|
153
115
|
let ensureIndexDurationMs;
|
|
154
116
|
let improveLockOwnership;
|
|
155
117
|
let exitBackstop;
|
|
156
|
-
// I1: open a single state.db connection for the main improve run so all
|
|
157
|
-
// appendEvent calls reuse one handle instead of open/migrate/close per call.
|
|
158
118
|
let eventsDb;
|
|
159
|
-
//
|
|
160
|
-
//
|
|
161
|
-
// append, so one owner and counter span both lifecycle phases.
|
|
119
|
+
// Boundary-pinned for the prepass; replaced by the long-lived handle after it.
|
|
120
|
+
// The usage sink resolves it per append.
|
|
162
121
|
let eventsCtx = { dbPath: resolvedStateDbPath };
|
|
163
122
|
let disposeLlmUsageSink = () => { };
|
|
164
123
|
const releaseRunLock = () => {
|
|
@@ -170,15 +129,27 @@ export async function akmImprove(options = {}) {
|
|
|
170
129
|
releaseImproveLock(ownership);
|
|
171
130
|
}
|
|
172
131
|
catch {
|
|
173
|
-
// Best-effort
|
|
132
|
+
// Best-effort; exact ownership never deletes a successor's lock.
|
|
174
133
|
}
|
|
175
134
|
};
|
|
135
|
+
const teardown = () => {
|
|
136
|
+
// The usage sink goes before eventsDb closes; the journal closes after the
|
|
137
|
+
// crash-safety commit that still needs it.
|
|
138
|
+
disposeLlmUsageSink();
|
|
139
|
+
clearFirstResponseHeartbeat();
|
|
140
|
+
clearBudgetTimer();
|
|
141
|
+
if (exitBackstop) {
|
|
142
|
+
// Only our own listener: removeAllListeners would drop the host's.
|
|
143
|
+
process.removeListener("exit", exitBackstop);
|
|
144
|
+
exitBackstop = undefined;
|
|
145
|
+
}
|
|
146
|
+
releaseRunLock();
|
|
147
|
+
closeJournal();
|
|
148
|
+
};
|
|
176
149
|
const commitStashBatch = makeCommitStashBatch({
|
|
177
150
|
run: setup,
|
|
178
151
|
getInitialGitPaths: () => initialGitPaths,
|
|
179
|
-
getWriteJournal: () =>
|
|
180
|
-
// Captured via getter: the live `eventsCtx` binding is reassigned after the
|
|
181
|
-
// prepass, and the catch-path sync must write through the current context.
|
|
152
|
+
getWriteJournal: () => journal,
|
|
182
153
|
getEventsCtx: () => eventsCtx,
|
|
183
154
|
});
|
|
184
155
|
if (!options.dryRun) {
|
|
@@ -187,8 +158,7 @@ export async function akmImprove(options = {}) {
|
|
|
187
158
|
}
|
|
188
159
|
try {
|
|
189
160
|
if (!options.dryRun) {
|
|
190
|
-
const acquisition = tryAcquireImproveLock(resolvedLockPath, options.skipIfLocked, {
|
|
191
|
-
// R25: C2 boundary-pinned path — the long-lived handle doesn't exist yet.
|
|
161
|
+
const acquisition = tryAcquireImproveLock(setup.resolvedLockPath, options.skipIfLocked, {
|
|
192
162
|
dbPath: resolvedStateDbPath,
|
|
193
163
|
});
|
|
194
164
|
if (acquisition.state === "skipped") {
|
|
@@ -204,86 +174,38 @@ export async function akmImprove(options = {}) {
|
|
|
204
174
|
process.on("exit", exitBackstop);
|
|
205
175
|
initialGitPaths =
|
|
206
176
|
syncRepoDir && isGitBackedStash(syncRepoDir) ? new Set(listGitChangedPaths(syncRepoDir)) : new Set();
|
|
207
|
-
|
|
208
|
-
//
|
|
209
|
-
|
|
210
|
-
// R6: ensureIndex BEFORE triage, not after. Triage promotes proposals
|
|
211
|
-
// straight into the flat `knowledge/` root; running the blocking
|
|
212
|
-
// reindex only afterward (inside indexAndCollect, below) meant every
|
|
213
|
-
// triage promotion dirtied the stash right before the rescan that is
|
|
214
|
-
// supposed to precede it, so the "blocking" reindex always found fresh
|
|
215
|
-
// work and paid for a full walk on the very run it was meant to avoid.
|
|
177
|
+
journal = beginWriteProvenance();
|
|
178
|
+
// The index is made current BEFORE triage (R6): triage promotes into the
|
|
179
|
+
// stash, and a reindex after it would always find fresh work.
|
|
216
180
|
const bootstrap = await runIndexBootstrapPass(setup, budgetAbortController.signal);
|
|
217
181
|
preEnsureCleanupWarnings.push(...bootstrap.warnings);
|
|
218
182
|
ensureIndexDurationMs = bootstrap.ensureIndexDurationMs;
|
|
219
|
-
// Drain the standing proposal backlog under the now-current index, so
|
|
220
|
-
// fresh proposal generation sees promotions from this same serialized
|
|
221
|
-
// run.
|
|
222
183
|
triageDrain = await runTriagePrePass(setup);
|
|
223
|
-
//
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
// needs their `content_hash` recorded so it does not re-trigger a full
|
|
228
|
-
// rescan next run for content it already has.
|
|
229
|
-
if (setup.primaryStashDir) {
|
|
230
|
-
const triageWrittenPaths = runJournal.current()?.writtenPaths() ?? [];
|
|
231
|
-
if (triageWrittenPaths.length > 0) {
|
|
232
|
-
await indexWrittenAssets(setup.primaryStashDir, triageWrittenPaths);
|
|
233
|
-
}
|
|
184
|
+
// Index triage's own writes incrementally so selection sees them.
|
|
185
|
+
const triageWrittenPaths = journal?.writtenPaths() ?? [];
|
|
186
|
+
if (setup.primaryStashDir && triageWrittenPaths.length > 0) {
|
|
187
|
+
await indexWrittenAssets(setup.primaryStashDir, triageWrittenPaths);
|
|
234
188
|
}
|
|
235
189
|
}
|
|
236
|
-
|
|
237
|
-
plannedRefs = collected.plannedRefs;
|
|
238
|
-
memorySummary = collected.memorySummary;
|
|
239
|
-
strategyFilteredRefs = collected.strategyFilteredRefs;
|
|
240
|
-
indexSnapshot = collected.indexSnapshot;
|
|
241
|
-
memoryCleanupPlan = collected.memoryCleanupPlan;
|
|
242
|
-
autonomyGatedDirectLanes = collected.autonomyGatedDirectLanes;
|
|
243
|
-
guidance = collected.guidance;
|
|
190
|
+
collected = await indexAndCollect(setup);
|
|
244
191
|
if (options.dryRun) {
|
|
245
|
-
const result = await runDryPlanningStage(
|
|
246
|
-
run: setup,
|
|
247
|
-
collected,
|
|
248
|
-
resolvedStateDbPath,
|
|
249
|
-
budgetMs,
|
|
250
|
-
initialCleanupWarnings: preEnsureCleanupWarnings,
|
|
251
|
-
signal: budgetAbortController.signal,
|
|
252
|
-
});
|
|
192
|
+
const result = await runDryPlanningStage(setup, collected, preEnsureCleanupWarnings);
|
|
253
193
|
clearBudgetTimer();
|
|
254
194
|
return result;
|
|
255
195
|
}
|
|
256
196
|
}
|
|
257
197
|
catch (err) {
|
|
258
|
-
|
|
259
|
-
// before any run teardown. The disposer is idempotent with the main finalizer.
|
|
260
|
-
disposeLlmUsageSink();
|
|
261
|
-
clearFirstResponseHeartbeat();
|
|
262
|
-
clearBudgetTimer();
|
|
263
|
-
if (exitBackstop) {
|
|
264
|
-
process.removeListener("exit", exitBackstop);
|
|
265
|
-
exitBackstop = undefined;
|
|
266
|
-
}
|
|
267
|
-
releaseRunLock();
|
|
268
|
-
runJournal.close();
|
|
198
|
+
teardown();
|
|
269
199
|
throw err;
|
|
270
200
|
}
|
|
271
201
|
try {
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
const ctx = buildImproveRunContext(setup, eventsCtx);
|
|
280
|
-
// #957: arm the heartbeat here, immediately before the improve loop
|
|
281
|
-
// starts dispatching engine requests — not at run start, where its timer
|
|
282
|
-
// would measure the triage/index prepass instead of engine latency. A
|
|
283
|
-
// no-op when the prepass already produced a terminal LLM record (the
|
|
284
|
-
// onRecord callback above already saw it). Cleared the moment any call
|
|
285
|
-
// terminates (success or error) — never rearmed, so this prints at most
|
|
286
|
-
// once per run.
|
|
202
|
+
try {
|
|
203
|
+
eventsDb = openStateDatabase(resolvedStateDbPath);
|
|
204
|
+
eventsCtx = { db: eventsDb };
|
|
205
|
+
}
|
|
206
|
+
catch (err) {
|
|
207
|
+
rethrowIfTestIsolationError(err);
|
|
208
|
+
}
|
|
287
209
|
if (!firstEngineResponseSeen) {
|
|
288
210
|
const firstResponseTimer = setTimeout(() => {
|
|
289
211
|
warn("[improve] Still waiting for the first engine response...");
|
|
@@ -291,46 +213,11 @@ export async function akmImprove(options = {}) {
|
|
|
291
213
|
firstResponseTimer.unref?.();
|
|
292
214
|
clearFirstResponseHeartbeat = () => clearTimeout(firstResponseTimer);
|
|
293
215
|
}
|
|
294
|
-
const seq = await runImproveStageSequence(
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
autonomyGatedDirectLanes,
|
|
300
|
-
memorySummary,
|
|
301
|
-
preEnsureCleanupWarnings,
|
|
302
|
-
eventsCtx,
|
|
303
|
-
ctx,
|
|
304
|
-
});
|
|
305
|
-
const result = finalizeImproveResult({
|
|
306
|
-
run: setup,
|
|
307
|
-
seq,
|
|
308
|
-
guidance,
|
|
309
|
-
memorySummary,
|
|
310
|
-
memoryCleanupPlan,
|
|
311
|
-
strategyFilteredRefs,
|
|
312
|
-
rawPlannedRefs: plannedRefs,
|
|
313
|
-
indexSnapshot,
|
|
314
|
-
triageDrain,
|
|
315
|
-
ensureIndexDurationMs,
|
|
316
|
-
eventsCtx,
|
|
317
|
-
});
|
|
318
|
-
// End-of-run BATCH auto-sync — the converged commit. Recognition is
|
|
319
|
-
// decoupled from the per-write path (see write-source.ts case-3): the primary
|
|
320
|
-
// stash writes as a filesystem source during the run, then is committed via
|
|
321
|
-
// the same `saveGitStash` that `akm sync` calls. The gating (git-backed
|
|
322
|
-
// primary stash, sync not disabled) and the NON-FATAL guarantee now live in
|
|
323
|
-
// `commitStashBatch` (#662); the inter-cycle and catch-path calls reuse it.
|
|
324
|
-
// dry-run already returned above, so this always runs on a completed live
|
|
325
|
-
// run. `result.sync` reflects this final commit (for a one-cycle run it is
|
|
326
|
-
// the only commit; for a multi-cycle run the earlier cycles were banked by
|
|
327
|
-
// the inter-cycle calls and this records the last batch). `result` carries
|
|
328
|
-
// the full `{accepted}`/`{refs}`/`{triage_*}` token data for the message.
|
|
329
|
-
//
|
|
330
|
-
// #652: surface the run's write provenance on the envelope BEFORE the sync
|
|
331
|
-
// (the sync itself writes no assets), so `result.writtenPaths` describes
|
|
332
|
-
// exactly the set the commit below was scoped to.
|
|
333
|
-
const writtenPaths = describeRunWrittenPaths(setup, runJournal.current()?.writtenPaths() ?? []);
|
|
216
|
+
const seq = await runImproveStageSequence(setup, collected, preEnsureCleanupWarnings, eventsCtx);
|
|
217
|
+
const result = finalizeImproveResult({ run: setup, seq, collected, triageDrain, ensureIndexDurationMs, eventsCtx });
|
|
218
|
+
// The run's write provenance goes on the envelope before the sync, so
|
|
219
|
+
// `writtenPaths` is exactly the set the commit is scoped to.
|
|
220
|
+
const writtenPaths = describeRunWrittenPaths(setup, journal?.writtenPaths() ?? []);
|
|
334
221
|
if (writtenPaths.length > 0)
|
|
335
222
|
result.writtenPaths = writtenPaths;
|
|
336
223
|
result.sync = commitStashBatch(result);
|
|
@@ -338,64 +225,24 @@ export async function akmImprove(options = {}) {
|
|
|
338
225
|
}
|
|
339
226
|
catch (err) {
|
|
340
227
|
recordImproveFailure(err, setup, eventsCtx);
|
|
341
|
-
//
|
|
342
|
-
//
|
|
343
|
-
|
|
344
|
-
// writes uncommitted until a later clean run sweeps them up. Best-effort —
|
|
345
|
-
// `commitStashBatch` swallows its own errors and no-ops a clean tree, so this
|
|
346
|
-
// never masks or supersedes the original failure being rethrown below.
|
|
347
|
-
commitStashBatch({ scope, plannedRefs, runId: options.runId });
|
|
228
|
+
// Crash/abort safety net (#662): commit what this run already wrote.
|
|
229
|
+
// commitStashBatch never throws and no-ops a clean tree.
|
|
230
|
+
commitStashBatch({ scope, plannedRefs: collected.plannedRefs, runId: options.runId });
|
|
348
231
|
throw err;
|
|
349
232
|
}
|
|
350
233
|
finally {
|
|
351
|
-
|
|
352
|
-
// no late sink invocation can write through a closed handle.
|
|
353
|
-
disposeLlmUsageSink();
|
|
354
|
-
// #957: never leave the first-response heartbeat timer pending past the run.
|
|
355
|
-
clearFirstResponseHeartbeat();
|
|
356
|
-
// O-1 (#364): Clear the budget abort timer so it does not keep the event
|
|
357
|
-
// loop alive after the run completes.
|
|
358
|
-
clearBudgetTimer();
|
|
359
|
-
// Drop ONLY our own process.exit backstop so it does not fire later (or
|
|
360
|
-
// accumulate across repeated in-process calls). Must NOT use
|
|
361
|
-
// removeAllListeners("exit") here: in the in-process model (tests and
|
|
362
|
-
// programmatic callers import cli.ts) that would silently destroy exit
|
|
363
|
-
// handlers owned by the host or other commands.
|
|
364
|
-
if (exitBackstop) {
|
|
365
|
-
process.removeListener("exit", exitBackstop);
|
|
366
|
-
exitBackstop = undefined;
|
|
367
|
-
}
|
|
368
|
-
releaseRunLock();
|
|
369
|
-
// #652: close the write-provenance journal LAST among the write-facing
|
|
370
|
-
// teardown steps — the catch path's crash-safety commit above still needs
|
|
371
|
-
// it open to know what this run wrote.
|
|
372
|
-
runJournal.close();
|
|
373
|
-
// I1: close the long-lived state.db connection opened at the top of the run.
|
|
234
|
+
teardown();
|
|
374
235
|
try {
|
|
375
236
|
eventsDb?.close();
|
|
376
237
|
}
|
|
377
238
|
catch {
|
|
378
|
-
//
|
|
239
|
+
// already closed
|
|
379
240
|
}
|
|
380
241
|
}
|
|
381
242
|
}
|
|
382
|
-
/** Open the run-owned state handle while preserving the boundary-pinned fallback. */
|
|
383
|
-
function openImproveEventsContext(dbPath) {
|
|
384
|
-
try {
|
|
385
|
-
const db = openStateDatabase(dbPath);
|
|
386
|
-
return { db, ctx: { db } };
|
|
387
|
-
}
|
|
388
|
-
catch (err) {
|
|
389
|
-
rethrowIfTestIsolationError(err);
|
|
390
|
-
return { ctx: { dbPath } };
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
243
|
/**
|
|
394
|
-
*
|
|
395
|
-
*
|
|
396
|
-
* stash) when the write landed inside it, absolute otherwise — a run writing to
|
|
397
|
-
* a `--target` bundle outside the stash still reports what it wrote. Deduped and
|
|
398
|
-
* sorted so the field is stable across runs.
|
|
244
|
+
* The run's journaled paths for `result.writtenPaths`: POSIX-relative to the
|
|
245
|
+
* primary stash when inside it, absolute otherwise; deduped and sorted.
|
|
399
246
|
*/
|
|
400
247
|
function describeRunWrittenPaths(setup, writtenPaths) {
|
|
401
248
|
const root = setup.primaryStashDir ?? setup.syncRepoDir;
|
|
@@ -407,19 +254,16 @@ function describeRunWrittenPaths(setup, writtenPaths) {
|
|
|
407
254
|
return [...described].sort();
|
|
408
255
|
}
|
|
409
256
|
/**
|
|
410
|
-
*
|
|
411
|
-
*
|
|
412
|
-
* read-only bundle a plain `--dry-run` would, without duplicating this
|
|
413
|
-
* selector/target/fallback precedence.
|
|
257
|
+
* The source a dry run (or `--show-prompt`) inspects, without adapting it into
|
|
258
|
+
* a write target.
|
|
414
259
|
*/
|
|
415
260
|
export function resolveImproveReadSource(config, scopedRef, explicitTarget, fallbackStashDir) {
|
|
416
261
|
if (scopedRef?.origin && explicitTarget && scopedRef.origin !== explicitTarget) {
|
|
417
262
|
throw new UsageError(`Qualified ref bundle "${scopedRef.origin}" conflicts with --target "${explicitTarget}".`, "INVALID_FLAG_VALUE", `Drop --target or use --target ${scopedRef.origin}.`);
|
|
418
263
|
}
|
|
419
264
|
const selector = scopedRef?.origin ?? explicitTarget ?? config.defaultWriteTarget;
|
|
420
|
-
if (!selector && fallbackStashDir)
|
|
265
|
+
if (!selector && fallbackStashDir)
|
|
421
266
|
return { source: { name: "stash", path: fallbackStashDir } };
|
|
422
|
-
}
|
|
423
267
|
const configuredSelector = selector ?? config.defaultBundle;
|
|
424
268
|
if (configuredSelector) {
|
|
425
269
|
const entry = bundlesToSourceEntries(config)?.find((source) => source.name === configuredSelector);
|
|
@@ -433,17 +277,14 @@ export function resolveImproveReadSource(config, scopedRef, explicitTarget, fall
|
|
|
433
277
|
return { selector: configuredSelector, source: { name: configuredSelector, path: sourcePath } };
|
|
434
278
|
}
|
|
435
279
|
const implicit = resolveSourceEntries(undefined, config)[0];
|
|
436
|
-
if (!implicit)
|
|
280
|
+
if (!implicit)
|
|
437
281
|
throw new ConfigError("no source configured; run `akm bundle create`", "STASH_DIR_NOT_FOUND");
|
|
438
|
-
}
|
|
439
282
|
return { source: { name: implicit.registryId ?? "stash", path: implicit.path } };
|
|
440
283
|
}
|
|
441
284
|
/**
|
|
442
|
-
* Run
|
|
443
|
-
*
|
|
444
|
-
* the
|
|
445
|
-
* snapshot (resolvedStateDbPath) must be taken before the first await of the
|
|
446
|
-
* run.
|
|
285
|
+
* Run setup, fully synchronous: the budget signal, the invocation plan, the
|
|
286
|
+
* write target, the profile-defaulted options, and the state.db and lock paths
|
|
287
|
+
* pinned before the first await (C2: a later env change cannot redirect them).
|
|
447
288
|
*/
|
|
448
289
|
function resolveImproveRunSetup(options) {
|
|
449
290
|
const startMs = Date.now();
|
|
@@ -455,30 +296,15 @@ function resolveImproveRunSetup(options) {
|
|
|
455
296
|
configurable: true,
|
|
456
297
|
});
|
|
457
298
|
const scope = resolveImproveScope(options.scope);
|
|
458
|
-
const
|
|
459
|
-
const
|
|
460
|
-
|
|
461
|
-
const reindexFn = options.reindexFn ?? akmIndex;
|
|
462
|
-
const drainProposalsFn = options.drainProposalsFn ?? drainProposals;
|
|
463
|
-
// #616 multi-cycle test seams. Default to the real module-local fns.
|
|
464
|
-
const collectEligibleRefsImpl = options.collectEligibleRefsFn ?? (options.dryRun ? collectEligibleRefsReadOnly : collectEligibleRefs);
|
|
465
|
-
const runImprovePreparationStageImpl = options.runImprovePreparationStageFn ?? runImprovePreparationStage;
|
|
466
|
-
const runImproveLoopStageImpl = options.runImproveLoopStageFn ?? runImproveLoopStage;
|
|
467
|
-
const runImprovePostLoopStageImpl = options.runImprovePostLoopStageFn ?? runImprovePostLoopStage;
|
|
468
|
-
// Resolve the improve profile for this run. Profile drives type filtering,
|
|
469
|
-
// process gating, and the default limit value.
|
|
470
|
-
const _earlyConfig = options.config ?? loadConfig();
|
|
471
|
-
const configuredImproveProfile = resolveImproveStrategy(options.strategy, _earlyConfig).config;
|
|
299
|
+
const config = options.config ?? loadConfig();
|
|
300
|
+
const configuredImproveProfile = resolveImproveStrategy(options.strategy, config).config;
|
|
301
|
+
// A dry run never dispatches, so an all-disabled strategy must not abort it.
|
|
472
302
|
const resolvedPlan = options.resolvedPlan ??
|
|
473
|
-
|
|
474
|
-
// resolveImprovePlan call: a dry run never dispatches, so a strategy left
|
|
475
|
-
// fully disabled by an unreachable credential must not abort here either.
|
|
476
|
-
resolveImprovePlan(options.strategy, _earlyConfig, {
|
|
303
|
+
resolveImprovePlan(options.strategy, config, {
|
|
477
304
|
repairValidationFailures: options.repairValidationFailures,
|
|
478
305
|
allowAllDisabled: options.dryRun,
|
|
479
306
|
});
|
|
480
307
|
const selectedStrategy = resolvedPlan.strategy;
|
|
481
|
-
const improveSensitiveValues = collectEngineCredentialValues(_earlyConfig);
|
|
482
308
|
const improveProfile = selectedStrategy.config;
|
|
483
309
|
const configuredLimits = {
|
|
484
310
|
...(options.limit !== undefined ? { cli: options.limit } : {}),
|
|
@@ -487,52 +313,36 @@ function resolveImproveRunSetup(options) {
|
|
|
487
313
|
? { reflect: configuredImproveProfile.processes.reflect.limit }
|
|
488
314
|
: {}),
|
|
489
315
|
};
|
|
316
|
+
// --limit, then the reflect process limit, then the profile limit.
|
|
490
317
|
const effectiveLimit = options.limit ?? improveProfile?.processes?.reflect?.limit ?? improveProfile.limit;
|
|
491
318
|
const scopedRef = scope.mode === "ref" && scope.value ? parseRefInput(scope.value) : undefined;
|
|
492
319
|
const readSource = options.dryRun
|
|
493
320
|
? options.writeTarget
|
|
494
321
|
? { selector: options.writeTarget.selector, source: options.writeTarget.source }
|
|
495
|
-
: resolveImproveReadSource(
|
|
322
|
+
: resolveImproveReadSource(config, scopedRef, options.target, options.stashDir)
|
|
496
323
|
: undefined;
|
|
497
324
|
const writeTarget = options.dryRun
|
|
498
325
|
? undefined
|
|
499
326
|
: scopedRef?.origin
|
|
500
|
-
? resolveMutationTarget(
|
|
327
|
+
? resolveMutationTarget(config, scopedRef, options.writeTarget?.source.name ?? options.target).target
|
|
501
328
|
: (options.writeTarget ??
|
|
502
|
-
(options.target ||
|
|
503
|
-
? resolveWriteTarget(
|
|
329
|
+
(options.target || config.defaultWriteTarget || !options.stashDir
|
|
330
|
+
? resolveWriteTarget(config, options.target)
|
|
504
331
|
: {
|
|
505
332
|
source: { kind: "filesystem", name: "stash", path: options.stashDir },
|
|
506
|
-
config: {
|
|
507
|
-
type: "filesystem",
|
|
508
|
-
name: "stash",
|
|
509
|
-
path: options.stashDir,
|
|
510
|
-
writable: true,
|
|
511
|
-
},
|
|
333
|
+
config: { type: "filesystem", name: "stash", path: options.stashDir, writable: true },
|
|
512
334
|
}));
|
|
513
335
|
const selectedSource = writeTarget?.source ?? readSource?.source;
|
|
514
336
|
if (!selectedSource)
|
|
515
337
|
throw new ConfigError("improve could not resolve a source", "STASH_DIR_NOT_FOUND");
|
|
516
|
-
|
|
517
|
-
// Apply profile defaults — CLI flags take precedence over profile defaults.
|
|
518
|
-
// Rebuild options with effective values so all downstream stage functions
|
|
519
|
-
// automatically pick up the profile-driven defaults.
|
|
338
|
+
// Every stage reads this one config snapshot; nothing reloads it mid-run.
|
|
520
339
|
options = {
|
|
521
340
|
...options,
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
config: _earlyConfig,
|
|
525
|
-
target: selectedSelector,
|
|
341
|
+
config,
|
|
342
|
+
target: writeTarget?.selector ?? readSource?.selector,
|
|
526
343
|
sourceName: selectedSource.name,
|
|
527
344
|
...(writeTarget ? { writeTarget } : {}),
|
|
528
345
|
stashDir: selectedSource.path,
|
|
529
|
-
consolidateOptions: {
|
|
530
|
-
...options.consolidateOptions,
|
|
531
|
-
target: selectedSelector,
|
|
532
|
-
...(writeTarget ? { writeTarget } : {}),
|
|
533
|
-
},
|
|
534
|
-
// Profile-level limit, then process-level reflect.limit as fallback.
|
|
535
|
-
// CLI --limit takes precedence over both.
|
|
536
346
|
limit: effectiveLimit,
|
|
537
347
|
};
|
|
538
348
|
let primaryStashDir;
|
|
@@ -542,42 +352,24 @@ function resolveImproveRunSetup(options) {
|
|
|
542
352
|
catch {
|
|
543
353
|
primaryStashDir = undefined;
|
|
544
354
|
}
|
|
545
|
-
const syncRepoDir = writeTarget?.source.repoPath ?? primaryStashDir;
|
|
546
|
-
// C2 (#553/#554/#499): resolve the state.db path ONCE, synchronously, at the
|
|
547
|
-
// command boundary — before the first `await` below. Every state.db open in
|
|
548
|
-
// this run (`openStateDatabase`, every default-path `appendEvent`) is pinned
|
|
549
|
-
// to this snapshot via `eventsCtx.dbPath`, so a parallel test file mutating
|
|
550
|
-
// `process.env.XDG_DATA_HOME` across an await boundary can never redirect this
|
|
551
|
-
// run's DB opens to a wrong/just-deleted tmpdir mid-flight (the parallel-load
|
|
552
|
-
// timeout root cause). Because beforeEach runs synchronously, env is still the
|
|
553
|
-
// calling test's own at this point; we capture it before yielding the loop.
|
|
554
|
-
const resolvedStateDbPath = getStateDbPathInDataDir();
|
|
555
|
-
// One conservative run lock protects the complete mutation window, including
|
|
556
|
-
// triage, indexing, proposal work, maintenance, and final stash sync. Moved
|
|
557
|
-
// out of `$STASH/.akm` to `$STATE/locks/<stash>/` (itlackey/akm#890): a lock
|
|
558
|
-
// file is machine-local coordination state, not content that must travel
|
|
559
|
-
// with the bundle.
|
|
560
|
-
const lockBaseDir = getStashLocksDir(primaryStashDir ?? options.stashDir ?? ".");
|
|
561
|
-
const resolvedLockPath = improveLockPath(lockBaseDir);
|
|
562
|
-
const effectiveSync = { ...improveProfile.sync, ...options.sync };
|
|
563
355
|
return {
|
|
564
356
|
startMs,
|
|
565
357
|
budgetMs,
|
|
566
358
|
budgetAbortController,
|
|
567
359
|
scope,
|
|
568
|
-
reflectFn,
|
|
569
|
-
distillFn,
|
|
570
|
-
ensureIndexFn,
|
|
571
|
-
reindexFn,
|
|
572
|
-
drainProposalsFn,
|
|
573
|
-
collectEligibleRefsImpl,
|
|
574
|
-
runImprovePreparationStageImpl,
|
|
575
|
-
runImproveLoopStageImpl,
|
|
576
|
-
runImprovePostLoopStageImpl,
|
|
577
|
-
|
|
360
|
+
reflectFn: options.reflectFn ?? akmReflect,
|
|
361
|
+
distillFn: options.distillFn ?? akmDistill,
|
|
362
|
+
ensureIndexFn: options.ensureIndexFn ?? ensureIndex,
|
|
363
|
+
reindexFn: options.reindexFn ?? akmIndex,
|
|
364
|
+
drainProposalsFn: options.drainProposalsFn ?? drainProposals,
|
|
365
|
+
collectEligibleRefsImpl: options.collectEligibleRefsFn ?? (options.dryRun ? collectEligibleRefsReadOnly : collectEligibleRefs),
|
|
366
|
+
runImprovePreparationStageImpl: options.runImprovePreparationStageFn ?? runImprovePreparationStage,
|
|
367
|
+
runImproveLoopStageImpl: options.runImproveLoopStageFn ?? runImproveLoopStage,
|
|
368
|
+
runImprovePostLoopStageImpl: options.runImprovePostLoopStageFn ?? runImprovePostLoopStage,
|
|
369
|
+
config,
|
|
578
370
|
resolvedPlan,
|
|
579
371
|
selectedStrategy,
|
|
580
|
-
improveSensitiveValues,
|
|
372
|
+
improveSensitiveValues: collectEngineCredentialValues(config),
|
|
581
373
|
improveProfile,
|
|
582
374
|
configuredImproveProfile,
|
|
583
375
|
configuredLimits,
|
|
@@ -585,108 +377,58 @@ function resolveImproveRunSetup(options) {
|
|
|
585
377
|
writeTarget,
|
|
586
378
|
options,
|
|
587
379
|
primaryStashDir,
|
|
588
|
-
syncRepoDir,
|
|
589
|
-
resolvedStateDbPath,
|
|
590
|
-
|
|
591
|
-
|
|
380
|
+
syncRepoDir: writeTarget?.source.repoPath ?? primaryStashDir,
|
|
381
|
+
resolvedStateDbPath: getStateDbPathInDataDir(),
|
|
382
|
+
// The run lock is machine-local state, kept outside the bundle (#890).
|
|
383
|
+
resolvedLockPath: improveLockPath(getStashLocksDir(primaryStashDir ?? options.stashDir ?? ".")),
|
|
384
|
+
effectiveSync: { ...improveProfile.sync, ...options.sync },
|
|
592
385
|
};
|
|
593
386
|
}
|
|
594
|
-
/**
|
|
595
|
-
* WI-9.10: build the run's {@link RunContext} purely from values
|
|
596
|
-
* `resolveImproveRunSetup` and the long-lived state.db open already
|
|
597
|
-
* resolved — no second config load, no new db handle. Called once, right
|
|
598
|
-
* after `eventsCtx` is resolved (the last of {config, stashDir, eventsCtx,
|
|
599
|
-
* proposalsCtx, sourceRun, dryRun} to become available in `akmImprove`).
|
|
600
|
-
*/
|
|
601
|
-
function buildImproveRunContext(run, eventsCtx) {
|
|
602
|
-
return createRunContext({
|
|
603
|
-
config: run._earlyConfig,
|
|
604
|
-
// primaryStashDir can fail to resolve (rare); "." mirrors the existing
|
|
605
|
-
// lockBaseDir fallback in resolveImproveRunSetup for the same case. This
|
|
606
|
-
// is a BEST-EFFORT value for the required RunContext field only — the
|
|
607
|
-
// loop stage reads `ImproveLoopState.primaryStashDir` (the honest
|
|
608
|
-
// optional), so its `if (primaryStashDir)` guards still skip on the
|
|
609
|
-
// unresolvable path; no RunContext consumer reads `ctx.stashDir` there.
|
|
610
|
-
stashDir: run.primaryStashDir ?? run.options.stashDir ?? ".",
|
|
611
|
-
eventsCtx,
|
|
612
|
-
// ProposalsContext threads only a dbPath seam (D14: no db handle is
|
|
613
|
-
// threaded in), mirroring eventsCtx's own dbPath-only fallback shape.
|
|
614
|
-
// Not yet wired into any proposal call site this stage — verb-level
|
|
615
|
-
// RunContext adoption (reflect/distill/extract/consolidate) is later.
|
|
616
|
-
proposalsCtx: { dbPath: run.resolvedStateDbPath },
|
|
617
|
-
// Representative symbolic runner for this run: reflect is the loop's
|
|
618
|
-
// primary model-driving process. Credentials remain unresolved here.
|
|
619
|
-
getLlmRunner: () => run.resolvedPlan.processes.reflect.runner,
|
|
620
|
-
sourceRun: run.options.runId ?? `improve-${run.startMs}`,
|
|
621
|
-
// Always false here: callers only reach this point past the dry-run
|
|
622
|
-
// early return in akmImprove.
|
|
623
|
-
dryRun: run.options.dryRun ?? false,
|
|
624
|
-
// Same AbortSignal instance armBudgetWatchdog stamped a live
|
|
625
|
-
// `remainingBudgetMs` getter onto (#616) — identity preserved so
|
|
626
|
-
// `ctx.signal.remainingBudgetMs` resolves correctly for every consumer.
|
|
627
|
-
signal: run.budgetAbortController.signal,
|
|
628
|
-
});
|
|
629
|
-
}
|
|
630
|
-
/** The catch-path improve_failed audit event (D3), redacted. */
|
|
387
|
+
/** The redacted `improve_failed` event for a crashed run. */
|
|
631
388
|
function recordImproveFailure(err, run, eventsCtx) {
|
|
632
|
-
const { scope, selectedStrategy, improveSensitiveValues, startMs } = run;
|
|
633
|
-
// D3: emit improve_failed on unexpected crash so dashboards can detect failures.
|
|
634
389
|
appendEvent({
|
|
635
390
|
eventType: "improve_failed",
|
|
636
|
-
ref: scope.mode === "ref" ? scope.value : `improve:${scope.mode}:${scope.value ?? "all"}`,
|
|
391
|
+
ref: run.scope.mode === "ref" ? run.scope.value : `improve:${run.scope.mode}:${run.scope.value ?? "all"}`,
|
|
637
392
|
metadata: {
|
|
638
|
-
strategy: selectedStrategy.name,
|
|
639
|
-
error: redactSensitiveText(errMessage(err), improveSensitiveValues),
|
|
640
|
-
durationMs: Date.now() - startMs,
|
|
393
|
+
strategy: run.selectedStrategy.name,
|
|
394
|
+
error: redactSensitiveText(errMessage(err), run.improveSensitiveValues),
|
|
395
|
+
durationMs: Date.now() - run.startMs,
|
|
641
396
|
},
|
|
642
397
|
}, eventsCtx);
|
|
643
398
|
}
|
|
644
|
-
|
|
645
|
-
* R6: ensureIndex bootstrap pass, hoisted out of `indexAndCollect` (below) so
|
|
646
|
-
* the caller can run it BEFORE the triage pre-pass instead of after it —
|
|
647
|
-
* triage promotes proposals straight into `knowledge/`, so ensureIndex must
|
|
648
|
-
* see a current index before triage dirties it, not after. Gated exactly as
|
|
649
|
-
* this logic was gated inside `indexAndCollect`: only for a resolved
|
|
650
|
-
* `primaryStashDir` on a non-dry-run.
|
|
651
|
-
*
|
|
652
|
-
* #339 fix carried over unchanged: ensureIndex MUST run before
|
|
653
|
-
* collectEligibleRefs. The eligible-ref query reads the `entries` table; if a
|
|
654
|
-
* DB version upgrade just dropped that table (or the index is otherwise
|
|
655
|
-
* empty), skipping this would silently return plannedRefs=[] and the improve
|
|
656
|
-
* loop would no-op.
|
|
657
|
-
*/
|
|
658
|
-
async function runIndexBootstrapPass(run, signal) {
|
|
659
|
-
const { primaryStashDir, options, ensureIndexFn } = run;
|
|
660
|
-
const warnings = [];
|
|
661
|
-
if (!primaryStashDir || options.dryRun)
|
|
662
|
-
return { warnings };
|
|
663
|
-
// Probe pre-ensureIndex entry count to drive the loud-fail warning below.
|
|
664
|
-
// Best-effort: a missing DB / unreadable schema is the fresh-install case
|
|
665
|
-
// and not a bug — we silently skip the probe.
|
|
666
|
-
let preEnsureEntryCount;
|
|
399
|
+
function probeEntryCount() {
|
|
667
400
|
try {
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
}
|
|
401
|
+
if (!fs.existsSync(getDbPath()))
|
|
402
|
+
return undefined;
|
|
403
|
+
const db = openExistingDatabase();
|
|
404
|
+
try {
|
|
405
|
+
return getEntryCount(db);
|
|
406
|
+
}
|
|
407
|
+
finally {
|
|
408
|
+
closeDatabase(db);
|
|
677
409
|
}
|
|
678
410
|
}
|
|
679
411
|
catch (err) {
|
|
680
412
|
rethrowIfTestIsolationError(err);
|
|
681
|
-
|
|
413
|
+
return undefined;
|
|
682
414
|
}
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* ensureIndex before selection (#339): the eligible-ref query reads `entries`,
|
|
418
|
+
* which a DB version upgrade may just have dropped. An index that was empty
|
|
419
|
+
* and is now populated means an upgrade rebuild happened; say so.
|
|
420
|
+
*/
|
|
421
|
+
async function runIndexBootstrapPass(run, signal) {
|
|
422
|
+
const { primaryStashDir, options } = run;
|
|
423
|
+
const warnings = [];
|
|
424
|
+
if (!primaryStashDir || options.dryRun)
|
|
425
|
+
return { warnings };
|
|
426
|
+
const preEnsureEntryCount = probeEntryCount();
|
|
683
427
|
let ensureIndexDurationMs;
|
|
684
428
|
try {
|
|
685
|
-
await ensureIndexFn(primaryStashDir, {
|
|
429
|
+
await run.ensureIndexFn(primaryStashDir, {
|
|
686
430
|
mode: "blocking",
|
|
687
431
|
signal,
|
|
688
|
-
// R6: capture the implicit reindex's wall-clock cost (otherwise
|
|
689
|
-
// discarded) so the caller can surface it on the improve result.
|
|
690
432
|
onReindexTiming: ({ durationMs }) => {
|
|
691
433
|
ensureIndexDurationMs = durationMs;
|
|
692
434
|
},
|
|
@@ -697,72 +439,37 @@ async function runIndexBootstrapPass(run, signal) {
|
|
|
697
439
|
throw err;
|
|
698
440
|
warnings.push(`ensureIndex failed: ${errMessage(err)}`);
|
|
699
441
|
}
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
// that on stderr so the improve run is not silently masked by stale
|
|
703
|
-
// index state. Zero-before AND zero-after is the empty-stash case and
|
|
704
|
-
// is intentionally not warned (not a bug).
|
|
705
|
-
if (preEnsureEntryCount === 0) {
|
|
706
|
-
try {
|
|
707
|
-
const probeDb = openExistingDatabase();
|
|
708
|
-
let postCount = 0;
|
|
709
|
-
try {
|
|
710
|
-
postCount = getEntryCount(probeDb);
|
|
711
|
-
}
|
|
712
|
-
finally {
|
|
713
|
-
closeDatabase(probeDb);
|
|
714
|
-
}
|
|
715
|
-
if (postCount > 0) {
|
|
716
|
-
warn("[improve] index was empty after DB version upgrade — repopulating before continuing");
|
|
717
|
-
}
|
|
718
|
-
}
|
|
719
|
-
catch (err) {
|
|
720
|
-
rethrowIfTestIsolationError(err);
|
|
721
|
-
// best-effort
|
|
722
|
-
}
|
|
442
|
+
if (preEnsureEntryCount === 0 && (probeEntryCount() ?? 0) > 0) {
|
|
443
|
+
warn("[improve] index was empty after DB version upgrade — repopulating before continuing");
|
|
723
444
|
}
|
|
724
445
|
return { warnings, ensureIndexDurationMs };
|
|
725
446
|
}
|
|
726
|
-
/**
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
* function used to open with now runs earlier, before triage — see
|
|
731
|
-
* {@link runIndexBootstrapPass} and its call site in `akmImprove`.
|
|
732
|
-
*/
|
|
733
|
-
async function indexAndCollect(args) {
|
|
734
|
-
const { scope, options, primaryStashDir, improveProfile, _earlyConfig, collectEligibleRefsImpl } = args.run;
|
|
735
|
-
const { plannedRefs, memorySummary, strategyFilteredRefs = [], indexSnapshot, } = await collectEligibleRefsImpl(scope, options.stashDir, improveProfile, _earlyConfig);
|
|
447
|
+
/** Candidate selection plus the memory-cleanup plan and the autonomy-gated direct lanes. */
|
|
448
|
+
async function indexAndCollect(run) {
|
|
449
|
+
const { scope, options, primaryStashDir, improveProfile, config } = run;
|
|
450
|
+
const { plannedRefs, memorySummary, strategyFilteredRefs, indexSnapshot } = await run.collectEligibleRefsImpl(scope, options.stashDir, improveProfile, config);
|
|
736
451
|
const cleanupParentRef = memoryCleanupParentRef(scope, options.stashDir);
|
|
737
|
-
|
|
738
|
-
// and eligible-memory count and no strategy flag at all. Compute it once: it
|
|
739
|
-
// decides whether each lane would have run, which is also what decides whether
|
|
740
|
-
// a gated lane is worth REPORTING. A lane that was never eligible was not
|
|
741
|
-
// suppressed by the gate, so claiming it was would be noise.
|
|
742
|
-
const cleanupEligible = shouldAnalyzeMemoryCleanup(scope, memorySummary.eligible, primaryStashDir);
|
|
743
|
-
// Cleanup remains independent of the disabled contradiction writer.
|
|
744
|
-
const memoryCleanupPlan = cleanupEligible
|
|
452
|
+
const memoryCleanupPlan = shouldAnalyzeMemoryCleanup(scope, memorySummary.eligible, primaryStashDir)
|
|
745
453
|
? analyzeMemoryCleanup(primaryStashDir, cleanupParentRef ? { parentRef: cleanupParentRef } : undefined)
|
|
746
454
|
: undefined;
|
|
455
|
+
// A lane that would not have run anyway was not suppressed by the gate.
|
|
747
456
|
const cleanupWouldMutate = Boolean(memoryCleanupPlan &&
|
|
748
457
|
(memoryCleanupPlan.pruneCandidates.length > 0 ||
|
|
749
458
|
memoryCleanupPlan.beliefStateTransitions.length > 0 ||
|
|
750
459
|
memoryCleanupPlan.relativeDateCandidates.length > 0));
|
|
751
|
-
const autonomyGatedDirectLanes = cleanupWouldMutate && !isAutonomyLaneAllowed("memoryCleanup", _earlyConfig) ? ["memoryCleanup"] : [];
|
|
752
|
-
const guidance = memorySummary.eligible > 0
|
|
753
|
-
? "Improve folds memory cleanup into the same proposal queue: speculative promotions still go through reflect/distill proposals, while high-confidence redundant derived memories are moved into a recoverable cleanup archive instead of being left active in the stash."
|
|
754
|
-
: undefined;
|
|
755
460
|
return {
|
|
756
461
|
plannedRefs,
|
|
757
462
|
memorySummary,
|
|
758
463
|
strategyFilteredRefs: strategyFilteredRefs ?? [],
|
|
759
464
|
indexSnapshot,
|
|
760
465
|
memoryCleanupPlan,
|
|
761
|
-
autonomyGatedDirectLanes,
|
|
762
|
-
guidance
|
|
466
|
+
autonomyGatedDirectLanes: cleanupWouldMutate && !isAutonomyLaneAllowed("memoryCleanup", config) ? ["memoryCleanup"] : [],
|
|
467
|
+
guidance: memorySummary.eligible > 0
|
|
468
|
+
? "Improve folds memory cleanup into the same proposal queue: speculative promotions still go through reflect/distill proposals, while high-confidence redundant derived memories are moved into a recoverable cleanup archive instead of being left active in the stash."
|
|
469
|
+
: undefined,
|
|
763
470
|
};
|
|
764
471
|
}
|
|
765
|
-
/** The
|
|
472
|
+
/** The envelope for a run skipped because another run holds the lock. */
|
|
766
473
|
export function buildLockSkippedResult(strategyName, scope, runId) {
|
|
767
474
|
return {
|
|
768
475
|
schemaVersion: 2,
|
|
@@ -777,14 +484,13 @@ export function buildLockSkippedResult(strategyName, scope, runId) {
|
|
|
777
484
|
...(runId !== undefined ? { runId } : {}),
|
|
778
485
|
};
|
|
779
486
|
}
|
|
780
|
-
/**
|
|
781
|
-
async function runDryPlanningStage(
|
|
782
|
-
const { run, collected, resolvedStateDbPath, budgetMs, initialCleanupWarnings, signal } = args;
|
|
487
|
+
/** A dry run: the real selectors on a read-only state.db snapshot, stopping before every writer. */
|
|
488
|
+
async function runDryPlanningStage(run, collected, initialCleanupWarnings) {
|
|
783
489
|
let stateDb;
|
|
784
490
|
let stateSnapshotUnavailable = false;
|
|
785
491
|
try {
|
|
786
492
|
try {
|
|
787
|
-
stateDb = openSqliteReadSnapshot(resolvedStateDbPath);
|
|
493
|
+
stateDb = openSqliteReadSnapshot(run.resolvedStateDbPath);
|
|
788
494
|
stateSnapshotUnavailable = !stateDb;
|
|
789
495
|
}
|
|
790
496
|
catch (error) {
|
|
@@ -792,27 +498,13 @@ async function runDryPlanningStage(args) {
|
|
|
792
498
|
throw error;
|
|
793
499
|
stateSnapshotUnavailable = true;
|
|
794
500
|
}
|
|
795
|
-
const eventsCtx = {
|
|
796
|
-
...(stateDb ? { db: stateDb } : { dbPath: resolvedStateDbPath }),
|
|
797
|
-
readOnly: true,
|
|
798
|
-
...(stateSnapshotUnavailable ? { readOnlySnapshotUnavailable: true } : {}),
|
|
799
|
-
};
|
|
800
501
|
const preparation = await run.runImprovePreparationStageImpl({
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
reindexFn: run.reindexFn,
|
|
808
|
-
startMs: run.startMs,
|
|
809
|
-
budgetMs,
|
|
810
|
-
eventsCtx,
|
|
811
|
-
initialCleanupWarnings,
|
|
812
|
-
improveProfile: run.improveProfile,
|
|
813
|
-
resolvedPlan: run.resolvedPlan,
|
|
814
|
-
strategyName: run.selectedStrategy.name,
|
|
815
|
-
budgetSignal: signal,
|
|
502
|
+
...preparationArgs(run, collected, initialCleanupWarnings),
|
|
503
|
+
eventsCtx: {
|
|
504
|
+
...(stateDb ? { db: stateDb } : { dbPath: run.resolvedStateDbPath }),
|
|
505
|
+
readOnly: true,
|
|
506
|
+
...(stateSnapshotUnavailable ? { readOnlySnapshotUnavailable: true } : {}),
|
|
507
|
+
},
|
|
816
508
|
planOnly: true,
|
|
817
509
|
});
|
|
818
510
|
return buildDryRunResult(run, collected, preparation);
|
|
@@ -821,114 +513,101 @@ async function runDryPlanningStage(args) {
|
|
|
821
513
|
stateDb?.close();
|
|
822
514
|
}
|
|
823
515
|
}
|
|
824
|
-
|
|
516
|
+
function preparationArgs(run, collected, initialCleanupWarnings) {
|
|
517
|
+
return {
|
|
518
|
+
scope: run.scope,
|
|
519
|
+
options: run.options,
|
|
520
|
+
plannedRefs: collected.plannedRefs,
|
|
521
|
+
memoryCleanupPlan: collected.memoryCleanupPlan,
|
|
522
|
+
primaryStashDir: run.primaryStashDir,
|
|
523
|
+
memorySummary: collected.memorySummary,
|
|
524
|
+
reindexFn: run.reindexFn,
|
|
525
|
+
startMs: run.startMs,
|
|
526
|
+
budgetMs: run.budgetMs,
|
|
527
|
+
initialCleanupWarnings,
|
|
528
|
+
improveProfile: run.improveProfile,
|
|
529
|
+
resolvedPlan: run.resolvedPlan,
|
|
530
|
+
strategyName: run.selectedStrategy.name,
|
|
531
|
+
budgetSignal: run.budgetAbortController.signal,
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
/** The plan-only envelope of a dry run. */
|
|
825
535
|
export function buildDryRunResult(run, collected, preparation) {
|
|
826
|
-
const { selectedStrategy, scope } = run;
|
|
827
536
|
const { guidance, memorySummary, memoryCleanupPlan, plannedRefs, strategyFilteredRefs } = collected;
|
|
828
|
-
const
|
|
829
|
-
const notices = collectImproveNotices({ resolvedPlan: run.resolvedPlan });
|
|
537
|
+
const notices = collectImproveNotices(run.resolvedPlan, []);
|
|
830
538
|
return {
|
|
831
539
|
schemaVersion: 2,
|
|
832
540
|
ok: true,
|
|
833
|
-
strategy: selectedStrategy.name,
|
|
834
|
-
scope,
|
|
541
|
+
strategy: run.selectedStrategy.name,
|
|
542
|
+
scope: run.scope,
|
|
835
543
|
dryRun: true,
|
|
836
|
-
...
|
|
544
|
+
...notices.fields(),
|
|
837
545
|
...(guidance ? { guidance } : {}),
|
|
838
546
|
memorySummary,
|
|
839
547
|
...(memoryCleanupPlan ? { memoryCleanup: shapeMemoryCleanup(memoryCleanupPlan) } : {}),
|
|
840
|
-
plannedRefs:
|
|
841
|
-
...(preparation?.planning
|
|
842
|
-
? {
|
|
843
|
-
plan: buildResultExecutionPlan(run, preparation, plannedRefs, strategyFilteredRefs, collected.indexSnapshot, true),
|
|
844
|
-
}
|
|
845
|
-
: {}),
|
|
548
|
+
plannedRefs: preparation?.loopRefs ?? plannedRefs,
|
|
549
|
+
...(preparation?.planning ? { plan: buildResultExecutionPlan(run, preparation, collected, true) } : {}),
|
|
846
550
|
...(strategyFilteredRefs.length > 0 ? { strategyFilteredRefs } : {}),
|
|
847
551
|
...(run.resolvedPlan.engineUnavailable.length > 0 ? { skippedProcesses: run.resolvedPlan.engineUnavailable } : {}),
|
|
848
552
|
...(run.options.engineProbe !== undefined ? { engineProbe: run.options.engineProbe } : {}),
|
|
849
553
|
...(preparation?.proactiveMaintenance ? { proactiveMaintenance: preparation.proactiveMaintenance } : {}),
|
|
850
554
|
};
|
|
851
555
|
}
|
|
852
|
-
/**
|
|
853
|
-
function buildResultExecutionPlan(run, preparation,
|
|
854
|
-
const { improveProfile, configuredImproveProfile, resolvedPlan,
|
|
855
|
-
const
|
|
556
|
+
/** The public execution-plan projection shared by the dry and live results. */
|
|
557
|
+
function buildResultExecutionPlan(run, preparation, collected, dryRun) {
|
|
558
|
+
const { improveProfile, configuredImproveProfile, resolvedPlan, scope } = run;
|
|
559
|
+
const { strategyFilteredRefs } = collected;
|
|
856
560
|
const configuredTriage = configuredImproveProfile.processes?.triage;
|
|
857
|
-
const configuredProactive = configuredImproveProfile.processes?.proactiveMaintenance;
|
|
858
|
-
const configuredConsolidation = configuredImproveProfile.processes?.consolidate;
|
|
859
|
-
const triageEnabled = scope.mode !== "ref" && resolvedPlan.processes.triage.enabled;
|
|
860
|
-
const distillOnlySet = new Set(preparation.distillOnlyRefs.map((entry) => entry.ref));
|
|
861
561
|
const inferenceMinPending = improveProfile.processes?.memoryInference?.minPendingCount;
|
|
862
562
|
const pendingMemories = run.primaryStashDir && inferenceMinPending !== undefined && inferenceMinPending > 0
|
|
863
563
|
? collectPendingMemories(run.primaryStashDir).length
|
|
864
564
|
: undefined;
|
|
865
|
-
const
|
|
866
|
-
|
|
565
|
+
const belowMinPending = pendingMemories !== undefined && inferenceMinPending !== undefined && pendingMemories < inferenceMinPending;
|
|
566
|
+
const memoryInferenceEnabled = resolvedPlan.processes.memoryInference.enabled && !belowMinPending;
|
|
867
567
|
const graphExtractionEnabled = resolvedPlan.processes.graphExtraction.enabled && run.primaryStashDir !== undefined;
|
|
868
|
-
|
|
869
|
-
name: "profile",
|
|
870
|
-
removed: strategyFilteredRefs.length,
|
|
871
|
-
reason: "all enabled per-ref processes refuse the asset type",
|
|
872
|
-
};
|
|
873
|
-
// #947 — per-process resolved engine/model/notices, plus how many of this
|
|
874
|
-
// run's effective refs each ref-scoped process (reflect/distill/consolidate)
|
|
875
|
-
// would act on. Counts only (not a per-ref matrix) to bound result_json size.
|
|
876
|
-
const REF_SCOPED_PROCESSES = new Set(["reflect", "distill", "consolidate"]);
|
|
568
|
+
// Per-process routing, plus how many effective refs each ref-scoped process would act on (#947).
|
|
877
569
|
const processes = projectResolvedProcessRouting(resolvedPlan).map((row) => {
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
const eligibleRefs = preparation.loopRefs.filter((entry) => !shouldSkipRef(entry.ref, row.process, resolvedPlan.strategy.config)
|
|
881
|
-
.skip).length;
|
|
882
|
-
return { ...row, eligibleRefs };
|
|
570
|
+
const eligibleRefs = eligibleRefCount(preparation.loopRefs, row.process, resolvedPlan.strategy.config);
|
|
571
|
+
return eligibleRefs === undefined ? row : { ...row, eligibleRefs };
|
|
883
572
|
});
|
|
884
573
|
const proactive = preparation.planning.proactive
|
|
885
574
|
? {
|
|
886
575
|
...preparation.planning.proactive,
|
|
887
|
-
configured:
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
576
|
+
configured: pickDefined(configuredImproveProfile.processes?.proactiveMaintenance, [
|
|
577
|
+
"dueDays",
|
|
578
|
+
"maxPerRun",
|
|
579
|
+
"limit",
|
|
580
|
+
]),
|
|
892
581
|
}
|
|
893
582
|
: undefined;
|
|
894
|
-
const consolidation = {
|
|
895
|
-
...preparation.planning.consolidation,
|
|
896
|
-
configured: {
|
|
897
|
-
...(configuredConsolidation?.enabled !== undefined ? { enabled: configuredConsolidation.enabled } : {}),
|
|
898
|
-
...(configuredConsolidation?.minPoolSize !== undefined
|
|
899
|
-
? { minPoolSize: configuredConsolidation.minPoolSize }
|
|
900
|
-
: {}),
|
|
901
|
-
...(configuredConsolidation?.limit !== undefined ? { limit: configuredConsolidation.limit } : {}),
|
|
902
|
-
...(configuredConsolidation?.maxChunkSize !== undefined
|
|
903
|
-
? { maxChunkSize: configuredConsolidation.maxChunkSize }
|
|
904
|
-
: {}),
|
|
905
|
-
...(configuredConsolidation?.incrementalSince !== undefined
|
|
906
|
-
? { incrementalSince: configuredConsolidation.incrementalSince }
|
|
907
|
-
: {}),
|
|
908
|
-
},
|
|
909
|
-
};
|
|
910
583
|
return buildImproveExecutionPlan({
|
|
911
584
|
dryRun,
|
|
912
|
-
snapshot: indexSnapshot ?? {
|
|
585
|
+
snapshot: collected.indexSnapshot ?? {
|
|
913
586
|
status: "unknown",
|
|
914
587
|
reason: "the injected selector did not report an index snapshot status",
|
|
915
588
|
},
|
|
916
|
-
rawInScope:
|
|
589
|
+
rawInScope: collected.plannedRefs.length + strategyFilteredRefs.length,
|
|
917
590
|
selectedRefs: preparation.actionableRefs,
|
|
918
591
|
effectiveRefs: preparation.loopRefs,
|
|
919
|
-
distillOnlyRefs:
|
|
920
|
-
configuredLimits,
|
|
921
|
-
effectiveLimit,
|
|
922
|
-
|
|
923
|
-
|
|
592
|
+
distillOnlyRefs: new Set(preparation.distillOnlyRefs.map((entry) => entry.ref)),
|
|
593
|
+
configuredLimits: run.configuredLimits,
|
|
594
|
+
effectiveLimit: run.effectiveLimit,
|
|
595
|
+
gates: [
|
|
596
|
+
{
|
|
597
|
+
name: "profile",
|
|
598
|
+
removed: strategyFilteredRefs.length,
|
|
599
|
+
reason: "all enabled per-ref processes refuse the asset type",
|
|
600
|
+
},
|
|
601
|
+
...preparation.planning.gates,
|
|
602
|
+
],
|
|
924
603
|
processes,
|
|
925
604
|
...(proactive ? { proactive } : {}),
|
|
926
|
-
consolidation
|
|
605
|
+
consolidation: {
|
|
606
|
+
...preparation.planning.consolidation,
|
|
607
|
+
configured: pickDefined(configuredImproveProfile.processes?.consolidate, CONSOLIDATION_CONFIG_KEYS),
|
|
608
|
+
},
|
|
927
609
|
stageConfig: {
|
|
928
|
-
extract: {
|
|
929
|
-
enabled: preparation.planning.extract.wouldRun,
|
|
930
|
-
reason: preparation.planning.extract.reason,
|
|
931
|
-
},
|
|
610
|
+
extract: { enabled: preparation.planning.extract.wouldRun, reason: preparation.planning.extract.reason },
|
|
932
611
|
graphExtraction: {
|
|
933
612
|
enabled: graphExtractionEnabled,
|
|
934
613
|
reason: !resolvedPlan.processes.graphExtraction.enabled
|
|
@@ -949,78 +628,51 @@ function buildResultExecutionPlan(run, preparation, rawProfileEligibleRefs, stra
|
|
|
949
628
|
},
|
|
950
629
|
},
|
|
951
630
|
triage: {
|
|
952
|
-
enabled:
|
|
631
|
+
enabled: scope.mode !== "ref" && resolvedPlan.processes.triage.enabled,
|
|
953
632
|
configuredMode: configuredTriage?.applyMode ?? "queue",
|
|
954
|
-
mode:
|
|
633
|
+
mode: improveProfile.processes?.triage?.applyMode ?? "queue",
|
|
955
634
|
maxAcceptsPerRun: configuredTriage?.maxAcceptsPerRun ?? 25,
|
|
956
|
-
...(configuredTriage?.maxDiffLines !== undefined ? { maxDiffLines: configuredTriage.maxDiffLines } : {}),
|
|
957
635
|
},
|
|
958
636
|
});
|
|
959
637
|
}
|
|
960
|
-
/**
|
|
638
|
+
/** Drain the proposal backlog before generating more (non-fatal; a single-ref scope never drains). */
|
|
961
639
|
async function runTriagePrePass(run) {
|
|
962
|
-
const { primaryStashDir, resolvedPlan, scope, options, improveProfile
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
}), { engine: resolvedPlan.triageJudgment?.engine, process: "triage.judgment" });
|
|
986
|
-
}
|
|
987
|
-
catch (err) {
|
|
988
|
-
warn(`[improve] triage pre-pass failed (non-fatal): ${errMessage(err)}`);
|
|
989
|
-
}
|
|
990
|
-
}
|
|
640
|
+
const { primaryStashDir, resolvedPlan, scope, options, improveProfile } = run;
|
|
641
|
+
if (!primaryStashDir || !resolvedPlan.processes.triage.enabled)
|
|
642
|
+
return undefined;
|
|
643
|
+
if (scope.mode === "ref") {
|
|
644
|
+
warn("[improve] triage pre-pass skipped (single-ref scope never drains the whole queue)");
|
|
645
|
+
return undefined;
|
|
646
|
+
}
|
|
647
|
+
try {
|
|
648
|
+
const triageConfig = improveProfile.processes?.triage;
|
|
649
|
+
return await withLlmStage("triage", () => run.drainProposalsFn({
|
|
650
|
+
stashDir: primaryStashDir,
|
|
651
|
+
...(options.target ? { target: options.target } : {}),
|
|
652
|
+
config: options.config,
|
|
653
|
+
applyMode: triageConfig?.applyMode ?? "queue",
|
|
654
|
+
maxAccepts: triageConfig?.maxAcceptsPerRun ?? 25,
|
|
655
|
+
dryRun: false,
|
|
656
|
+
excludeIds: new Set(),
|
|
657
|
+
judgment: resolvedPlan.triageJudgment,
|
|
658
|
+
}), { engine: resolvedPlan.triageJudgment?.engine, process: "triage.judgment" });
|
|
659
|
+
}
|
|
660
|
+
catch (err) {
|
|
661
|
+
warn(`[improve] triage pre-pass failed (non-fatal): ${errMessage(err)}`);
|
|
662
|
+
return undefined;
|
|
991
663
|
}
|
|
992
|
-
return triageDrain;
|
|
993
|
-
}
|
|
994
|
-
/** A path is never staged by auto-sync when it is (or looks like) a lock file. */
|
|
995
|
-
function isSyncExcludedPath(relativePath) {
|
|
996
|
-
return path.basename(relativePath).includes(".lock");
|
|
997
664
|
}
|
|
998
665
|
/**
|
|
999
|
-
*
|
|
1000
|
-
*
|
|
1001
|
-
*
|
|
1002
|
-
*
|
|
1003
|
-
*
|
|
1004
|
-
*
|
|
1005
|
-
* Git currently reports as changed. The intersection is what makes the set
|
|
1006
|
-
* correct rather than merely plausible: it drops a journaled path whose
|
|
1007
|
-
* final bytes match HEAD (nothing to commit), a journaled path that was
|
|
1008
|
-
* created and then removed again (never existed for Git), and any journaled
|
|
1009
|
-
* path Git ignores (which `saveGitStash` would otherwise reject outright).
|
|
1010
|
-
* A journaled path that was ALREADY dirty when the run started stays IN —
|
|
1011
|
-
* this run rewrote it, so this run owns it.
|
|
1012
|
-
* 2. **Dirty-path diff** (pre-#652 behaviour) — only when no journal was open,
|
|
1013
|
-
* which a live run never hits. Kept as the defensive fallback so a future
|
|
1014
|
-
* caller that commits outside the journal window degrades to the previous,
|
|
1015
|
-
* battle-tested heuristic instead of silently committing nothing.
|
|
1016
|
-
*
|
|
1017
|
-
* `unattributed` counts in-scope paths that became dirty DURING the run without
|
|
1018
|
-
* this run writing them: concurrent human edits (correctly excluded) and, if the
|
|
1019
|
-
* number is ever surprising, the fingerprint of a missing journal call site.
|
|
666
|
+
* The exact repo-relative paths the auto-sync commit stages (#652): the paths
|
|
667
|
+
* this run wrote that Git reports changed (a journaled path already dirty at
|
|
668
|
+
* start stays in — this run rewrote it). Without a journal, the pre-#652
|
|
669
|
+
* dirty-path diff. `unattributed` counts in-scope paths that went dirty during
|
|
670
|
+
* the run without this run writing them (concurrent edits, left alone). Lock
|
|
671
|
+
* files are never staged.
|
|
1020
672
|
*/
|
|
1021
673
|
export function resolveSyncPathSet(input) {
|
|
1022
674
|
const { repoDir, assetPrefix, changedPaths, initialPaths, writtenPaths, provenance } = input;
|
|
1023
|
-
const inScope = (relativePath) => !
|
|
675
|
+
const inScope = (relativePath) => !path.basename(relativePath).includes(".lock") &&
|
|
1024
676
|
(!assetPrefix || relativePath === assetPrefix || relativePath.startsWith(`${assetPrefix}/`));
|
|
1025
677
|
if (!provenance) {
|
|
1026
678
|
return { paths: changedPaths.filter((p) => !initialPaths.has(p) && inScope(p)), unattributed: [] };
|
|
@@ -1029,9 +681,8 @@ export function resolveSyncPathSet(input) {
|
|
|
1029
681
|
const attributed = new Set();
|
|
1030
682
|
for (const absolutePath of writtenPaths) {
|
|
1031
683
|
const relativePath = relativeWrittenPath(repoDir, absolutePath);
|
|
1032
|
-
if (
|
|
1033
|
-
|
|
1034
|
-
attributed.add(relativePath);
|
|
684
|
+
if (relativePath && changed.has(relativePath) && inScope(relativePath))
|
|
685
|
+
attributed.add(relativePath);
|
|
1035
686
|
}
|
|
1036
687
|
return {
|
|
1037
688
|
paths: [...attributed].sort(),
|
|
@@ -1039,60 +690,46 @@ export function resolveSyncPathSet(input) {
|
|
|
1039
690
|
};
|
|
1040
691
|
}
|
|
1041
692
|
/**
|
|
1042
|
-
*
|
|
1043
|
-
*
|
|
1044
|
-
*
|
|
1045
|
-
* path. Idempotent + NON-FATAL. `getEventsCtx`/`getInitialGitPaths`/
|
|
1046
|
-
* `getWriteJournal` are getters because those outer bindings are (re)assigned
|
|
1047
|
-
* after this factory runs — the returned closure must observe the live values.
|
|
693
|
+
* The auto-sync commit (#662), used at end of run and from the crash path:
|
|
694
|
+
* idempotent and never throws. The getters read bindings reassigned after this
|
|
695
|
+
* factory runs.
|
|
1048
696
|
*/
|
|
1049
697
|
function makeCommitStashBatch(deps) {
|
|
1050
|
-
const { writeTarget, primaryStashDir, effectiveSync, options,
|
|
698
|
+
const { writeTarget, primaryStashDir, effectiveSync, options, config, improveProfile } = deps.run;
|
|
1051
699
|
return (messageContext) => {
|
|
1052
700
|
const eventsCtx = deps.getEventsCtx();
|
|
1053
|
-
const initialGitPaths = deps.getInitialGitPaths();
|
|
1054
701
|
const writeJournal = deps.getWriteJournal();
|
|
1055
702
|
const repoDir = writeTarget?.source.repoPath ?? primaryStashDir;
|
|
1056
703
|
if (!primaryStashDir || !repoDir || effectiveSync.enabled === false || !isGitBackedStash(repoDir)) {
|
|
1057
704
|
return undefined;
|
|
1058
705
|
}
|
|
1059
706
|
const saveGitStashFn = options.saveGitStashFn ?? saveGitStash;
|
|
1060
|
-
const writableOverride = writeTarget ? resolveWritable(writeTarget.config) : resolveWritableOverride(
|
|
707
|
+
const writableOverride = writeTarget ? resolveWritable(writeTarget.config) : resolveWritableOverride(config);
|
|
1061
708
|
const push = options.sync?.push ?? improveProfile.sync?.push ?? true;
|
|
1062
709
|
const message = renderSyncCommitMessage(effectiveSync.message ?? "akm improve auto-sync", messageContext, Date.now());
|
|
710
|
+
const record = (metadata) => appendEvent({ eventType: "stash_synced", metadata }, eventsCtx);
|
|
1063
711
|
try {
|
|
1064
712
|
const assetRoot = writeTarget?.source.path ?? primaryStashDir;
|
|
1065
|
-
const assetPrefix = assetRoot ? path.relative(repoDir, assetRoot).replaceAll(path.sep, "/") : "";
|
|
1066
713
|
const { paths, unattributed } = resolveSyncPathSet({
|
|
1067
714
|
repoDir,
|
|
1068
|
-
assetPrefix,
|
|
715
|
+
assetPrefix: path.relative(repoDir, assetRoot).replaceAll(path.sep, "/"),
|
|
1069
716
|
changedPaths: listGitChangedPaths(repoDir),
|
|
1070
|
-
initialPaths:
|
|
717
|
+
initialPaths: deps.getInitialGitPaths(),
|
|
1071
718
|
writtenPaths: writeJournal?.writtenPaths() ?? [],
|
|
1072
719
|
provenance: writeJournal !== undefined,
|
|
1073
720
|
});
|
|
1074
721
|
if (unattributed.length > 0) {
|
|
1075
722
|
warnVerbose(`[improve] auto-sync left ${unattributed.length} path(s) uncommitted — not written by this run: ${unattributed.join(", ")}`);
|
|
1076
723
|
}
|
|
1077
|
-
const syncResult = saveGitStashFn(undefined, message, writableOverride, {
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
724
|
+
const syncResult = saveGitStashFn(undefined, message, writableOverride, { push, repoDir, paths });
|
|
725
|
+
record({
|
|
726
|
+
committed: syncResult.committed,
|
|
727
|
+
pushed: syncResult.pushed,
|
|
728
|
+
skipped: syncResult.skipped,
|
|
729
|
+
reason: syncResult.reason ?? null,
|
|
730
|
+
attributed: paths.length,
|
|
731
|
+
unattributed: unattributed.length,
|
|
1081
732
|
});
|
|
1082
|
-
appendEvent({
|
|
1083
|
-
eventType: "stash_synced",
|
|
1084
|
-
metadata: {
|
|
1085
|
-
committed: syncResult.committed,
|
|
1086
|
-
pushed: syncResult.pushed,
|
|
1087
|
-
skipped: syncResult.skipped,
|
|
1088
|
-
reason: syncResult.reason ?? null,
|
|
1089
|
-
// #652 provenance audit trail: how many paths this run staged, and
|
|
1090
|
-
// how many in-scope paths went dirty during the run that it did NOT
|
|
1091
|
-
// write (concurrent edits — deliberately left for their author).
|
|
1092
|
-
attributed: paths.length,
|
|
1093
|
-
unattributed: unattributed.length,
|
|
1094
|
-
},
|
|
1095
|
-
}, eventsCtx);
|
|
1096
733
|
return {
|
|
1097
734
|
committed: syncResult.committed,
|
|
1098
735
|
pushed: syncResult.pushed,
|
|
@@ -1103,296 +740,131 @@ function makeCommitStashBatch(deps) {
|
|
|
1103
740
|
catch (syncErr) {
|
|
1104
741
|
const reason = errMessage(syncErr);
|
|
1105
742
|
warn(`improve: stash sync failed (non-fatal): ${reason}`);
|
|
1106
|
-
|
|
1107
|
-
eventType: "stash_synced",
|
|
1108
|
-
metadata: { committed: false, pushed: false, skipped: true, reason },
|
|
1109
|
-
}, eventsCtx);
|
|
743
|
+
record({ committed: false, pushed: false, skipped: true, reason });
|
|
1110
744
|
return { committed: false, pushed: false, skipped: true, reason };
|
|
1111
745
|
}
|
|
1112
746
|
};
|
|
1113
747
|
}
|
|
1114
|
-
/** D6: pre-load the last 30 days of proposal_rejected events once per run. */
|
|
1115
|
-
function preloadRejectedProposals() {
|
|
1116
|
-
// D6: pre-load all proposal_rejected events from the last 30 days once,
|
|
1117
|
-
// so the per-asset loop can use a Map lookup instead of N DB round trips.
|
|
1118
|
-
const REJECTED_PROPOSAL_WINDOW_MS = daysToMs(30);
|
|
1119
|
-
const rejectedProposalSince = new Date(Date.now() - REJECTED_PROPOSAL_WINDOW_MS).toISOString();
|
|
1120
|
-
const allRejectedProposalEvents = readEvents({ type: "proposal_rejected", since: rejectedProposalSince }).events;
|
|
1121
|
-
const rejectedProposalsByRef = new Map();
|
|
1122
|
-
for (const e of allRejectedProposalEvents) {
|
|
1123
|
-
if (e.ref && (!rejectedProposalsByRef.has(e.ref) || e.ts > (rejectedProposalsByRef.get(e.ref)?.ts ?? ""))) {
|
|
1124
|
-
rejectedProposalsByRef.set(e.ref, e);
|
|
1125
|
-
}
|
|
1126
|
-
}
|
|
1127
|
-
return rejectedProposalsByRef;
|
|
1128
|
-
}
|
|
1129
748
|
/**
|
|
1130
|
-
*
|
|
1131
|
-
*
|
|
1132
|
-
* before this run acquired its lock are visible.
|
|
749
|
+
* Re-read the improve ledger under the lock and drop proactive refs another run
|
|
750
|
+
* attempted after this one planned.
|
|
1133
751
|
*/
|
|
1134
|
-
export function refilterProactiveLoopRefs(loopRefs, improveProfile) {
|
|
1135
|
-
// Re-read cooldown timestamps immediately before execution so external
|
|
1136
|
-
// proposal writes that occurred before this run acquired its lock are visible.
|
|
752
|
+
export function refilterProactiveLoopRefs(loopRefs, improveProfile, ledgerAccess) {
|
|
1137
753
|
const proactiveLoopRefs = loopRefs.filter((r) => r.eligibilitySource === "proactive");
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
postLockLoopRefs = loopRefs.filter((r) => r.eligibilitySource !== "proactive" || stillDue.has(r.ref));
|
|
1151
|
-
}
|
|
1152
|
-
}
|
|
1153
|
-
return postLockLoopRefs;
|
|
754
|
+
if (proactiveLoopRefs.length === 0 || !ledgerAccess.stashDir)
|
|
755
|
+
return loopRefs;
|
|
756
|
+
const ledger = loadLedgerSnapshot({ eventsCtx: ledgerAccess.eventsCtx }, ledgerAccess.stashDir, [
|
|
757
|
+
"reflect",
|
|
758
|
+
"distill",
|
|
759
|
+
]);
|
|
760
|
+
const stillDue = new Set(filterProactiveDue(proactiveLoopRefs, lastAttemptByRef(ledger, "reflect", proactiveLoopRefs), lastAttemptByRef(ledger, "distill", proactiveLoopRefs), improveProfile.processes?.proactiveMaintenance?.dueDays ?? DEFAULT_DUE_DAYS, Date.now()).map((r) => r.ref));
|
|
761
|
+
const dropped = proactiveLoopRefs.filter((r) => !stillDue.has(r.ref));
|
|
762
|
+
if (dropped.length === 0)
|
|
763
|
+
return loopRefs;
|
|
764
|
+
info(`[improve] post-lock cooldown re-filter: dropped ${dropped.length} proactive ref(s) claimed by concurrent run (${dropped.map((r) => r.ref).join(", ")})`);
|
|
765
|
+
return loopRefs.filter((r) => r.eligibilitySource !== "proactive" || stillDue.has(r.ref));
|
|
1154
766
|
}
|
|
1155
767
|
/**
|
|
1156
|
-
*
|
|
1157
|
-
*
|
|
1158
|
-
*
|
|
1159
|
-
* `runImproveStageSequence` — fn-size decomposition, no logic change).
|
|
768
|
+
* The audit events for refs and lanes this run will not touch, then
|
|
769
|
+
* preparation → loop → post-loop. No post-loop work starts past the budget; the
|
|
770
|
+
* result still finalizes, so budget exhaustion exits 0.
|
|
1160
771
|
*/
|
|
1161
|
-
async function
|
|
1162
|
-
const {
|
|
1163
|
-
|
|
1164
|
-
//
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
if (budgetAbortController.signal.aborted || (remainingBudget !== undefined && remainingBudget <= 0)) {
|
|
1172
|
-
info("[improve] post-loop maintenance skipped (wall-clock budget exhausted)");
|
|
1173
|
-
return emptyPostLoopResult;
|
|
772
|
+
async function runImproveStageSequence(run, collected, preEnsureCleanupWarnings, eventsCtx) {
|
|
773
|
+
const { scope, options, primaryStashDir, improveProfile, resolvedPlan, budgetAbortController } = run;
|
|
774
|
+
const strategy = run.selectedStrategy.name;
|
|
775
|
+
// One count-only row for planner-filtered refs, never one per ref (#592).
|
|
776
|
+
if (collected.strategyFilteredRefs.length > 0) {
|
|
777
|
+
recordImproveSkip(eventsCtx, undefined, {
|
|
778
|
+
strategy,
|
|
779
|
+
reason: "strategy_filtered_all_passes",
|
|
780
|
+
count: collected.strategyFilteredRefs.length,
|
|
781
|
+
});
|
|
1174
782
|
}
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
783
|
+
// A gated lane names the config key that would enable it.
|
|
784
|
+
for (const lane of [...resolvedPlan.autonomyGated, ...describeGatedLanes(collected.autonomyGatedDirectLanes)]) {
|
|
785
|
+
warn(`[improve] ${lane.lane} skipped — it ${lane.reason}. Set \`${lane.configKey}: true\` to enable it.`);
|
|
786
|
+
recordImproveSkip(eventsCtx, undefined, {
|
|
787
|
+
strategy,
|
|
788
|
+
reason: "autonomy_gated",
|
|
789
|
+
lane: lane.lane,
|
|
790
|
+
configKey: lane.configKey,
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
for (const item of resolvedPlan.engineUnavailable) {
|
|
794
|
+
warn(`[improve] ${item.process} skipped — it ${item.reason}.`);
|
|
795
|
+
recordImproveSkip(eventsCtx, undefined, {
|
|
796
|
+
strategy,
|
|
797
|
+
reason: "engine_unavailable",
|
|
798
|
+
process: item.process,
|
|
799
|
+
configKey: item.configKey,
|
|
800
|
+
});
|
|
801
|
+
}
|
|
802
|
+
const preparation = await run.runImprovePreparationStageImpl({
|
|
803
|
+
...preparationArgs(run, collected, preEnsureCleanupWarnings),
|
|
804
|
+
eventsCtx,
|
|
805
|
+
});
|
|
806
|
+
const loopResult = await run.runImproveLoopStageImpl({
|
|
1183
807
|
eventsCtx,
|
|
1184
808
|
budgetSignal: budgetAbortController.signal,
|
|
809
|
+
primaryStashDir,
|
|
810
|
+
scope,
|
|
811
|
+
options,
|
|
812
|
+
reflectFn: run.reflectFn,
|
|
813
|
+
distillFn: run.distillFn,
|
|
814
|
+
loopRefs: refilterProactiveLoopRefs(preparation.loopRefs, improveProfile, {
|
|
815
|
+
stashDir: primaryStashDir ?? options.stashDir,
|
|
816
|
+
eventsCtx,
|
|
817
|
+
}),
|
|
818
|
+
actions: preparation.actions,
|
|
819
|
+
signalBearingSet: preparation.signalBearingSet,
|
|
820
|
+
distillCooledRefs: preparation.distillCooledRefs,
|
|
821
|
+
distillOnlyRefs: preparation.distillOnlyRefs,
|
|
822
|
+
recentErrors: preparation.recentErrors,
|
|
823
|
+
startMs: run.startMs,
|
|
824
|
+
budgetMs: run.budgetMs,
|
|
1185
825
|
improveProfile,
|
|
1186
826
|
resolvedPlan,
|
|
1187
|
-
consolidationRan: preparation.consolidationRan,
|
|
1188
|
-
// R5: floor violations from this run's consolidate pass, for the collapse
|
|
1189
|
-
// detector's merge-floor advisory.
|
|
1190
|
-
consolidationMergeFloorViolations: preparation.consolidation.mergeFloorViolations ?? 0,
|
|
1191
827
|
});
|
|
1192
|
-
}
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
* accumulator the result assembly reads — the old closure-scoped `let`s.
|
|
1197
|
-
*/
|
|
1198
|
-
async function runImproveStageSequence(args) {
|
|
1199
|
-
const { strategyFilteredRefs, plannedRefs, memoryCleanupPlan, memorySummary, preEnsureCleanupWarnings, eventsCtx, ctx, } = args;
|
|
1200
|
-
const { scope, options, primaryStashDir, startMs, budgetMs, improveProfile, resolvedPlan, budgetAbortController, selectedStrategy, reflectFn, distillFn, reindexFn, runImprovePreparationStageImpl, runImproveLoopStageImpl, runImprovePostLoopStageImpl, } = args.run;
|
|
1201
|
-
// 2026-05-27: one summary `improve_skipped` audit event (count only) for
|
|
1202
|
-
// planner-pre-filtered refs — never per-ref (#592: O(n) sequential state.db
|
|
1203
|
-
// writes cost ~500 s on a 9 000-ref stash; health reads the counters).
|
|
1204
|
-
if (strategyFilteredRefs.length > 0) {
|
|
1205
|
-
appendEvent({
|
|
1206
|
-
eventType: "improve_skipped",
|
|
1207
|
-
ref: undefined,
|
|
1208
|
-
metadata: {
|
|
1209
|
-
strategy: selectedStrategy.name,
|
|
1210
|
-
reason: "strategy_filtered_all_passes",
|
|
1211
|
-
count: strategyFilteredRefs.length,
|
|
1212
|
-
},
|
|
1213
|
-
}, eventsCtx);
|
|
1214
|
-
}
|
|
1215
|
-
// D8 — one event per lane the autonomy gate downgraded, naming the lane AND
|
|
1216
|
-
// the config key that would enable it. This is the difference between a gate
|
|
1217
|
-
// and a silent no-op: whatever the user would have seen happen, they now see
|
|
1218
|
-
// explained. `health` already aggregates `improve_skipped` by reason
|
|
1219
|
-
// (buildImproveSkipSummary), so these surface there without new machinery.
|
|
1220
|
-
for (const lane of [...args.run.resolvedPlan.autonomyGated, ...describeGatedLanes(args.autonomyGatedDirectLanes)]) {
|
|
1221
|
-
warn(`[improve] ${lane.lane} skipped — it ${lane.reason}. Set \`${lane.configKey}: true\` to enable it.`);
|
|
1222
|
-
appendEvent({
|
|
1223
|
-
eventType: "improve_skipped",
|
|
1224
|
-
ref: undefined,
|
|
1225
|
-
metadata: {
|
|
1226
|
-
strategy: selectedStrategy.name,
|
|
1227
|
-
reason: "autonomy_gated",
|
|
1228
|
-
lane: lane.lane,
|
|
1229
|
-
configKey: lane.configKey,
|
|
1230
|
-
},
|
|
1231
|
-
}, eventsCtx);
|
|
1232
|
-
}
|
|
1233
|
-
for (const item of resolvedPlan.engineUnavailable) {
|
|
1234
|
-
warn(`[improve] ${item.process} skipped — it ${item.reason}.`);
|
|
1235
|
-
appendEvent({
|
|
1236
|
-
eventType: "improve_skipped",
|
|
1237
|
-
ref: undefined,
|
|
1238
|
-
metadata: {
|
|
1239
|
-
strategy: selectedStrategy.name,
|
|
1240
|
-
reason: "engine_unavailable",
|
|
1241
|
-
process: item.process,
|
|
1242
|
-
configKey: item.configKey,
|
|
1243
|
-
},
|
|
1244
|
-
}, eventsCtx);
|
|
828
|
+
let postLoop = { allWarnings: [], memoryInferenceDurationMs: 0, graphExtractionDurationMs: 0 };
|
|
829
|
+
const remainingBudget = budgetAbortController.signal.remainingBudgetMs;
|
|
830
|
+
if (budgetAbortController.signal.aborted || (remainingBudget !== undefined && remainingBudget <= 0)) {
|
|
831
|
+
info("[improve] post-loop maintenance skipped (wall-clock budget exhausted)");
|
|
1245
832
|
}
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
let preparation;
|
|
1249
|
-
let memoryRefsForInference = new Set();
|
|
1250
|
-
let consolidation;
|
|
1251
|
-
let memoryInference;
|
|
1252
|
-
let graphExtraction;
|
|
1253
|
-
let cycleMetrics;
|
|
1254
|
-
// Summed counters/durations.
|
|
1255
|
-
let reflectsWithErrorContext = 0;
|
|
1256
|
-
let memoryInferenceDurationMs = 0;
|
|
1257
|
-
let graphExtractionDurationMs = 0;
|
|
1258
|
-
let orphansPurged;
|
|
1259
|
-
let proposalsExpired;
|
|
1260
|
-
// Concatenated arrays.
|
|
1261
|
-
const allWarnings = [];
|
|
1262
|
-
let deadUrls;
|
|
1263
|
-
let deadUrlCoverage;
|
|
1264
|
-
const finalActions = [];
|
|
1265
|
-
{
|
|
1266
|
-
const runPreparation = () => runImprovePreparationStageImpl({
|
|
833
|
+
else {
|
|
834
|
+
postLoop = await run.runImprovePostLoopStageImpl({
|
|
1267
835
|
scope,
|
|
1268
836
|
options,
|
|
1269
|
-
plannedRefs,
|
|
1270
|
-
memoryCleanupPlan,
|
|
1271
837
|
primaryStashDir,
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
838
|
+
actionableRefs: preparation.actionableRefs,
|
|
839
|
+
appliedCleanup: preparation.appliedCleanup,
|
|
840
|
+
cleanupWarnings: preparation.cleanupWarnings,
|
|
841
|
+
memoryRefsForInference: loopResult.memoryRefsForInference,
|
|
1276
842
|
eventsCtx,
|
|
1277
|
-
initialCleanupWarnings: preEnsureCleanupWarnings,
|
|
1278
|
-
improveProfile,
|
|
1279
|
-
resolvedPlan,
|
|
1280
|
-
strategyName: selectedStrategy.name,
|
|
1281
843
|
budgetSignal: budgetAbortController.signal,
|
|
1282
|
-
});
|
|
1283
|
-
preparation = await runPreparation();
|
|
1284
|
-
const rejectedProposalsByRef = preloadRejectedProposals();
|
|
1285
|
-
const runLoop = () => {
|
|
1286
|
-
const postLockLoopRefs = refilterProactiveLoopRefs(preparation.loopRefs, improveProfile);
|
|
1287
|
-
return runImproveLoopStageImpl({
|
|
1288
|
-
ctx,
|
|
1289
|
-
primaryStashDir,
|
|
1290
|
-
scope,
|
|
1291
|
-
options,
|
|
1292
|
-
reflectFn,
|
|
1293
|
-
distillFn,
|
|
1294
|
-
loopRefs: postLockLoopRefs,
|
|
1295
|
-
actions: preparation.actions,
|
|
1296
|
-
signalBearingSet: preparation.signalBearingSet,
|
|
1297
|
-
distillCooledRefs: preparation.distillCooledRefs,
|
|
1298
|
-
distillOnlyRefs: preparation.distillOnlyRefs,
|
|
1299
|
-
recentErrors: preparation.recentErrors,
|
|
1300
|
-
rejectedProposalsByRef,
|
|
1301
|
-
utilityMap: preparation.utilityMap,
|
|
1302
|
-
startMs,
|
|
1303
|
-
budgetMs,
|
|
1304
|
-
improveProfile,
|
|
1305
|
-
resolvedPlan,
|
|
1306
|
-
});
|
|
1307
|
-
};
|
|
1308
|
-
const loopResult = await runLoop();
|
|
1309
|
-
reflectsWithErrorContext += loopResult.reflectsWithErrorContext;
|
|
1310
|
-
memoryRefsForInference = loopResult.memoryRefsForInference;
|
|
1311
|
-
// #551: consolidation now runs in the preparation stage (before extract);
|
|
1312
|
-
// its result and run-flag are read from `preparation`, not the post-loop.
|
|
1313
|
-
consolidation = preparation.consolidation;
|
|
1314
|
-
const postLoopResult = await runPostLoopStageOrSkip({
|
|
1315
|
-
budgetAbortController,
|
|
1316
|
-
scope,
|
|
1317
|
-
options,
|
|
1318
|
-
primaryStashDir,
|
|
1319
|
-
preparation,
|
|
1320
|
-
memoryRefsForInference,
|
|
1321
|
-
eventsCtx,
|
|
1322
844
|
improveProfile,
|
|
1323
845
|
resolvedPlan,
|
|
1324
|
-
runImprovePostLoopStageImpl,
|
|
1325
846
|
});
|
|
1326
|
-
// Result objects (single pass — no cycle accumulation).
|
|
1327
|
-
memoryInference = postLoopResult.memoryInference;
|
|
1328
|
-
graphExtraction = postLoopResult.graphExtraction;
|
|
1329
|
-
if (postLoopResult.cycleMetrics)
|
|
1330
|
-
cycleMetrics = postLoopResult.cycleMetrics;
|
|
1331
|
-
// Summed counters/durations.
|
|
1332
|
-
memoryInferenceDurationMs += postLoopResult.memoryInferenceDurationMs;
|
|
1333
|
-
graphExtractionDurationMs += postLoopResult.graphExtractionDurationMs;
|
|
1334
|
-
if (postLoopResult.orphansPurged !== undefined) {
|
|
1335
|
-
orphansPurged = (orphansPurged ?? 0) + postLoopResult.orphansPurged;
|
|
1336
|
-
}
|
|
1337
|
-
if (postLoopResult.proposalsExpired !== undefined) {
|
|
1338
|
-
proposalsExpired = (proposalsExpired ?? 0) + postLoopResult.proposalsExpired;
|
|
1339
|
-
}
|
|
1340
|
-
// Concatenated arrays.
|
|
1341
|
-
allWarnings.push(...postLoopResult.allWarnings);
|
|
1342
|
-
if (postLoopResult.deadUrls !== undefined) {
|
|
1343
|
-
deadUrls = [...(deadUrls ?? []), ...postLoopResult.deadUrls];
|
|
1344
|
-
}
|
|
1345
|
-
if (postLoopResult.deadUrlCoverage !== undefined) {
|
|
1346
|
-
deadUrlCoverage = postLoopResult.deadUrlCoverage;
|
|
1347
|
-
}
|
|
1348
|
-
const maintenanceActions = postLoopResult.maintenanceActions;
|
|
1349
|
-
if (maintenanceActions && maintenanceActions.length > 0) {
|
|
1350
|
-
finalActions.push(...preparation.actions, ...maintenanceActions);
|
|
1351
|
-
}
|
|
1352
|
-
else {
|
|
1353
|
-
finalActions.push(...preparation.actions);
|
|
1354
|
-
}
|
|
1355
847
|
}
|
|
1356
848
|
return {
|
|
1357
849
|
preparation,
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
graphExtraction,
|
|
1362
|
-
cycleMetrics,
|
|
1363
|
-
reflectsWithErrorContext,
|
|
1364
|
-
memoryInferenceDurationMs,
|
|
1365
|
-
graphExtractionDurationMs,
|
|
1366
|
-
orphansPurged,
|
|
1367
|
-
proposalsExpired,
|
|
1368
|
-
allWarnings,
|
|
1369
|
-
deadUrls,
|
|
1370
|
-
deadUrlCoverage,
|
|
1371
|
-
finalActions,
|
|
850
|
+
postLoop,
|
|
851
|
+
reflectsWithErrorContext: loopResult.reflectsWithErrorContext,
|
|
852
|
+
finalActions: [...preparation.actions, ...(postLoop.maintenanceActions ?? [])],
|
|
1372
853
|
};
|
|
1373
854
|
}
|
|
1374
|
-
/**
|
|
1375
|
-
* Run-teardown, result half: assemble the AkmImproveResult envelope and emit
|
|
1376
|
-
* the improve_completed event (same adjacency as the old inline code — no
|
|
1377
|
-
* side effects run between assembly and emit).
|
|
1378
|
-
*/
|
|
855
|
+
/** Assemble the result envelope and emit `improve_completed`. */
|
|
1379
856
|
function finalizeImproveResult(args) {
|
|
1380
|
-
const {
|
|
1381
|
-
const {
|
|
1382
|
-
const {
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
//
|
|
857
|
+
const { run, collected, triageDrain, ensureIndexDurationMs, eventsCtx } = args;
|
|
858
|
+
const { preparation, postLoop, reflectsWithErrorContext, finalActions } = args.seq;
|
|
859
|
+
const { options, startMs, resolvedPlan } = run;
|
|
860
|
+
const { memoryCleanupPlan, strategyFilteredRefs } = collected;
|
|
861
|
+
const consolidation = preparation.consolidation;
|
|
862
|
+
const { memoryInference, graphExtraction, allWarnings, deadUrls, deadUrlCoverage, orphansPurged, proposalsExpired } = postLoop;
|
|
863
|
+
const { memoryInferenceDurationMs, graphExtractionDurationMs } = postLoop;
|
|
864
|
+
// The per-ref distill-skipped rows fold into a bounded aggregate before persistence (C1).
|
|
1388
865
|
const { actions: persistedActions, aggregate: distillSkippedAggregate } = foldDistillSkipped(finalActions);
|
|
1389
|
-
//
|
|
1390
|
-
//
|
|
1391
|
-
// events carry no runId column, so bound the read to this run's own wall
|
|
1392
|
-
// clock — the same per-run event-scoping technique `health/windows.ts`
|
|
1393
|
-
// already uses for wall time. `until` is "now" (assembly happens at
|
|
1394
|
-
// teardown, after every LLM call this run will make has already emitted
|
|
1395
|
-
// its event).
|
|
866
|
+
// This run's LLM accounting (#944): llm_usage rows carry no run id, so the
|
|
867
|
+
// read is bounded by the run's own wall clock.
|
|
1396
868
|
const usageEvents = readEvents({ since: new Date(startMs).toISOString(), type: LLM_USAGE_EVENT }, eventsCtx).events;
|
|
1397
869
|
const usageReport = buildImproveUsageReport({
|
|
1398
870
|
resolvedPlan,
|
|
@@ -1402,37 +874,36 @@ function finalizeImproveResult(args) {
|
|
|
1402
874
|
persistedActions,
|
|
1403
875
|
distillSkippedAggregate,
|
|
1404
876
|
});
|
|
1405
|
-
const notices = collectImproveNotices(
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
schemaRepairs: preparation.schemaRepairs,
|
|
877
|
+
const notices = collectImproveNotices(resolvedPlan, [
|
|
878
|
+
...finalActions.map((action) => action.result),
|
|
879
|
+
...preparation.schemaRepairs,
|
|
1409
880
|
consolidation,
|
|
1410
|
-
extract
|
|
881
|
+
...(preparation.extract ?? []).flatMap((extract) => [extract, ...(extract.sessions ?? [])]),
|
|
1411
882
|
memoryInference,
|
|
1412
883
|
graphExtraction,
|
|
1413
884
|
triageDrain,
|
|
1414
|
-
|
|
885
|
+
]);
|
|
886
|
+
const applied = preparation.appliedCleanup;
|
|
887
|
+
const countMode = (mode) => finalActions.filter((a) => a.mode === mode).length;
|
|
1415
888
|
const result = {
|
|
1416
889
|
schemaVersion: 2,
|
|
1417
890
|
ok: true,
|
|
1418
|
-
strategy: selectedStrategy.name,
|
|
1419
|
-
scope,
|
|
891
|
+
strategy: run.selectedStrategy.name,
|
|
892
|
+
scope: run.scope,
|
|
1420
893
|
dryRun: false,
|
|
1421
|
-
...
|
|
1422
|
-
...(guidance ? { guidance } : {}),
|
|
1423
|
-
memorySummary,
|
|
894
|
+
...notices.fields(),
|
|
895
|
+
...(collected.guidance ? { guidance: collected.guidance } : {}),
|
|
896
|
+
memorySummary: collected.memorySummary,
|
|
1424
897
|
...(memoryCleanupPlan
|
|
1425
898
|
? {
|
|
1426
899
|
memoryCleanup: {
|
|
1427
900
|
...shapeMemoryCleanup(memoryCleanupPlan),
|
|
1428
|
-
...(
|
|
901
|
+
...(applied
|
|
1429
902
|
? {
|
|
1430
|
-
archived:
|
|
1431
|
-
...(
|
|
1432
|
-
|
|
1433
|
-
:
|
|
1434
|
-
...(preparation.appliedCleanup.transitionLogEntries !== undefined
|
|
1435
|
-
? { transitionLogEntries: preparation.appliedCleanup.transitionLogEntries }
|
|
903
|
+
archived: applied.archived,
|
|
904
|
+
...(applied.transitionLogPath ? { transitionLogPath: applied.transitionLogPath } : {}),
|
|
905
|
+
...(applied.transitionLogEntries !== undefined
|
|
906
|
+
? { transitionLogEntries: applied.transitionLogEntries }
|
|
1436
907
|
: {}),
|
|
1437
908
|
...(allWarnings.length > 0 ? { warnings: allWarnings } : {}),
|
|
1438
909
|
}
|
|
@@ -1443,11 +914,7 @@ function finalizeImproveResult(args) {
|
|
|
1443
914
|
}
|
|
1444
915
|
: {}),
|
|
1445
916
|
plannedRefs: preparation.loopRefs,
|
|
1446
|
-
...(preparation.planning
|
|
1447
|
-
? {
|
|
1448
|
-
plan: buildResultExecutionPlan(args.run, preparation, rawPlannedRefs, strategyFilteredRefs, indexSnapshot, false),
|
|
1449
|
-
}
|
|
1450
|
-
: {}),
|
|
917
|
+
...(preparation.planning ? { plan: buildResultExecutionPlan(run, preparation, collected, false) } : {}),
|
|
1451
918
|
...(strategyFilteredRefs.length > 0 ? { strategyFilteredRefs } : {}),
|
|
1452
919
|
...(resolvedPlan.engineUnavailable.length > 0 ? { skippedProcesses: resolvedPlan.engineUnavailable } : {}),
|
|
1453
920
|
...(options.engineProbe !== undefined ? { engineProbe: options.engineProbe } : {}),
|
|
@@ -1462,37 +929,21 @@ function finalizeImproveResult(args) {
|
|
|
1462
929
|
...(preparation.coverageGaps.length > 0 ? { coverageGaps: preparation.coverageGaps } : {}),
|
|
1463
930
|
...(preparation.extract && preparation.extract.length > 0 ? { extract: preparation.extract } : {}),
|
|
1464
931
|
...(deadUrls !== undefined && deadUrls.length > 0 ? { deadUrls } : {}),
|
|
1465
|
-
// Present whenever the check ran,
|
|
1466
|
-
// (zero dead links) still needs to tell the health report how much of
|
|
1467
|
-
// the bundle it actually covered (#892).
|
|
932
|
+
// Present whenever the check ran, so health knows the coverage of a clean run (#892).
|
|
1468
933
|
...(deadUrlCoverage !== undefined ? { deadUrlCoverage } : {}),
|
|
1469
934
|
...(reflectsWithErrorContext > 0 ? { reflectsWithErrorContext } : {}),
|
|
1470
935
|
...(memoryInference ? { memoryInference } : {}),
|
|
1471
936
|
...(graphExtraction ? { graphExtraction } : {}),
|
|
1472
|
-
//
|
|
1473
|
-
//
|
|
1474
|
-
// aggregator and the existing `memoryInference.durationMs` /
|
|
1475
|
-
// `graphExtraction.durationMs` health buckets all read
|
|
1476
|
-
// `result.{memoryInferenceDurationMs,graphExtractionDurationMs}`
|
|
1477
|
-
// directly. Mirrors how `consolidation.durationMs` is surfaced inside
|
|
1478
|
-
// the consolidation sub-object (different convention because the
|
|
1479
|
-
// consolidation result type already owns that field). Phases that did
|
|
1480
|
-
// not run (zero duration) are omitted so the aggregator's
|
|
1481
|
-
// "phase actually ran" filter (`> 0`) excludes them from the median/p95
|
|
1482
|
-
// sample. Plumbed in d1273d0's follow-up — see
|
|
1483
|
-
// `/tmp/akm-health-investigations/metrics-taxonomy-review.md` §1k / §3.
|
|
937
|
+
// Top-level phase durations feed health's wall-time buckets; a phase that
|
|
938
|
+
// did not run is omitted, not zero.
|
|
1484
939
|
...(memoryInferenceDurationMs > 0 ? { memoryInferenceDurationMs } : {}),
|
|
1485
940
|
...(graphExtractionDurationMs > 0 ? { graphExtractionDurationMs } : {}),
|
|
1486
|
-
// R6: the start-of-run implicit reindex's wall-clock cost, when one ran
|
|
1487
|
-
// (absent — not zero — when the index was already fresh and no inline
|
|
1488
|
-
// rebuild was needed).
|
|
1489
941
|
...(ensureIndexDurationMs !== undefined ? { ensureIndexDurationMs } : {}),
|
|
1490
|
-
...(cycleMetrics ? { cycleMetrics } : {}),
|
|
1491
942
|
...(orphansPurged !== undefined ? { orphansPurged } : {}),
|
|
1492
943
|
...(proposalsExpired !== undefined && proposalsExpired > 0 ? { proposalsExpired } : {}),
|
|
1493
|
-
reflectCooldownActions:
|
|
1494
|
-
reflectSkippedActions:
|
|
1495
|
-
reflectGuardRejectedActions:
|
|
944
|
+
reflectCooldownActions: countMode("reflect-cooldown"),
|
|
945
|
+
reflectSkippedActions: countMode("reflect-skipped"),
|
|
946
|
+
reflectGuardRejectedActions: countMode("reflect-guard-rejected"),
|
|
1496
947
|
...(triageDrain
|
|
1497
948
|
? {
|
|
1498
949
|
triage: {
|
|
@@ -1507,116 +958,60 @@ function finalizeImproveResult(args) {
|
|
|
1507
958
|
...(preparation.proactiveMaintenance ? { proactiveMaintenance: preparation.proactiveMaintenance } : {}),
|
|
1508
959
|
...(options.runId !== undefined ? { runId: options.runId } : {}),
|
|
1509
960
|
};
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
}, eventsCtx);
|
|
961
|
+
emitImproveCompletedEvent(result, {
|
|
962
|
+
memoryInferenceDurationMs,
|
|
963
|
+
graphExtractionDurationMs,
|
|
964
|
+
totalDurationMs: Date.now() - startMs,
|
|
965
|
+
warningCount: allWarnings.length,
|
|
966
|
+
orphansPurged: orphansPurged ?? 0,
|
|
967
|
+
}, eventsCtx);
|
|
1518
968
|
return result;
|
|
1519
969
|
}
|
|
1520
|
-
|
|
1521
|
-
|
|
970
|
+
/** Every lowering notice the plan and this run's stage results carry, deduplicated. */
|
|
971
|
+
function collectImproveNotices(resolvedPlan, carriers) {
|
|
972
|
+
const notices = noticeSet();
|
|
1522
973
|
const collect = (carrier) => {
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
if (!Array.isArray(notices))
|
|
1527
|
-
return;
|
|
1528
|
-
for (const notice of notices)
|
|
1529
|
-
byKey.set(JSON.stringify(notice), notice);
|
|
974
|
+
const list = carrier?.notices;
|
|
975
|
+
if (typeof carrier === "object" && Array.isArray(list))
|
|
976
|
+
notices.add(list);
|
|
1530
977
|
};
|
|
1531
|
-
for (const process of Object.values(
|
|
978
|
+
for (const process of Object.values(resolvedPlan.processes))
|
|
1532
979
|
collect(process);
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
collect(action.result);
|
|
1538
|
-
for (const repair of args.schemaRepairs ?? [])
|
|
1539
|
-
collect(repair);
|
|
1540
|
-
collect(args.consolidation);
|
|
1541
|
-
for (const extract of args.extract ?? []) {
|
|
1542
|
-
collect(extract);
|
|
1543
|
-
for (const session of extract.sessions ?? [])
|
|
1544
|
-
collect(session);
|
|
1545
|
-
}
|
|
1546
|
-
collect(args.memoryInference);
|
|
1547
|
-
collect(args.graphExtraction);
|
|
1548
|
-
collect(args.triageDrain);
|
|
1549
|
-
return Object.freeze([...byKey.values()]);
|
|
980
|
+
notices.add(resolvedPlan.triageJudgmentNotices ?? []);
|
|
981
|
+
for (const carrier of carriers)
|
|
982
|
+
collect(carrier);
|
|
983
|
+
return notices;
|
|
1550
984
|
}
|
|
985
|
+
/** `improve_completed` per-mode counters; keyed by every mode so a new one cannot be dropped. */
|
|
986
|
+
const ACTION_COUNTER = {
|
|
987
|
+
reflect: "reflectActions",
|
|
988
|
+
distill: "distillActions",
|
|
989
|
+
"distill-skipped": "distillSkippedActions",
|
|
990
|
+
"memory-prune": "memoryPruneActions",
|
|
991
|
+
"memory-inference": "memoryInferenceActions",
|
|
992
|
+
"graph-extraction": "graphExtractionActions",
|
|
993
|
+
error: "errorActions",
|
|
994
|
+
"reflect-failed": "reflectFailedActions",
|
|
995
|
+
"reflect-cooldown": "reflectCooldownActions",
|
|
996
|
+
"reflect-skipped": "reflectSkippedActions",
|
|
997
|
+
"reflect-guard-rejected": "reflectGuardRejectedActions",
|
|
998
|
+
};
|
|
1551
999
|
function emitImproveCompletedEvent(result, durations, eventsCtx) {
|
|
1552
|
-
const
|
|
1553
|
-
|
|
1554
|
-
reflectFailed: 0,
|
|
1555
|
-
reflectCooldown: 0,
|
|
1556
|
-
reflectSkipped: 0,
|
|
1557
|
-
reflectGuardRejected: 0,
|
|
1558
|
-
distill: 0,
|
|
1559
|
-
distillSkipped: 0,
|
|
1560
|
-
memoryPrune: 0,
|
|
1561
|
-
memoryInference: 0,
|
|
1562
|
-
graphExtraction: 0,
|
|
1563
|
-
error: 0,
|
|
1564
|
-
};
|
|
1565
|
-
// Coarse audit buckets, derived from the SAME classifyImproveAction the
|
|
1566
|
-
// persisted metrics_json uses (state-db.ts#computeImproveRunMetrics) so the
|
|
1567
|
-
// emitted event and the stored row can never disagree.
|
|
1000
|
+
const counts = Object.fromEntries(Object.values(ACTION_COUNTER).map((key) => [key, 0]));
|
|
1001
|
+
// The coarse buckets come from the same classifier the persisted metrics use.
|
|
1568
1002
|
const classCounts = { accepted: 0, rejected: 0, skipped: 0, error: 0, noop: 0 };
|
|
1569
1003
|
for (const action of result.actions ?? []) {
|
|
1004
|
+
const key = ACTION_COUNTER[action.mode];
|
|
1005
|
+
if (key)
|
|
1006
|
+
counts[key] = (counts[key] ?? 0) + 1;
|
|
1570
1007
|
classCounts[classifyImproveAction(action.mode)] += 1;
|
|
1571
|
-
// Per-variant counters for the event metadata. The default arm makes any
|
|
1572
|
-
// new ImproveActionMode variant a compile error so a future variant cannot
|
|
1573
|
-
// be silently dropped from the improve_completed event (the `reflect-guard-
|
|
1574
|
-
// rejected` case below was previously missing here entirely).
|
|
1575
|
-
switch (action.mode) {
|
|
1576
|
-
case "reflect":
|
|
1577
|
-
actionCounts.reflect += 1;
|
|
1578
|
-
break;
|
|
1579
|
-
case "reflect-failed":
|
|
1580
|
-
actionCounts.reflectFailed += 1;
|
|
1581
|
-
break;
|
|
1582
|
-
case "reflect-cooldown":
|
|
1583
|
-
actionCounts.reflectCooldown += 1;
|
|
1584
|
-
break;
|
|
1585
|
-
case "reflect-skipped":
|
|
1586
|
-
actionCounts.reflectSkipped += 1;
|
|
1587
|
-
break;
|
|
1588
|
-
case "reflect-guard-rejected":
|
|
1589
|
-
actionCounts.reflectGuardRejected += 1;
|
|
1590
|
-
break;
|
|
1591
|
-
case "distill":
|
|
1592
|
-
actionCounts.distill += 1;
|
|
1593
|
-
break;
|
|
1594
|
-
case "distill-skipped":
|
|
1595
|
-
actionCounts.distillSkipped += 1;
|
|
1596
|
-
break;
|
|
1597
|
-
case "memory-prune":
|
|
1598
|
-
actionCounts.memoryPrune += 1;
|
|
1599
|
-
break;
|
|
1600
|
-
case "memory-inference":
|
|
1601
|
-
actionCounts.memoryInference += 1;
|
|
1602
|
-
break;
|
|
1603
|
-
case "graph-extraction":
|
|
1604
|
-
actionCounts.graphExtraction += 1;
|
|
1605
|
-
break;
|
|
1606
|
-
case "error":
|
|
1607
|
-
actionCounts.error += 1;
|
|
1608
|
-
break;
|
|
1609
|
-
default:
|
|
1610
|
-
assertNever(action.mode);
|
|
1611
|
-
}
|
|
1612
1008
|
}
|
|
1613
|
-
//
|
|
1614
|
-
// bounded `distillSkipped` aggregate at assembly). Add the aggregate total to
|
|
1615
|
-
// the per-variant counter AND the coarse `skipped` bucket so the emitted event
|
|
1616
|
-
// still reports the true skipped volume.
|
|
1009
|
+
// distill-skipped rows were folded into the aggregate; count them back in.
|
|
1617
1010
|
const distillSkippedTotal = result.distillSkipped?.total ?? 0;
|
|
1618
|
-
|
|
1011
|
+
counts.distillSkippedActions = (counts.distillSkippedActions ?? 0) + distillSkippedTotal;
|
|
1619
1012
|
classCounts.skipped += distillSkippedTotal;
|
|
1013
|
+
const cleanup = result.memoryCleanup;
|
|
1014
|
+
const quality = result.graphExtraction?.quality;
|
|
1620
1015
|
appendEvent({
|
|
1621
1016
|
eventType: "improve_completed",
|
|
1622
1017
|
ref: result.scope.mode === "ref"
|
|
@@ -1625,19 +1020,7 @@ function emitImproveCompletedEvent(result, durations, eventsCtx) {
|
|
|
1625
1020
|
metadata: {
|
|
1626
1021
|
strategy: result.strategy,
|
|
1627
1022
|
plannedRefs: result.plannedRefs.length,
|
|
1628
|
-
|
|
1629
|
-
distillActions: actionCounts.distill,
|
|
1630
|
-
distillSkippedActions: actionCounts.distillSkipped,
|
|
1631
|
-
memoryPruneActions: actionCounts.memoryPrune,
|
|
1632
|
-
memoryInferenceActions: actionCounts.memoryInference,
|
|
1633
|
-
graphExtractionActions: actionCounts.graphExtraction,
|
|
1634
|
-
errorActions: actionCounts.error,
|
|
1635
|
-
reflectFailedActions: actionCounts.reflectFailed,
|
|
1636
|
-
reflectCooldownActions: actionCounts.reflectCooldown,
|
|
1637
|
-
reflectSkippedActions: actionCounts.reflectSkipped,
|
|
1638
|
-
// Previously dropped from the event entirely; now emitted so the guard
|
|
1639
|
-
// rejections are visible in improve_completed telemetry.
|
|
1640
|
-
reflectGuardRejectedActions: actionCounts.reflectGuardRejected,
|
|
1023
|
+
...counts,
|
|
1641
1024
|
acceptedActions: classCounts.accepted,
|
|
1642
1025
|
rejectedActions: classCounts.rejected,
|
|
1643
1026
|
skippedActions: classCounts.skipped,
|
|
@@ -1650,32 +1033,29 @@ function emitImproveCompletedEvent(result, durations, eventsCtx) {
|
|
|
1650
1033
|
deadUrlsSkipped: result.deadUrlCoverage?.skipped ?? 0,
|
|
1651
1034
|
memoryEligible: result.memorySummary.eligible,
|
|
1652
1035
|
memoryDerived: result.memorySummary.derived,
|
|
1653
|
-
memoryCleanupPruneCandidates:
|
|
1654
|
-
memoryCleanupContradictionCandidates:
|
|
1655
|
-
memoryCleanupBeliefStateTransitions:
|
|
1656
|
-
memoryCleanupConsolidationCandidates:
|
|
1657
|
-
memoryCleanupArchived:
|
|
1658
|
-
memoryCleanupWarnings:
|
|
1036
|
+
memoryCleanupPruneCandidates: cleanup?.pruneCandidates.length ?? 0,
|
|
1037
|
+
memoryCleanupContradictionCandidates: cleanup?.contradictionCandidates.length ?? 0,
|
|
1038
|
+
memoryCleanupBeliefStateTransitions: cleanup?.beliefStateTransitions.length ?? 0,
|
|
1039
|
+
memoryCleanupConsolidationCandidates: cleanup?.consolidationCandidates.length ?? 0,
|
|
1040
|
+
memoryCleanupArchived: cleanup?.archived?.length ?? 0,
|
|
1041
|
+
memoryCleanupWarnings: cleanup?.warnings?.length ?? 0,
|
|
1659
1042
|
consolidationProcessed: result.consolidation?.processed ?? 0,
|
|
1660
1043
|
consolidationDurationMs: result.consolidation?.durationMs ?? 0,
|
|
1661
1044
|
memoryInferenceWrites: result.memoryInference?.writtenFacts ?? 0,
|
|
1662
1045
|
memoryInferenceDurationMs: durations.memoryInferenceDurationMs,
|
|
1663
|
-
graphExtractionExtractedFiles:
|
|
1046
|
+
graphExtractionExtractedFiles: quality?.extractedFiles ?? 0,
|
|
1664
1047
|
graphExtractionDurationMs: durations.graphExtractionDurationMs,
|
|
1665
|
-
// Layer-2 proactive-maintenance coverage (0 when the process is disabled
|
|
1666
|
-
// or the run was ref-scoped) so a scheduled sweep's reach is trackable.
|
|
1667
1048
|
proactiveSelected: result.proactiveMaintenance?.selected ?? 0,
|
|
1668
1049
|
proactiveDueTotal: result.proactiveMaintenance?.dueTotal ?? 0,
|
|
1669
1050
|
proactiveNeverReflected: result.proactiveMaintenance?.neverReflected ?? 0,
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
...(
|
|
1674
|
-
...(result.graphExtraction?.quality
|
|
1051
|
+
durationMs: durations.totalDurationMs,
|
|
1052
|
+
warningCount: durations.warningCount,
|
|
1053
|
+
orphansPurged: durations.orphansPurged,
|
|
1054
|
+
...(quality
|
|
1675
1055
|
? {
|
|
1676
|
-
graphCoverage:
|
|
1677
|
-
graphDensity:
|
|
1678
|
-
graphEntities:
|
|
1056
|
+
graphCoverage: quality.extractionCoverage,
|
|
1057
|
+
graphDensity: quality.density,
|
|
1058
|
+
graphEntities: quality.entityCount,
|
|
1679
1059
|
}
|
|
1680
1060
|
: {}),
|
|
1681
1061
|
},
|