akm-cli 0.9.0-rc.8 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1063 -44
- package/README.md +51 -25
- package/SECURITY.md +14 -1
- package/STABILITY.md +497 -0
- package/dist/akm +148 -35
- package/dist/{akm-migrate-storage → akm-migrate} +6 -9
- package/dist/assets/hints/cli-hints-full.md +223 -95
- package/dist/assets/hints/cli-hints-short.md +85 -22
- package/dist/assets/improve-strategies/default.json +1 -1
- package/dist/assets/improve-strategies/reflect-distill.json +1 -1
- package/dist/assets/prompts/memory-infer-user.md +2 -3
- package/dist/assets/stash-skeleton/README.md +6 -5
- package/dist/assets/stash-skeleton/facts/conventions/assets/agent.md +2 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/command.md +2 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/fact.md +2 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/knowledge.md +2 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/lesson.md +2 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/memory.md +2 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/script.md +2 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/skill.md +2 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/workflow.md +2 -0
- package/dist/assets/stash-skeleton/facts/conventions/backlinks.md +2 -0
- package/dist/assets/stash-skeleton/facts/conventions/domains.md +2 -0
- package/dist/assets/stash-skeleton/facts/conventions/organization.md +20 -9
- package/dist/assets/tasks/core/extract.yml +1 -1
- package/dist/assets/tasks/core/version-check.yml +1 -1
- package/dist/assets/tasks/improve/akm-graph-refresh-weekly.yml +5 -0
- package/dist/assets/tasks/improve/akm-improve-catchup.yml +8 -0
- package/dist/assets/tasks/improve/akm-improve-consolidate.yml +5 -0
- package/dist/assets/tasks/improve/akm-improve-frequent.yml +5 -0
- package/dist/assets/tasks/improve/akm-improve-nightly.yml +5 -0
- package/dist/assets/templates/html/health.html +1 -3
- package/dist/assets/workflows/workflow-template.md +32 -15
- package/dist/cli/invocation.js +40 -15
- package/dist/cli/parse-args.js +0 -22
- package/dist/cli/retired-commands.js +121 -0
- package/dist/cli/shared.js +154 -22
- package/dist/cli/unknown-flags.js +236 -0
- package/dist/cli-node.mjs +2 -1
- package/dist/cli.js +696 -258
- package/dist/commands/agent/agent-dispatch.js +14 -3
- package/dist/commands/agent/contribute-cli.js +73 -88
- package/dist/commands/completions.js +79 -22
- package/dist/commands/config-cli.js +17 -150
- package/dist/commands/env/env-cli.js +59 -143
- package/dist/commands/env/env.js +12 -163
- package/dist/commands/env/marker-path.js +6 -0
- package/dist/commands/env/secret-cli.js +36 -66
- package/dist/commands/env/secret.js +24 -57
- package/dist/commands/feedback-cli.js +141 -87
- package/dist/commands/health/accept-rate.js +58 -0
- package/dist/commands/health/advisories.js +3 -4
- package/dist/commands/health/checks.js +85 -23
- package/dist/commands/health/html-report.js +7 -10
- package/dist/commands/health/improve-metrics.js +25 -83
- package/dist/commands/health/md-report.js +5 -9
- package/dist/commands/health/metrics.js +62 -20
- package/dist/commands/health/renderers.js +47 -0
- package/dist/commands/health/report-view-model.js +4 -5
- package/dist/commands/health/stash-exposure.js +1 -1
- package/dist/commands/health/surfaces.js +3 -48
- package/dist/commands/health/task-runs.js +3 -67
- package/dist/commands/health/types-improve.js +7 -0
- package/dist/commands/health.js +99 -28
- package/dist/commands/improve/anti-collapse.js +2 -2
- package/dist/commands/improve/autonomy-gate.js +68 -0
- package/dist/commands/improve/collapse-detector.js +41 -40
- package/dist/commands/improve/consolidate/eligibility.js +1 -23
- package/dist/commands/improve/consolidate/merge.js +4 -0
- package/dist/commands/improve/consolidate.js +140 -1000
- package/dist/commands/improve/distill/promote-memory.js +12 -12
- package/dist/commands/improve/distill/quality-gate.js +6 -6
- package/dist/commands/improve/distill.js +58 -69
- package/dist/commands/improve/eligibility.js +105 -57
- package/dist/commands/improve/extract-cli.js +14 -133
- package/dist/commands/improve/improve-cli.js +98 -114
- package/dist/commands/improve/improve-result-file.js +1 -28
- package/dist/commands/improve/improve-strategies.js +8 -5
- package/dist/commands/improve/improve.js +128 -91
- package/dist/commands/improve/loop-stages.js +182 -20
- package/dist/commands/improve/memory/derived-ref.js +45 -43
- package/dist/commands/improve/memory/memory-belief.js +1 -1
- package/dist/commands/improve/memory/memory-contradiction-detect.js +4 -12
- package/dist/commands/improve/memory/memory-improve.js +6 -5
- package/dist/commands/improve/outcome-loop.js +22 -65
- package/dist/commands/improve/preparation.js +114 -123
- package/dist/commands/improve/proactive-maintenance.js +2 -5
- package/dist/commands/improve/reflect.js +56 -160
- package/dist/commands/improve/salience.js +11 -122
- package/dist/commands/improve/source-identity.js +10 -38
- package/dist/commands/lint/base-linter.js +20 -124
- package/dist/commands/lint/env-key-rules.js +31 -47
- package/dist/commands/lint/index.js +249 -43
- package/dist/commands/{events.js → log.js} +33 -38
- package/dist/commands/migrate-cli.js +92 -12
- package/dist/commands/migration-tool.js +46 -0
- package/dist/commands/observability-cli.js +70 -209
- package/dist/commands/proposal/drain.js +101 -29
- package/dist/commands/proposal/proposal-cli.js +76 -48
- package/dist/commands/proposal/proposal.js +54 -18
- package/dist/commands/proposal/propose-cli.js +88 -0
- package/dist/commands/proposal/propose.js +23 -15
- package/dist/commands/proposal/repository.js +701 -278
- package/dist/commands/proposal/validators/proposal-quality-validators.js +2 -8
- package/dist/commands/proposal/validators/proposal-validators.js +55 -7
- package/dist/commands/proposal/validators/proposals.js +4 -7
- package/dist/commands/read/curate.js +34 -53
- package/dist/commands/read/knowledge.js +150 -95
- package/dist/commands/read/registry-search.js +2 -2
- package/dist/commands/read/remember-cli.js +42 -15
- package/dist/commands/read/search-cli.js +180 -78
- package/dist/commands/read/search.js +58 -43
- package/dist/commands/read/show.js +197 -141
- package/dist/commands/registry-cli.js +12 -51
- package/dist/commands/remember.js +14 -57
- package/dist/commands/sources/add-cli.js +100 -31
- package/dist/commands/sources/bundle-cli.js +166 -0
- package/dist/commands/sources/bundle-config-ops.js +7 -2
- package/dist/commands/sources/info.js +18 -5
- package/dist/commands/sources/init.js +12 -12
- package/dist/commands/sources/installed-stashes.js +382 -98
- package/dist/commands/sources/schema-repair.js +3 -2
- package/dist/commands/sources/self-update.js +131 -38
- package/dist/commands/sources/source-add.js +72 -17
- package/dist/commands/sources/source-clone.js +129 -45
- package/dist/commands/sources/source-manage.js +43 -23
- package/dist/commands/sources/sources-cli.js +57 -208
- package/dist/commands/sources/stash-cli.js +46 -53
- package/dist/commands/tasks/tasks-cli.js +91 -97
- package/dist/commands/tasks/tasks.js +276 -421
- package/dist/commands/workflow-cli.js +175 -450
- package/dist/core/adapter/adapters/akm-adapter.js +47 -28
- package/dist/core/adapter/adapters/akm-lint.js +42 -27
- package/dist/core/adapter/adapters/akm-metadata.js +15 -44
- package/dist/core/adapter/adapters/akm-task-adapter.js +15 -13
- package/dist/core/adapter/adapters/akm-workflow-adapter.js +55 -71
- package/dist/core/adapter/adapters/dotenv-adapter.js +1 -1
- package/dist/core/adapter/adapters/generic-files-adapter.js +2 -0
- package/dist/core/adapter/adapters/index.js +6 -6
- package/dist/core/adapter/adapters/llm-wiki-adapter.js +14 -8
- package/dist/core/adapter/adapters/okf-adapter.js +187 -19
- package/dist/core/adapter/adapters/shared.js +3 -19
- package/dist/core/adapter/adapters/tool-dir-shared.js +8 -3
- package/dist/core/adapter/adapters/website-snapshot-adapter.js +1 -0
- package/dist/core/adapter/detect-adapter.js +17 -0
- package/dist/core/adapter/recognize-match.js +6 -4
- package/dist/core/adapter/validate-context.js +214 -0
- package/dist/core/asset/akm-markdown.js +63 -0
- package/dist/core/asset/asset-placement.js +20 -6
- package/dist/core/asset/asset-ref.js +11 -9
- package/dist/core/asset/frontmatter-lint.js +30 -0
- package/dist/core/asset/frontmatter.js +37 -9
- package/dist/core/asset/markdown.js +40 -51
- package/dist/core/asset/resolve-ref.js +89 -18
- package/dist/core/asset/stash-meta.js +1 -1
- package/dist/core/bundle-id.js +51 -0
- package/dist/core/common.js +152 -38
- package/dist/core/config/config-io.js +12 -1
- package/dist/core/config/config-schema.js +35 -8
- package/dist/core/config/config-sources.js +55 -11
- package/dist/core/config/config-walker.js +25 -9
- package/dist/core/config/config.js +9 -48
- package/dist/core/config/experimental.js +21 -0
- package/dist/core/config/schema/embedding.js +5 -1
- package/dist/core/config/schema/experimental.js +30 -0
- package/dist/core/config/schema/improve-processes.js +0 -6
- package/dist/core/config/schema/improve.js +21 -3
- package/dist/core/config/schema/index-config.js +8 -15
- package/dist/core/config/schema/output.js +4 -1
- package/dist/core/config/schema/setup.js +9 -18
- package/dist/core/config/schema/sources-bundles.js +49 -33
- package/dist/core/config/schema/workflow.js +3 -3
- package/dist/core/env-secret-ref.js +76 -46
- package/dist/core/errors.js +18 -12
- package/dist/core/events.js +46 -128
- package/dist/core/file-change.js +6 -5
- package/dist/core/fs-txn.js +83 -7
- package/dist/core/git-message.js +2 -2
- package/dist/core/improve-result.js +1 -100
- package/dist/core/lesson-lint.js +1 -17
- package/dist/core/logs-db.js +2 -1
- package/dist/core/migration-operation.js +16 -0
- package/dist/core/mutation-target.js +78 -0
- package/dist/core/parse.js +4 -1
- package/dist/core/paths.js +17 -20
- package/dist/core/recognition-util.js +12 -14
- package/dist/core/redaction.js +34 -0
- package/dist/core/standards/resolve-standards-context.js +2 -14
- package/dist/core/standards/resolve-stash-standards.js +2 -2
- package/dist/core/standards/resolve-type-conventions.js +2 -2
- package/dist/core/state/migrations.js +41 -18
- package/dist/core/state-db.js +5 -14
- package/dist/core/structured.js +1 -1
- package/dist/core/subprocess.js +6 -4
- package/dist/core/text-truncation.js +9 -5
- package/dist/core/type-presentation.js +3 -3
- package/dist/core/warn.js +0 -3
- package/dist/core/write-source.js +771 -95
- package/dist/indexer/bundle-identity-guard.js +3 -2
- package/dist/indexer/db/graph-db.js +0 -24
- package/dist/indexer/ensure-index.js +1 -0
- package/dist/indexer/graph/graph-boost.js +9 -34
- package/dist/indexer/graph/graph-extraction.js +8 -5
- package/dist/indexer/index-writer-lock.js +53 -17
- package/dist/indexer/index-written-assets.js +16 -22
- package/dist/indexer/indexer.js +497 -239
- package/dist/indexer/installations.js +14 -96
- package/dist/indexer/passes/dir-staleness.js +16 -9
- package/dist/indexer/passes/memory-inference.js +11 -9
- package/dist/indexer/passes/metadata.js +113 -47
- package/dist/indexer/scan/doc-to-entry.js +38 -1
- package/dist/indexer/scan/drain-dir.js +13 -23
- package/dist/indexer/search/db-search.js +99 -54
- package/dist/indexer/search/fts-query.js +47 -24
- package/dist/indexer/search/ranking-contributors.js +42 -20
- package/dist/indexer/search/ranking.js +18 -99
- package/dist/indexer/search/search-fields.js +7 -2
- package/dist/indexer/search/search-source.js +82 -93
- package/dist/indexer/usage/usage-events.js +0 -89
- package/dist/indexer/walk/file-context.js +2 -1
- package/dist/indexer/walk/matchers.js +30 -43
- package/dist/indexer/walk/path-resolver.js +7 -2
- package/dist/indexer/walk/walker.js +38 -12
- package/dist/integrations/agent/builders.js +0 -6
- package/dist/integrations/agent/config.js +2 -2
- package/dist/integrations/agent/detect.js +49 -19
- package/dist/integrations/agent/engine-fallback.js +76 -0
- package/dist/integrations/agent/profiles.js +14 -0
- package/dist/integrations/agent/prompts.js +12 -8
- package/dist/integrations/agent/runner-dispatch.js +4 -2
- package/dist/integrations/agent/runner.js +0 -1
- package/dist/integrations/agent/spawn.js +5 -6
- package/dist/integrations/github.js +1 -1
- package/dist/integrations/harnesses/aider/agent-builder.js +6 -4
- package/dist/integrations/harnesses/amazonq/agent-builder.js +7 -4
- package/dist/integrations/harnesses/claude/session-log.js +0 -10
- package/dist/integrations/harnesses/codex/agent-builder.js +5 -2
- package/dist/integrations/harnesses/copilot/agent-builder.js +5 -3
- package/dist/integrations/harnesses/gemini/agent-builder.js +5 -3
- package/dist/integrations/harnesses/index.js +3 -7
- package/dist/integrations/harnesses/opencode/agent-builder.js +21 -2
- package/dist/integrations/harnesses/opencode/session-log.js +0 -15
- package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +13 -4
- package/dist/integrations/harnesses/openhands/agent-builder.js +9 -6
- package/dist/integrations/harnesses/pi/agent-builder.js +6 -4
- package/dist/integrations/lockfile.js +101 -6
- package/dist/integrations/session-logs/index.js +3 -28
- package/dist/llm/client.js +136 -100
- package/dist/llm/embedders/remote.js +13 -5
- package/dist/llm/feature-gate.js +4 -12
- package/dist/llm/graph-extract.js +5 -11
- package/dist/llm/memory-infer.js +144 -1
- package/dist/llm/metadata-enhance.js +5 -7
- package/dist/llm/structured-call.js +1 -1
- package/dist/llm/usage-persist.js +26 -5
- package/dist/llm/usage-telemetry.js +25 -2
- package/dist/output/cli-hints.js +1 -2
- package/dist/output/context.js +22 -7
- package/dist/output/format-exempt.js +80 -0
- package/dist/output/generic-render.js +259 -0
- package/dist/output/render-registry.js +57 -0
- package/dist/output/renderers.js +14 -36
- package/dist/output/shapes/curate.js +10 -1
- package/dist/output/shapes/events.js +12 -7
- package/dist/output/shapes/helpers.js +56 -83
- package/dist/output/shapes/migrate.js +8 -0
- package/dist/output/shapes/passthrough.js +7 -41
- package/dist/output/shapes/proposal/producer.js +15 -7
- package/dist/output/shapes.js +2 -9
- package/dist/output/text/{init.js → bundle-create.js} +3 -1
- package/dist/output/text/bundle-show.js +7 -0
- package/dist/output/text/command-format.js +164 -96
- package/dist/output/text/env.js +1 -3
- package/dist/output/text/events.js +8 -7
- package/dist/output/text/health-format.js +103 -0
- package/dist/output/text/health.js +7 -0
- package/dist/output/text/helpers.js +10 -8
- package/dist/output/text/lint-format.js +43 -0
- package/dist/output/text/{save.js → lint.js} +2 -2
- package/dist/output/text/migrate.js +88 -0
- package/dist/output/text/proposal/producer.js +4 -2
- package/dist/output/text/proposal-format.js +44 -72
- package/dist/output/text/registry-commands.js +1 -2
- package/dist/output/text/show-directives.js +15 -7
- package/dist/output/text/status-list.js +32 -0
- package/dist/output/text/sync.js +5 -0
- package/dist/output/text/workflow-format.js +24 -203
- package/dist/output/text/workflow.js +1 -7
- package/dist/output/text.js +16 -17
- package/dist/registry/factory.js +4 -6
- package/dist/registry/origin-resolve.js +16 -27
- package/dist/registry/providers/skills-sh.js +3 -3
- package/dist/registry/providers/static-index.js +13 -23
- package/dist/registry/resolve.js +42 -7
- package/dist/registry/semver.js +34 -84
- package/dist/runtime.js +2 -23
- package/dist/scripts/akm-migrate-node.js +60290 -0
- package/dist/scripts/akm-migrate.js +59628 -0
- package/dist/setup/detect.js +42 -15
- package/dist/setup/registry-stash-loader.js +2 -2
- package/dist/setup/setup.js +236 -136
- package/dist/setup/steps/connection.js +7 -9
- package/dist/setup/steps/platforms.js +9 -9
- package/dist/setup/steps/semantic.js +15 -3
- package/dist/setup/steps/sources.js +12 -13
- package/dist/setup/steps/stashdir.js +2 -3
- package/dist/setup/steps/tasks.js +237 -120
- package/dist/sources/freshness.js +1 -1
- package/dist/sources/provider-factory.js +11 -17
- package/dist/sources/providers/filesystem.js +2 -3
- package/dist/sources/providers/git-install.js +278 -34
- package/dist/sources/providers/git-provider.js +25 -23
- package/dist/sources/providers/git-stash.js +395 -106
- package/dist/sources/providers/git.js +2 -2
- package/dist/sources/providers/npm.js +16 -19
- package/dist/sources/providers/provider-utils.js +7 -4
- package/dist/sources/providers/sync-from-ref.js +3 -9
- package/dist/sources/providers/website.js +6 -1
- package/dist/sources/resolve.js +6 -5
- package/dist/sources/snapshot-fetchers/bluesky.js +146 -0
- package/dist/sources/snapshot-fetchers/content-extract.js +566 -0
- package/dist/sources/snapshot-fetchers/fetcher-util.js +41 -0
- package/dist/sources/snapshot-fetchers/github.js +100 -0
- package/dist/sources/snapshot-fetchers/host-guard.js +291 -0
- package/dist/sources/snapshot-fetchers/registry.js +17 -1
- package/dist/sources/snapshot-fetchers/robots.js +348 -0
- package/dist/sources/snapshot-fetchers/rss.js +282 -0
- package/dist/sources/snapshot-fetchers/secret-seam.js +42 -0
- package/dist/sources/snapshot-fetchers/website-ingest.js +566 -268
- package/dist/sources/snapshot-fetchers/x.js +910 -0
- package/dist/storage/database.js +7 -0
- package/dist/storage/engines/sqlite-migrations.js +23 -111
- package/dist/storage/managed-db.js +2 -2
- package/dist/storage/repositories/canaries-repository.js +1 -1
- package/dist/storage/repositories/events-repository.js +27 -11
- package/dist/storage/repositories/improve-runs-repository.js +6 -12
- package/dist/storage/repositories/index-connection.js +17 -6
- package/dist/storage/repositories/index-entries-repository.js +151 -240
- package/dist/storage/repositories/index-entry-mapper.js +15 -11
- package/dist/storage/repositories/index-fts-repository.js +5 -2
- package/dist/storage/repositories/index-llm-cache-repository.js +0 -1
- package/dist/storage/repositories/index-meta-repository.js +2 -3
- package/dist/storage/repositories/index-schema.js +10 -25
- package/dist/storage/repositories/index-utility-repository.js +15 -28
- package/dist/storage/repositories/index-vec-repository.js +6 -1
- package/dist/storage/repositories/outcome-repository.js +119 -0
- package/dist/storage/repositories/proposals-repository.js +296 -59
- package/dist/storage/repositories/registry-cache.js +19 -0
- package/dist/storage/repositories/salience-repository.js +172 -0
- package/dist/storage/repositories/task-history-repository.js +15 -13
- package/dist/storage/repositories/workflow-runs-repository.js +52 -40
- package/dist/tasks/backends/cron.js +105 -15
- package/dist/tasks/backends/index.js +1 -1
- package/dist/tasks/backends/launchd.js +85 -38
- package/dist/tasks/backends/schtasks.js +135 -15
- package/dist/tasks/embedded.js +56 -40
- package/dist/tasks/parser.js +7 -157
- package/dist/tasks/resolve-akm-bin.js +137 -59
- package/dist/tasks/runner.js +79 -42
- package/dist/tasks/scheduler-invocation.js +220 -10
- package/dist/tasks/schema.js +24 -1
- package/dist/tasks/task-id.js +1 -3
- package/dist/tasks/validator.js +20 -6
- package/dist/workflows/authoring/authoring.js +94 -143
- package/dist/workflows/authoring/scope-key.js +1 -1
- package/dist/workflows/exec/frozen-judge.js +28 -2
- package/dist/workflows/exec/native-executor.js +77 -57
- package/dist/workflows/exec/param-secrets.js +9 -9
- package/dist/workflows/exec/run-workflow.js +133 -79
- package/dist/workflows/exec/step-work.js +219 -346
- package/dist/{migrate-storage-node.mjs → workflows/exec/unit-dispatch.js} +1 -5
- package/dist/workflows/ir/compile.js +141 -270
- package/dist/workflows/ir/freeze.js +40 -30
- package/dist/workflows/ir/params.js +135 -11
- package/dist/workflows/ir/plan-hash.js +1 -1
- package/dist/workflows/ir/schema.js +25 -26
- package/dist/workflows/parser.js +872 -307
- package/dist/workflows/program/expressions.js +20 -208
- package/dist/workflows/program/schema.js +7 -10
- package/dist/workflows/renderer.js +95 -68
- package/dist/workflows/resource-limits.js +2 -0
- package/dist/workflows/runtime/checkin.js +3 -3
- package/dist/workflows/runtime/plan-classifier.js +16 -75
- package/dist/workflows/runtime/runs.js +186 -127
- package/dist/workflows/runtime/unit-checkin.js +1 -1
- package/dist/workflows/runtime/unit-phases.js +2 -2
- package/dist/workflows/runtime/workflow-asset-loader.js +232 -83
- package/dist/workflows/schema.js +1 -11
- package/dist/workflows/validate-summary.js +30 -36
- package/dist/workflows/validator.js +21 -62
- package/docs/README.md +68 -0
- package/docs/migration/README.md +8 -0
- package/docs/migration/release-notes/0.7.0.md +11 -11
- package/docs/migration/release-notes/0.9.0.md +208 -27
- package/docs/migration/v0.7-to-v0.8.md +46 -47
- package/docs/migration/v0.8-to-v0.9.md +564 -208
- package/docs/migration/v0.9.0-troubleshooting.md +561 -0
- package/docs/reference/README.md +12 -0
- package/docs/reference/cli.md +2253 -0
- package/docs/reference/configuration.md +358 -0
- package/docs/reference/data-and-telemetry.md +105 -42
- package/docs/reference/workflows.md +647 -0
- package/package.json +22 -11
- package/schemas/akm-asset-envelope.json +93 -0
- package/schemas/akm-config.json +81 -128
- package/schemas/akm-workflow.json +74 -73
- package/dist/assets/tasks/core/backup.yml +0 -5
- package/dist/assets/tasks/graph-refresh-weekly.yml +0 -10
- package/dist/cli/config-migrate.js +0 -1806
- package/dist/cli/config-validate.js +0 -41
- package/dist/commands/backup-cli.js +0 -56
- package/dist/commands/bundle/bundle-cli.js +0 -68
- package/dist/commands/bundle/bundle.js +0 -219
- package/dist/commands/graph/graph-cli.js +0 -124
- package/dist/commands/graph/graph.js +0 -489
- package/dist/commands/improve/extract-watch.js +0 -140
- package/dist/commands/mv-cli.js +0 -1221
- package/dist/commands/sources/history.js +0 -201
- package/dist/commands/tasks/default-tasks.js +0 -186
- package/dist/core/migration-backup.js +0 -1234
- package/dist/indexer/usage/unmigrated-vaults-guard.js +0 -95
- package/dist/llm/memory-infer-impl.js +0 -138
- package/dist/migrate/legacy/config-source-migration.js +0 -223
- package/dist/migrate/legacy/content-migration.js +0 -305
- package/dist/migrate/legacy/legacy-layout.js +0 -779
- package/dist/migrate/legacy/legacy-paths.js +0 -25
- package/dist/migrate/legacy/legacy-stash-json.js +0 -72
- package/dist/migrate/legacy/proposal-fs-import.js +0 -168
- package/dist/migrate/legacy/task-target-ref-migration.js +0 -272
- package/dist/migrate/legacy/three-db-cutover.js +0 -841
- package/dist/migrate/legacy/workflow-migrations-bodies.js +0 -52
- package/dist/migrate/legacy/workflow-migrations-frozen.js +0 -21
- package/dist/migrate/legacy-ref-grammar.js +0 -214
- package/dist/output/shapes/distill.js +0 -14
- package/dist/output/shapes/history.js +0 -11
- package/dist/output/text/distill.js +0 -6
- package/dist/output/text/enable-disable.js +0 -8
- package/dist/output/text/history.js +0 -6
- package/dist/registry/build-index.js +0 -382
- package/dist/schemas/akm-config.json +0 -4704
- package/dist/schemas/akm-task.json +0 -87
- package/dist/schemas/akm-workflow.json +0 -372
- package/dist/scripts/migrate-storage.js +0 -3816
- package/dist/workflows/authoring/workflow-program-template.yaml +0 -31
- package/dist/workflows/cli.js +0 -53
- package/dist/workflows/exec/brief.js +0 -481
- package/dist/workflows/exec/report.js +0 -1460
- package/dist/workflows/exec/watch.js +0 -116
- package/dist/workflows/program/parser.js +0 -813
- package/dist/workflows/program/project.js +0 -104
|
@@ -4,32 +4,18 @@
|
|
|
4
4
|
import { createHash } from "node:crypto";
|
|
5
5
|
import fs from "node:fs";
|
|
6
6
|
import path from "node:path";
|
|
7
|
-
import readline from "node:readline";
|
|
8
7
|
import consolidateSystemPrompt from "../../assets/prompts/consolidate-system.md" with { type: "text" };
|
|
8
|
+
import { detectAdapterId } from "../../core/adapter/detect-adapter.js";
|
|
9
9
|
import { assembleAssetFromString, serializeFrontmatter } from "../../core/asset/asset-serialize.js";
|
|
10
10
|
import { parseFrontmatter } from "../../core/asset/frontmatter.js";
|
|
11
11
|
import { conceptIdFromTypeName, displayRef, parseRefInput } from "../../core/asset/resolve-ref.js";
|
|
12
|
-
import { timestampForFilename } from "../../core/common.js";
|
|
13
12
|
import { getImproveProcessConfig, loadConfig } from "../../core/config/config.js";
|
|
14
|
-
import { ConfigError } from "../../core/errors.js";
|
|
15
|
-
import { advanceTxn, beginTxn, cleanupTxn, registerTxnKind, txnNamespaceDir, } from "../../core/fs-txn.js";
|
|
16
|
-
// Note: appendEvent import removed (WS-3a: archive TTL machinery retired)
|
|
17
13
|
import { parseEmbeddedJsonResponse } from "../../core/parse.js";
|
|
18
14
|
import { resolveStandardsContext } from "../../core/standards/resolve-standards-context.js";
|
|
19
|
-
import { detectTruncatedDescription } from "../../core/text-truncation.js";
|
|
20
|
-
import { parseSinceToIsoLenient } from "../../core/time.js";
|
|
21
|
-
import { isProposalSkipped, listProposals, proposalContent } from "../proposal/repository.js";
|
|
22
|
-
import { hasSupersededStatus, MERGE_ABSOLUTE_FLOOR_CHARS, MERGE_SHRINK_RATIO_MIN, validateProposalFrontmatter, } from "../proposal/validators/proposal-quality-validators.js";
|
|
23
|
-
import { checkGenerationGuard, checkLexicalDiversity, checkMergeInformationFloor, computeMergedGeneration, readAssetGeneration, } from "./anti-collapse.js";
|
|
24
|
-
import { cacheHash, stripFrontmatterBody } from "./content-hash.js";
|
|
25
|
-
import { writeContradictEdge } from "./memory/memory-belief.js";
|
|
26
|
-
import { emitProposal } from "./proposal-envelope.js";
|
|
27
|
-
import { createRunContext } from "./run-context.js";
|
|
28
|
-
// Re-export the moved helpers so existing test imports continue to resolve.
|
|
29
|
-
export { hasSupersededStatus, validateProposalFrontmatter };
|
|
30
15
|
import { openStateDatabase } from "../../core/state-db.js";
|
|
31
|
-
import {
|
|
32
|
-
import {
|
|
16
|
+
import { parseSinceToIsoLenient } from "../../core/time.js";
|
|
17
|
+
import { warn, warnVerbose } from "../../core/warn.js";
|
|
18
|
+
import { resolveWriteTarget } from "../../core/write-source.js";
|
|
33
19
|
import { getDefaultLlmConfig } from "../../integrations/agent/engine-resolution.js";
|
|
34
20
|
import { materializeLlmRunnerConnection, resolveImproveProcessRunner } from "../../integrations/agent/runner.js";
|
|
35
21
|
import { cosineSimilarity, embedBatch, resolveEmbeddingModelId } from "../../llm/embedder.js";
|
|
@@ -38,28 +24,27 @@ import { getBodyEmbeddings, upsertBodyEmbeddings } from "../../storage/repositor
|
|
|
38
24
|
import { closeDatabase, openExistingDatabase } from "../../storage/repositories/index-connection.js";
|
|
39
25
|
import { findEntryIdByRef, getAllEntries, getEntryById } from "../../storage/repositories/index-entries-repository.js";
|
|
40
26
|
import { getNeighborsByEntryId } from "../../storage/repositories/index-vec-repository.js";
|
|
27
|
+
import { isProposalSkipped, listProposals, proposalContent } from "../proposal/repository.js";
|
|
28
|
+
import { hasSupersededStatus, validateProposalFrontmatter } from "../proposal/validators/proposal-quality-validators.js";
|
|
29
|
+
import { cacheHash } from "./content-hash.js";
|
|
41
30
|
import { resolveImproveStrategy, resolveProcessEnabled } from "./improve-strategies.js";
|
|
31
|
+
import { emitProposal } from "./proposal-envelope.js";
|
|
32
|
+
import { createRunContext } from "./run-context.js";
|
|
42
33
|
// Chunk sizing + per-chunk prompt assembly live in ./consolidate/chunking.
|
|
43
|
-
// Imported for internal use by the orchestrator and re-exported for importers.
|
|
44
34
|
import { buildChunkPrompt, computeSafeChunkSize, DEFAULT_CONTEXT_LENGTH_TOKENS } from "./consolidate/chunking.js";
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
// ./consolidate/sanitize. Imported for internal use + re-exported for importers.
|
|
48
|
-
import { normalizeUpdatedField, sanitizeMergedContent } from "./consolidate/sanitize.js";
|
|
49
|
-
export { normalizeUpdatedField, sanitizeMergedContent, stripOuterCodeFence } from "./consolidate/sanitize.js";
|
|
50
|
-
// Eligibility / safety predicates live in ./consolidate/eligibility. Imported
|
|
51
|
-
// for internal guard use; the two public predicates are re-exported.
|
|
52
|
-
import { consolidateGuardStatus, isConsolidationEligibleMemoryName, isHotCapturedMemory, } from "./consolidate/eligibility.js";
|
|
53
|
-
export { isConsolidationEligibleMemoryName, isHotCapturedMemory } from "./consolidate/eligibility.js";
|
|
35
|
+
// Eligibility / safety predicates live in ./consolidate/eligibility.
|
|
36
|
+
import { isConsolidationEligibleMemoryName, isHotCapturedMemory } from "./consolidate/eligibility.js";
|
|
54
37
|
// Plan parsing / merging (pure op-reconciliation algebra) lives in
|
|
55
|
-
// ./consolidate/merge.
|
|
38
|
+
// ./consolidate/merge.
|
|
56
39
|
import { isValidOp, mergePlans } from "./consolidate/merge.js";
|
|
57
|
-
|
|
40
|
+
// LLM-output sanitization (pure string/frontmatter transforms) lives in
|
|
41
|
+
// ./consolidate/sanitize.
|
|
42
|
+
import { sanitizeMergedContent } from "./consolidate/sanitize.js";
|
|
58
43
|
// ── Prompts ─────────────────────────────────────────────────────────────────
|
|
59
44
|
const CONSOLIDATE_SYSTEM_PROMPT = consolidateSystemPrompt;
|
|
60
45
|
/**
|
|
61
46
|
* JSON Schema for structured consolidate plans (PR 1 of the asset-writers
|
|
62
|
-
* decision — see knowledge
|
|
47
|
+
* decision — see knowledge/projects/akm/asset-writers-investigation/00-synthesis).
|
|
63
48
|
* Mirrors the {ops[], warnings?[]} shape currently described in
|
|
64
49
|
* CONSOLIDATE_SYSTEM_PROMPT. Providers with `supportsJsonSchema: true` enforce
|
|
65
50
|
* the shape upstream so the chunk-level "invalid plan from AI — skipping"
|
|
@@ -90,6 +75,7 @@ export const CONSOLIDATE_PLAN_JSON_SCHEMA = {
|
|
|
90
75
|
secondaries: {
|
|
91
76
|
type: "array",
|
|
92
77
|
minItems: 1,
|
|
78
|
+
maxItems: 1,
|
|
93
79
|
items: { type: "string", minLength: 1 },
|
|
94
80
|
},
|
|
95
81
|
mergeStrategy: { type: "string", minLength: 1 },
|
|
@@ -142,7 +128,7 @@ export const CONSOLIDATE_PLAN_JSON_SCHEMA = {
|
|
|
142
128
|
},
|
|
143
129
|
},
|
|
144
130
|
};
|
|
145
|
-
async function clusterMemoriesBySimilarity(memories, config, stateDb) {
|
|
131
|
+
async function clusterMemoriesBySimilarity(memories, config, stateDb, signal) {
|
|
146
132
|
const noTelemetry = { embedMs: 0, cacheHits: 0, cacheMisses: 0 };
|
|
147
133
|
if (memories.length < 3 || !config.embedding)
|
|
148
134
|
return { ordered: memories, embedTelemetry: noTelemetry };
|
|
@@ -193,7 +179,7 @@ async function clusterMemoriesBySimilarity(memories, config, stateDb) {
|
|
|
193
179
|
if (missTexts.length > 0) {
|
|
194
180
|
const embedStart = Date.now();
|
|
195
181
|
try {
|
|
196
|
-
missVecs = await embedBatch(missTexts, config.embedding);
|
|
182
|
+
missVecs = await embedBatch(missTexts, config.embedding, signal);
|
|
197
183
|
}
|
|
198
184
|
catch {
|
|
199
185
|
// Fail open: embedding failures degrade gracefully to original order.
|
|
@@ -300,157 +286,19 @@ function loadPendingConsolidateProposalHashes(stashDir) {
|
|
|
300
286
|
}
|
|
301
287
|
return hashes;
|
|
302
288
|
}
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
// so the engine never classifies these as rollback-able; the registered
|
|
306
|
-
// handlers below abort loudly if generic recovery ever reaches one.
|
|
307
|
-
const CONSOLIDATE_TXN_PHASES = ["applying", "committed"];
|
|
308
|
-
function consolidateBackupDir(txn) {
|
|
309
|
-
return path.join(txn.dir, "backup");
|
|
310
|
-
}
|
|
311
|
-
/** Open the checklist journal for this run (durably, before any mutation). */
|
|
312
|
-
function beginConsolidateTxn(stashDir, ops) {
|
|
313
|
-
return beginTxn({
|
|
314
|
-
kind: CONSOLIDATE_TXN_KIND,
|
|
315
|
-
root: stashDir,
|
|
316
|
-
changes: [],
|
|
317
|
-
payload: { startedAt: new Date().toISOString(), operations: ops, completed: [] },
|
|
318
|
-
});
|
|
319
|
-
}
|
|
320
|
-
/** Durably mark one op complete on the checklist. Best-effort, like before. */
|
|
321
|
-
function markJournalCompleted(txn, opRef) {
|
|
322
|
-
try {
|
|
323
|
-
txn.journal.payload.completed.push(opRef);
|
|
324
|
-
advanceTxn(txn, "applying");
|
|
325
|
-
}
|
|
326
|
-
catch {
|
|
327
|
-
// best-effort
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
/**
|
|
331
|
-
* Run-entry recovery decision for stale consolidate journals (unchanged
|
|
332
|
-
* semantics: `abort` refuses the run, `clean` removes stale artifacts). A
|
|
333
|
-
* journal whose checklist is complete is a committed leftover and is swept
|
|
334
|
-
* quietly — under the legacy single-path home the next run's journal write
|
|
335
|
-
* overwrote it as a side effect; per-transaction dirs need the sweep to be
|
|
336
|
-
* explicit (the legacy orphaned-backup leak documented in the journal
|
|
337
|
-
* lifecycle golden is gone with it).
|
|
338
|
-
*/
|
|
339
|
-
function checkForIncompleteJournal(stashDir, recoveryMode, warnings) {
|
|
340
|
-
const nsDir = txnNamespaceDir(stashDir);
|
|
341
|
-
if (!fs.existsSync(nsDir))
|
|
342
|
-
return;
|
|
343
|
-
for (const entry of fs.readdirSync(nsDir, { withFileTypes: true })) {
|
|
344
|
-
if (!entry.isDirectory())
|
|
345
|
-
continue;
|
|
346
|
-
const transactionDir = path.join(nsDir, entry.name);
|
|
347
|
-
const journalPath = path.join(transactionDir, "journal.json");
|
|
348
|
-
if (!fs.existsSync(journalPath))
|
|
349
|
-
continue;
|
|
350
|
-
let journal;
|
|
351
|
-
try {
|
|
352
|
-
journal = JSON.parse(fs.readFileSync(journalPath, "utf8"));
|
|
353
|
-
}
|
|
354
|
-
catch {
|
|
355
|
-
// An unreadable journal CANNOT be attributed to a kind — it may fence a
|
|
356
|
-
// sibling domain's interrupted, irreversible mutation (mv/proposal
|
|
357
|
-
// journals share the stash namespace). Never destroy it from here:
|
|
358
|
-
// `clean` skips it with a warning; the default aborts fail-closed with
|
|
359
|
-
// kind-agnostic guidance.
|
|
360
|
-
if (recoveryMode === "clean") {
|
|
361
|
-
warnings.push(`Unreadable transaction journal at ${journalPath} may belong to another transaction kind; not removed — inspect manually.`);
|
|
362
|
-
continue;
|
|
363
|
-
}
|
|
364
|
-
throw new ConfigError(`Incomplete transaction state detected: unreadable journal at ${journalPath}. It may fence an interrupted mv or proposal mutation — inspect or remove the file manually.`, "INVALID_CONFIG_FILE");
|
|
365
|
-
}
|
|
366
|
-
if (journal.kind !== CONSOLIDATE_TXN_KIND)
|
|
367
|
-
continue;
|
|
368
|
-
const operationCount = Array.isArray(journal.payload?.operations) ? journal.payload.operations.length : 0;
|
|
369
|
-
const completedCount = Array.isArray(journal.payload?.completed) ? journal.payload.completed.length : 0;
|
|
370
|
-
if (completedCount >= operationCount) {
|
|
371
|
-
// Committed leftover — sweep quietly (see doc comment above).
|
|
372
|
-
cleanupTxn(transactionDir);
|
|
373
|
-
continue;
|
|
374
|
-
}
|
|
375
|
-
if (recoveryMode === "clean") {
|
|
376
|
-
const cleanupWarning = cleanupTxn(transactionDir);
|
|
377
|
-
warnings.push(cleanupWarning
|
|
378
|
-
? `Failed to remove stale consolidation journal at ${journalPath}.`
|
|
379
|
-
: `Removed stale consolidation journal at ${journalPath} (${completedCount}/${operationCount} operations completed).`);
|
|
380
|
-
continue;
|
|
381
|
-
}
|
|
382
|
-
throw new ConfigError(`Incomplete consolidation run detected at ${journalPath} (${completedCount}/${operationCount} operations completed). Re-run with --consolidate-recovery clean to remove stale journal artifacts. Backup dir: ${path.join(transactionDir, "backup")}.`, "INVALID_CONFIG_FILE");
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
function backupFile(filePath, backupDir, name) {
|
|
386
|
-
try {
|
|
387
|
-
fs.mkdirSync(backupDir, { recursive: true });
|
|
388
|
-
fs.copyFileSync(filePath, path.join(backupDir, `${name}.md`));
|
|
389
|
-
}
|
|
390
|
-
catch {
|
|
391
|
-
// best-effort
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
// ── WS-3b: Generation frontmatter injection ───────────────────────────────────
|
|
395
|
-
/**
|
|
396
|
-
* Inject `generation` and canonical `xrefs` into merged content.
|
|
397
|
-
* generation = max(sourceGenerations) + 1.
|
|
398
|
-
* xrefs = UNION of the provided provenance refs (participants + their cited
|
|
399
|
-
* sources) with anything already present in xrefs or legacy source_refs —
|
|
400
|
-
* R5 §4.2: the old set-if-absent behavior dropped second-generation
|
|
401
|
-
* provenance whenever the LLM emitted its own (partial) source_refs.
|
|
402
|
-
* Fails open — returns original content if frontmatter can't be parsed.
|
|
403
|
-
*/
|
|
404
|
-
export function injectGenerationFrontmatter(mergedContent, sourceGenerations, provenanceRefs) {
|
|
405
|
-
try {
|
|
406
|
-
const parsed = parseFrontmatter(mergedContent);
|
|
407
|
-
const existingFm = parsed.data;
|
|
408
|
-
const existingRefs = [
|
|
409
|
-
...(Array.isArray(existingFm.xrefs) ? existingFm.xrefs.map(String) : []),
|
|
410
|
-
...(Array.isArray(existingFm.source_refs) ? existingFm.source_refs.map(String) : []),
|
|
411
|
-
];
|
|
412
|
-
const canonicalRefs = [...existingRefs, ...provenanceRefs].flatMap((ref) => {
|
|
413
|
-
try {
|
|
414
|
-
// Canonical stored provenance xref, WI-8.5b display-flip: emit the D-R5
|
|
415
|
-
// new-grammar spelling (short conceptId for the primary/default bundle,
|
|
416
|
-
// `bundle//conceptId` for a slug source, legacy `origin//type:name` only
|
|
417
|
-
// for a non-slug registry origin) via `displayRef`, so a merged asset's
|
|
418
|
-
// stored `xrefs` unify with the WI-8.5a content/frontmatter xref writer.
|
|
419
|
-
const p = parseRefInput(ref);
|
|
420
|
-
return [displayRef({ type: p.type, name: p.name, bundleId: p.origin })];
|
|
421
|
-
}
|
|
422
|
-
catch {
|
|
423
|
-
return [];
|
|
424
|
-
}
|
|
425
|
-
});
|
|
426
|
-
const updatedFm = {
|
|
427
|
-
...existingFm,
|
|
428
|
-
generation: computeMergedGeneration(sourceGenerations),
|
|
429
|
-
xrefs: [...new Set(canonicalRefs)],
|
|
430
|
-
};
|
|
431
|
-
delete updatedFm.source_refs;
|
|
432
|
-
return assembleAssetFromString(serializeFrontmatter(updatedFm), parsed.content);
|
|
433
|
-
}
|
|
434
|
-
catch {
|
|
435
|
-
return mergedContent; // fail open
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
/**
|
|
439
|
-
* WI-8.5b — canonical D-R5 display spelling of a stored provenance xref: short
|
|
440
|
-
* conceptId for the primary/default bundle, `bundle//conceptId` for a slug
|
|
441
|
-
* source, legacy `origin//type:name` only for a non-slug registry origin.
|
|
442
|
-
* Returns the input unchanged when it cannot be parsed as a ref (fail-open, the
|
|
443
|
-
* pre-flip behavior for hand-authored provenance strings).
|
|
444
|
-
*/
|
|
445
|
-
function canonicalXref(ref) {
|
|
289
|
+
/** Parse a stored provenance ref and emit its canonical D-R5 display spelling. */
|
|
290
|
+
function canonicalStoredXref(ref) {
|
|
446
291
|
try {
|
|
447
292
|
const p = parseRefInput(ref);
|
|
448
293
|
return displayRef({ type: p.type, name: p.name, bundleId: p.origin });
|
|
449
294
|
}
|
|
450
295
|
catch {
|
|
451
|
-
return
|
|
296
|
+
return undefined;
|
|
452
297
|
}
|
|
453
298
|
}
|
|
299
|
+
function canonicalXref(ref) {
|
|
300
|
+
return canonicalStoredXref(ref) ?? ref;
|
|
301
|
+
}
|
|
454
302
|
/**
|
|
455
303
|
* The promoted asset's provenance xref set: existing body-frontmatter xrefs +
|
|
456
304
|
* the promoted source ref, deduped after canonicalization (WI-8.5b: emitted in
|
|
@@ -460,62 +308,13 @@ function promoteProvenanceXrefs(existing, sourceRef) {
|
|
|
460
308
|
const priors = Array.isArray(existing) ? existing.map(String) : [];
|
|
461
309
|
return [...new Set([...priors, sourceRef].map(canonicalXref))];
|
|
462
310
|
}
|
|
463
|
-
// ── Archive helper (P1-B: soft-invalidation) ─────────────────────────────────
|
|
464
|
-
/**
|
|
465
|
-
* Move a memory asset to `.akm/archive/` with `status: superseded` frontmatter
|
|
466
|
-
* instead of deleting it outright. The live stash delete still happens after
|
|
467
|
-
* this call — this is belt-and-suspenders archival that survives the hard delete.
|
|
468
|
-
*
|
|
469
|
-
* Archive filename: `<iso-ts>-<opIndex>-<basename>.md`
|
|
470
|
-
* New frontmatter fields: status, superseded_at, superseded_by (optional),
|
|
471
|
-
* superseded_reason.
|
|
472
|
-
*/
|
|
473
|
-
function archiveMemory(filePath, stashDir, ref, reason, opIndex, supersededBy, warnings) {
|
|
474
|
-
const archiveDir = path.join(stashDir, ".akm", "archive");
|
|
475
|
-
fs.mkdirSync(archiveDir, { recursive: true });
|
|
476
|
-
let raw;
|
|
477
|
-
try {
|
|
478
|
-
raw = fs.readFileSync(filePath, "utf8");
|
|
479
|
-
}
|
|
480
|
-
catch {
|
|
481
|
-
if (warnings)
|
|
482
|
-
warnings.push(`archiveMemory: could not read ${ref} for archiving — skipping archive write`);
|
|
483
|
-
return;
|
|
484
|
-
}
|
|
485
|
-
let content = raw;
|
|
486
|
-
try {
|
|
487
|
-
const parsed = parseFrontmatter(raw);
|
|
488
|
-
const newFm = {
|
|
489
|
-
...parsed.data,
|
|
490
|
-
status: "superseded",
|
|
491
|
-
superseded_at: new Date().toISOString(),
|
|
492
|
-
...(supersededBy ? { superseded_by: supersededBy } : {}),
|
|
493
|
-
superseded_reason: reason,
|
|
494
|
-
};
|
|
495
|
-
content = assembleAssetFromString(serializeFrontmatter(newFm), parsed.content);
|
|
496
|
-
}
|
|
497
|
-
catch {
|
|
498
|
-
if (warnings)
|
|
499
|
-
warnings.push(`archiveMemory: could not parse frontmatter for ${ref} — archiving raw`);
|
|
500
|
-
}
|
|
501
|
-
const ts = timestampForFilename();
|
|
502
|
-
const safeName = path.basename(filePath, ".md");
|
|
503
|
-
const archivePath = path.join(archiveDir, `${ts}-${opIndex}-${safeName}.md`);
|
|
504
|
-
try {
|
|
505
|
-
fs.writeFileSync(archivePath, content, "utf8");
|
|
506
|
-
}
|
|
507
|
-
catch (e) {
|
|
508
|
-
if (warnings)
|
|
509
|
-
warnings.push(`archiveMemory: write failed for ${ref}: ${String(e)}`);
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
311
|
// ── LLM resolution ──────────────────────────────────────────────────────────
|
|
513
312
|
/**
|
|
514
313
|
* Resolve the LLM connection for the consolidate pass.
|
|
515
314
|
*
|
|
516
315
|
* Priority order (mirrors extract / reflect / distill — see
|
|
517
|
-
* `src/commands/extract.ts
|
|
518
|
-
* `resolveImproveProcessRunner` pattern):
|
|
316
|
+
* `resolveExtractRunConfig` in `src/commands/improve/extract.ts` and the
|
|
317
|
+
* canonical `resolveImproveProcessRunner` pattern):
|
|
519
318
|
*
|
|
520
319
|
* 1. `improve.strategies.<name>.processes.consolidate.engine`
|
|
521
320
|
* via {@link resolveImproveProcessRunner}. Lets the user pin
|
|
@@ -562,22 +361,24 @@ export function makeConsolidateResult(overrides) {
|
|
|
562
361
|
// ── Main entry point ─────────────────────────────────────────────────────────
|
|
563
362
|
function resolveConsolidationWriteTarget(opts, config) {
|
|
564
363
|
if (opts.writeTarget) {
|
|
364
|
+
const root = path.resolve(opts.writeTarget.source.path);
|
|
565
365
|
return {
|
|
566
366
|
...opts.writeTarget,
|
|
567
|
-
source: {
|
|
367
|
+
source: {
|
|
368
|
+
...opts.writeTarget.source,
|
|
369
|
+
path: root,
|
|
370
|
+
adapterId: opts.writeTarget.source.adapterId ?? detectAdapterId(root),
|
|
371
|
+
},
|
|
568
372
|
};
|
|
569
373
|
}
|
|
570
|
-
if (opts.target
|
|
374
|
+
if (opts.target) {
|
|
571
375
|
const target = resolveWriteTarget(config, opts.target);
|
|
572
376
|
return { ...target, source: { ...target.source, path: path.resolve(target.source.path) } };
|
|
573
377
|
}
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
const explicitRoot = opts.target ?? opts.stashDir;
|
|
577
|
-
if (explicitRoot) {
|
|
578
|
-
const root = path.resolve(explicitRoot);
|
|
378
|
+
if (opts.stashDir) {
|
|
379
|
+
const root = path.resolve(opts.stashDir);
|
|
579
380
|
return {
|
|
580
|
-
source: { kind: "filesystem", name: "stash", path: root },
|
|
381
|
+
source: { kind: "filesystem", name: "stash", path: root, adapterId: detectAdapterId(root) },
|
|
581
382
|
config: { type: "filesystem", name: "stash", path: root, writable: true },
|
|
582
383
|
};
|
|
583
384
|
}
|
|
@@ -600,7 +401,7 @@ export async function akmConsolidate(opts = {}) {
|
|
|
600
401
|
// handle. consolidate.ts has no `eventsCtx`/proposals-`ctx` option at all
|
|
601
402
|
// (WS-3a retired its only appendEvent usage; `emitProposal` here is always
|
|
602
403
|
// called with the default, seam-less ProposalsContext — see
|
|
603
|
-
//
|
|
404
|
+
// emitPromotionProposal below), so both get the safe empty-object default,
|
|
604
405
|
// behaviorally identical to `undefined` (EventsContext/ProposalsContext
|
|
605
406
|
// fields are all optional-chained by their consumers). `getLlmConfig`
|
|
606
407
|
// mirrors `planConsolidation`'s own resolution (`resolveConsolidateLlmConfig`)
|
|
@@ -624,6 +425,7 @@ export async function akmConsolidate(opts = {}) {
|
|
|
624
425
|
dryRun: opts.dryRun ?? false,
|
|
625
426
|
signal: opts.signal,
|
|
626
427
|
});
|
|
428
|
+
const warnings = [];
|
|
627
429
|
if (!resolveProcessEnabled("consolidate", opts.improveProfile ?? resolveImproveStrategy(undefined, config).config)) {
|
|
628
430
|
return makeConsolidateResult({
|
|
629
431
|
// Sourced from runContext (identical value to `opts.dryRun ?? false`)
|
|
@@ -633,10 +435,9 @@ export async function akmConsolidate(opts = {}) {
|
|
|
633
435
|
dryRun: runContext.dryRun,
|
|
634
436
|
target: opts.target ?? stashDir,
|
|
635
437
|
durationMs: Date.now() - startMs,
|
|
438
|
+
warnings,
|
|
636
439
|
});
|
|
637
440
|
}
|
|
638
|
-
const warnings = [];
|
|
639
|
-
checkForIncompleteJournal(stashDir, opts.recoveryMode ?? "abort", warnings);
|
|
640
441
|
// WS-3a: open one state.db handle shared by the body-embedding cache (dedup
|
|
641
442
|
// + cluster) and the judged-state cache. All callers in the function body
|
|
642
443
|
// receive this handle; it is closed in the `finally` block below.
|
|
@@ -649,7 +450,7 @@ export async function akmConsolidate(opts = {}) {
|
|
|
649
450
|
// State DB unavailable → skip the embedding cache for this run.
|
|
650
451
|
}
|
|
651
452
|
try {
|
|
652
|
-
return await akmConsolidateInner(opts, config, stashDir, startMs,
|
|
453
|
+
return await akmConsolidateInner(opts, config, stashDir, startMs, warnings, sharedStateDb);
|
|
653
454
|
}
|
|
654
455
|
finally {
|
|
655
456
|
sharedStateDb?.close();
|
|
@@ -903,7 +704,12 @@ async function judgeConsolidationChunks(args) {
|
|
|
903
704
|
{ role: "system", content: CONSOLIDATE_SYSTEM_PROMPT },
|
|
904
705
|
{ role: "user", content: userPrompt },
|
|
905
706
|
],
|
|
906
|
-
request: {
|
|
707
|
+
request: {
|
|
708
|
+
responseSchema: CONSOLIDATE_PLAN_JSON_SCHEMA,
|
|
709
|
+
enableThinking: false,
|
|
710
|
+
timeoutMs: llmConfig.timeoutMs,
|
|
711
|
+
signal: opts.signal,
|
|
712
|
+
},
|
|
907
713
|
parse: (raw) => ({ ok: true, content: raw ?? "" }),
|
|
908
714
|
// A transport throw was caught INSIDE the gated fn and returned as an
|
|
909
715
|
// {ok:false} envelope (never reaching the gate's fallback); onError
|
|
@@ -933,9 +739,12 @@ async function judgeConsolidationChunks(args) {
|
|
|
933
739
|
}
|
|
934
740
|
raw = retry;
|
|
935
741
|
}
|
|
936
|
-
|
|
742
|
+
// C9 action 1: AKM_DEBUG_LLM was a separate, undocumented env var for this
|
|
743
|
+
// one diagnostic; folded into the standard AKM_VERBOSE gate (warnVerbose)
|
|
744
|
+
// rather than kept as its own toggle.
|
|
745
|
+
{
|
|
937
746
|
const preview = (raw.content ?? "").slice(0, 500);
|
|
938
|
-
|
|
747
|
+
warnVerbose(`[akm:consolidate] chunk ${chunkIdx + 1} raw response (first 500 chars): ${preview}`);
|
|
939
748
|
}
|
|
940
749
|
const parsed = parseEmbeddedJsonResponse(raw.content);
|
|
941
750
|
if (!parsed || !Array.isArray(parsed.operations)) {
|
|
@@ -980,7 +789,6 @@ async function planConsolidation(opts, config, stashDir, _startMs, memories, war
|
|
|
980
789
|
const llmConfig = Object.hasOwn(opts, "llmConfig")
|
|
981
790
|
? (opts.llmConfig ?? undefined)
|
|
982
791
|
: resolveConsolidateLlmConfig(config, opts.improveProfile);
|
|
983
|
-
const isHttpPath = !!llmConfig;
|
|
984
792
|
// Chunk sizing: derive a safe chunk size from the configured model context
|
|
985
793
|
// window so that the full prompt (system prompt + chunk user prompt) never
|
|
986
794
|
// exceeds the model's n_ctx limit. When no context length is configured we
|
|
@@ -996,15 +804,43 @@ async function planConsolidation(opts, config, stashDir, _startMs, memories, war
|
|
|
996
804
|
const chunkSize = computeSafeChunkSize(modelContextLength, bodyTruncation, opts.maxChunkSize);
|
|
997
805
|
// -- Phase A: plan generation -----------------------------------------------
|
|
998
806
|
const sourceName = opts.target ?? stashDir;
|
|
999
|
-
|
|
1000
|
-
|
|
807
|
+
let budgetedMemories = memories;
|
|
808
|
+
if (opts.signal) {
|
|
809
|
+
const budgetMs = opts.signal.remainingBudgetMs;
|
|
810
|
+
if (budgetMs !== undefined) {
|
|
811
|
+
const p90Chunk = opts.p90ChunkSecondsDefault ?? 30;
|
|
812
|
+
const safeChunks = Math.max(0, Math.floor((Math.max(0, budgetMs) / 1000 / p90Chunk) * 0.6));
|
|
813
|
+
const cap = safeChunks * chunkSize;
|
|
814
|
+
if (cap < memories.length) {
|
|
815
|
+
budgetedMemories = memories
|
|
816
|
+
.map((entry) => {
|
|
817
|
+
let mtimeMs = 0;
|
|
818
|
+
try {
|
|
819
|
+
mtimeMs = fs.statSync(entry.filePath).mtimeMs;
|
|
820
|
+
}
|
|
821
|
+
catch {
|
|
822
|
+
// Missing files sort first and are filtered by the existing guards.
|
|
823
|
+
}
|
|
824
|
+
return { entry, mtimeMs };
|
|
825
|
+
})
|
|
826
|
+
.sort((a, b) => a.mtimeMs - b.mtimeMs || a.entry.name.localeCompare(b.entry.name))
|
|
827
|
+
.map(({ entry }) => entry)
|
|
828
|
+
.slice(0, cap);
|
|
829
|
+
const msg = `[consolidate] cold-start budget: reducing pool from ${memories.length} to ${budgetedMemories.length} memories (${safeChunks} safe chunks; remainder deferred).`;
|
|
830
|
+
warn(msg);
|
|
831
|
+
warnings.push(msg);
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
// WS-5: capture llmPoolSize after every pre-LLM cap.
|
|
836
|
+
const llmPoolSize = budgetedMemories.length;
|
|
1001
837
|
// C-1 / #380: Pre-cluster memories by embedding similarity before chunking.
|
|
1002
838
|
// This ensures that semantically similar memories land in the same LLM
|
|
1003
839
|
// context window, allowing the model to detect and merge duplicates that
|
|
1004
840
|
// would otherwise be split across chunks and survive indefinitely.
|
|
1005
841
|
// mem0 arXiv:2504.19413, A-MEM arXiv:2502.12110.
|
|
1006
842
|
// Fails open: if embeddings are unavailable or fail, original order is used.
|
|
1007
|
-
const { ordered: clusteredMemories, embedTelemetry } = await clusterMemoriesBySimilarity(
|
|
843
|
+
const { ordered: clusteredMemories, embedTelemetry } = await clusterMemoriesBySimilarity(budgetedMemories, config, sharedStateDb, opts.signal);
|
|
1008
844
|
// WS-3b Anti-collapse step 8c: inject random (non-similar) clusters.
|
|
1009
845
|
// A small fraction (default 5%) of the pool is shuffled into random positions
|
|
1010
846
|
// so the pipeline isn't PURELY similarity-driven. This prevents rich-get-richer
|
|
@@ -1063,41 +899,7 @@ async function planConsolidation(opts, config, stashDir, _startMs, memories, war
|
|
|
1063
899
|
// 4h on this user's stack. See
|
|
1064
900
|
// /tmp/akm-health-investigations/tuning-reasons-investigation.md §Q3.
|
|
1065
901
|
const pendingProposalBodyHashes = loadPendingConsolidateProposalHashes(stashDir);
|
|
1066
|
-
|
|
1067
|
-
// Estimate wall-clock cost BEFORE issuing any LLM calls. When a signal is
|
|
1068
|
-
// provided and the estimated cost exceeds ~60% of the remaining budget we
|
|
1069
|
-
// auto-reduce the pool and log the reduction so the run never starts work
|
|
1070
|
-
// it cannot finish (avoiding SIGTERM mid-LLM-call).
|
|
1071
|
-
//
|
|
1072
|
-
// Formula: chunks.length × p90_chunk_seconds. The p90 comes from
|
|
1073
|
-
// `opts.p90ChunkSecondsDefault` (caller-supplied, typically from the profile
|
|
1074
|
-
// config); absent = 30 s (conservative default matching a medium local LLM).
|
|
1075
|
-
//
|
|
1076
|
-
// "Remaining budget" is read from a custom property on the AbortSignal if
|
|
1077
|
-
// the caller (improve.ts) has attached one. Without it no auto-reduction
|
|
1078
|
-
// fires but the check is still cheap to run.
|
|
1079
|
-
if (chunks.length > 10 && opts.signal) {
|
|
1080
|
-
const p90Chunk = opts.p90ChunkSecondsDefault ?? 30;
|
|
1081
|
-
const estimatedSeconds = chunks.length * p90Chunk;
|
|
1082
|
-
// remainingBudgetMs is a non-standard extension set by improve.ts when it
|
|
1083
|
-
// creates the budget AbortController. Undefined = no budget information.
|
|
1084
|
-
const budgetMs = opts.signal.remainingBudgetMs;
|
|
1085
|
-
if (budgetMs !== undefined && budgetMs > 0) {
|
|
1086
|
-
const remainingSeconds = budgetMs / 1000;
|
|
1087
|
-
if (estimatedSeconds > remainingSeconds * 0.6) {
|
|
1088
|
-
const safeCaps = Math.max(1, Math.floor((remainingSeconds * 0.6) / p90Chunk));
|
|
1089
|
-
const removedChunks = chunks.length - safeCaps;
|
|
1090
|
-
if (removedChunks > 0) {
|
|
1091
|
-
const msg = `[consolidate] cold-start budget: estimated ${estimatedSeconds.toFixed(0)}s > 60% of remaining ${remainingSeconds.toFixed(0)}s; ` +
|
|
1092
|
-
`reducing pool from ${chunks.length} to ${safeCaps} chunks (${removedChunks} deferred to next run).`;
|
|
1093
|
-
warn(msg);
|
|
1094
|
-
warnings.push(msg);
|
|
1095
|
-
chunks.splice(safeCaps);
|
|
1096
|
-
}
|
|
1097
|
-
}
|
|
1098
|
-
}
|
|
1099
|
-
}
|
|
1100
|
-
warn(`[consolidate] ${memories.length} memories / ${chunks.length} chunk(s) / chunk_size=${chunkSize}` +
|
|
902
|
+
warn(`[consolidate] ${budgetedMemories.length} memories / ${chunks.length} chunk(s) / chunk_size=${chunkSize}` +
|
|
1101
903
|
` / pending-proposal hashes: ${pendingProposalBodyHashes.size}`);
|
|
1102
904
|
// Consolidate output merges memories (non-wiki) → stash authoring standards.
|
|
1103
905
|
// Resolved ONCE per run and passed to each chunk prompt (facts not re-read
|
|
@@ -1117,104 +919,19 @@ async function planConsolidation(opts, config, stashDir, _startMs, memories, war
|
|
|
1117
919
|
});
|
|
1118
920
|
// Build the known-refs set from the already-filtered memory pool so
|
|
1119
921
|
// mergePlans() can reject LLM-hallucinated primary refs before execution.
|
|
1120
|
-
const knownRefs = new Set(
|
|
922
|
+
const knownRefs = new Set(budgetedMemories.map((m) => conceptIdFromTypeName("memory", m.name)));
|
|
1121
923
|
const { ops: allOps, warnings: mergeWarnings } = mergePlans(chunkOpsArrays, knownRefs);
|
|
1122
924
|
warnings.push(...mergeWarnings);
|
|
1123
|
-
return {
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
* write block. Never invoked on the dry-run or aborted-confirm paths.
|
|
1131
|
-
*/
|
|
1132
|
-
async function applyConsolidationPlan(config, stashDir, target, sourceRun, memories, warnings, allOps, accounting, activeProfile, llmConfig) {
|
|
1133
|
-
// -- Phase B + writes -------------------------------------------------------
|
|
1134
|
-
// Open the checklist journal (durably) before any mutations; backups live
|
|
1135
|
-
// under the transaction directory.
|
|
1136
|
-
const txn = beginConsolidateTxn(stashDir, allOps);
|
|
1137
|
-
const backupDir = consolidateBackupDir(txn);
|
|
1138
|
-
const counts = {
|
|
1139
|
-
merged: 0,
|
|
1140
|
-
deleted: 0,
|
|
1141
|
-
contradicted: 0, // C-3 / #382: count of contradiction edges written
|
|
1142
|
-
mergeFloorViolations: 0, // R5 §4.2: advisory merge-information-floor failures
|
|
1143
|
-
mergedSecondaries: 0,
|
|
1144
|
-
};
|
|
1145
|
-
const promoted = [];
|
|
1146
|
-
// Within-run dedup: track source refs for which a promote proposal was
|
|
1147
|
-
// already created this run. The LLM can return multiple promote ops for
|
|
1148
|
-
// different source memories that happen to have identical content (all are
|
|
1149
|
-
// duplicate memories), so we also need a content-hash guard below.
|
|
1150
|
-
const promotedSourceRefs = new Set();
|
|
1151
|
-
// Build a lookup map: ref → MemoryEntry
|
|
1152
|
-
const memoryByRef = new Map();
|
|
1153
|
-
for (const m of memories) {
|
|
1154
|
-
memoryByRef.set(conceptIdFromTypeName("memory", m.name), m);
|
|
1155
|
-
}
|
|
1156
|
-
const opCtx = {
|
|
1157
|
-
config,
|
|
1158
|
-
txn,
|
|
1159
|
-
improveProfile: activeProfile,
|
|
1160
|
-
llmConfig: llmConfig ?? null,
|
|
1161
|
-
stashDir,
|
|
1162
|
-
sourceRun,
|
|
1163
|
-
target,
|
|
1164
|
-
backupDir,
|
|
1165
|
-
memoryByRef,
|
|
1166
|
-
promoted,
|
|
1167
|
-
promotedSourceRefs,
|
|
1168
|
-
warnings,
|
|
1169
|
-
counts,
|
|
1170
|
-
pushSkipReason: accounting.pushSkipReason,
|
|
925
|
+
return {
|
|
926
|
+
allOps,
|
|
927
|
+
totalChunks: chunks.length,
|
|
928
|
+
llmPoolSize,
|
|
929
|
+
deferredMemories: memories.length - budgetedMemories.length,
|
|
930
|
+
embedTelemetry,
|
|
931
|
+
sourceName,
|
|
1171
932
|
};
|
|
1172
|
-
// Thin dispatch over the op discriminator — each branch is now an isolated,
|
|
1173
|
-
// independently-testable handler that mutates `opCtx`.
|
|
1174
|
-
for (let opIndex = 0; opIndex < allOps.length; opIndex++) {
|
|
1175
|
-
const op = allOps[opIndex];
|
|
1176
|
-
const opDisplayRef = op.op === "merge" ? op.primary : op.op === "contradict" ? `${op.ref} ↔ ${op.contradictedByRef}` : op.ref;
|
|
1177
|
-
warn(`[consolidate] ${opIndex + 1}/${allOps.length} ${op.op} ${opDisplayRef}`);
|
|
1178
|
-
switch (op.op) {
|
|
1179
|
-
case "merge":
|
|
1180
|
-
await handleMergeOp(op, opIndex, opCtx);
|
|
1181
|
-
break;
|
|
1182
|
-
case "delete":
|
|
1183
|
-
await handleDeleteOp(op, opIndex, opCtx);
|
|
1184
|
-
break;
|
|
1185
|
-
case "promote":
|
|
1186
|
-
await handlePromoteOp(op, opCtx);
|
|
1187
|
-
break;
|
|
1188
|
-
case "contradict":
|
|
1189
|
-
await handleContradictOp(op, opCtx);
|
|
1190
|
-
break;
|
|
1191
|
-
}
|
|
1192
|
-
}
|
|
1193
|
-
const { merged, deleted, contradicted, mergeFloorViolations, mergedSecondaries } = counts;
|
|
1194
|
-
// 0.9.0 (issue #507): batch-at-boundary commit. The merge/delete loop above
|
|
1195
|
-
// wrote one merged primary and deleted N secondaries to the resolved target
|
|
1196
|
-
// with NO per-asset commit. If the target is a writable git source and any
|
|
1197
|
-
// asset was mutated, commit the whole batch ONCE here (stages .akm/ +
|
|
1198
|
-
// siblings together). No-op for filesystem/primary-stash targets.
|
|
1199
|
-
if (merged > 0 || deleted > 0) {
|
|
1200
|
-
commitWriteTargetBoundary(target, `Consolidate: ${merged} merged, ${deleted} removed`);
|
|
1201
|
-
}
|
|
1202
|
-
advanceTxn(txn, "committed");
|
|
1203
|
-
cleanupTxn(txn.dir);
|
|
1204
|
-
// [signoff 2026-06-15] TTL archive cleanup machinery RETIRED (WS-3a).
|
|
1205
|
-
// The elaborate archiveRetentionDays / archive-dir scan existed only to satisfy
|
|
1206
|
-
// the old irrecoverability constraint. Stashes are now git-backed, so git
|
|
1207
|
-
// history is the recovery path — no bespoke archive TTL needed. Any files in
|
|
1208
|
-
// .akm/archive/ will stay there harmlessly until the operator prunes them with
|
|
1209
|
-
// `git rm` or `find .akm/archive -mtime +90 -delete`. Changed N files this
|
|
1210
|
-
// run; recover any via `git show <sha>:<path>` or `git restore <path>`.
|
|
1211
|
-
if (merged > 0 || deleted > 0) {
|
|
1212
|
-
const totalChanged = merged + deleted;
|
|
1213
|
-
warnings.push(`Changed ${totalChanged} file(s) this run. Recover any via git if needed (git history is the backstop).`);
|
|
1214
|
-
}
|
|
1215
|
-
return { merged, deleted, contradicted, mergeFloorViolations, mergedSecondaries, promoted };
|
|
1216
933
|
}
|
|
1217
|
-
async function akmConsolidateInner(opts, config, stashDir, startMs,
|
|
934
|
+
async function akmConsolidateInner(opts, config, stashDir, startMs, warnings, sharedStateDb) {
|
|
1218
935
|
// -- Pass 1: narrow the memory pool (may early-return an envelope) ----------
|
|
1219
936
|
const narrowed = await narrowConsolidationPool(opts, stashDir, startMs, warnings);
|
|
1220
937
|
if (narrowed.done)
|
|
@@ -1222,14 +939,14 @@ async function akmConsolidateInner(opts, config, stashDir, startMs, sourceRun, w
|
|
|
1222
939
|
const { memories, dedupPoolSize } = narrowed;
|
|
1223
940
|
// -- Pass 2: build the LLM plan (populates the shared accounting counters) ---
|
|
1224
941
|
const accounting = createConsolidateAccounting();
|
|
1225
|
-
const { allOps, totalChunks, llmPoolSize,
|
|
942
|
+
const { allOps, totalChunks, llmPoolSize, deferredMemories, embedTelemetry, sourceName } = await planConsolidation(opts, config, stashDir, startMs, memories, warnings, sharedStateDb, accounting);
|
|
1226
943
|
// -- Dry-run: show AI plan without executing any writes --------------------
|
|
1227
944
|
if (opts.dryRun) {
|
|
1228
945
|
return makeConsolidateResult({
|
|
1229
946
|
dryRun: true,
|
|
1230
947
|
previewOnly: true,
|
|
1231
948
|
target: sourceName,
|
|
1232
|
-
processed:
|
|
949
|
+
processed: llmPoolSize,
|
|
1233
950
|
failedChunks: accounting.totalChunksFailed,
|
|
1234
951
|
totalChunks,
|
|
1235
952
|
judgedNoAction: accounting.judgedNoAction,
|
|
@@ -1238,420 +955,64 @@ async function akmConsolidateInner(opts, config, stashDir, startMs, sourceRun, w
|
|
|
1238
955
|
// provably still 0 here (it only increments in the op-execution loop).
|
|
1239
956
|
mergedSecondaries: 0,
|
|
1240
957
|
failedChunkMemories: accounting.failedChunkMemories,
|
|
958
|
+
deferredMemories,
|
|
1241
959
|
planned: allOps,
|
|
1242
960
|
warnings,
|
|
1243
961
|
durationMs: Date.now() - startMs,
|
|
1244
962
|
});
|
|
1245
963
|
}
|
|
1246
964
|
warn(`[consolidate] plan: ${allOps.length} operation(s)`);
|
|
1247
|
-
//
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
// No merge executed on the abort path — mergedSecondaries is still 0.
|
|
1272
|
-
mergedSecondaries: 0,
|
|
1273
|
-
failedChunkMemories: accounting.failedChunkMemories,
|
|
1274
|
-
planned: allOps,
|
|
1275
|
-
warnings: [...warnings, nonInteractive ? "Non-interactive context: skipped apply." : "Aborted by user."],
|
|
1276
|
-
durationMs: Date.now() - startMs,
|
|
1277
|
-
});
|
|
1278
|
-
}
|
|
1279
|
-
}
|
|
965
|
+
// Destructive operations remain advisory. Promote is safe to execute because
|
|
966
|
+
// it emits a reviewable proposal rather than mutating an asset.
|
|
967
|
+
const promoted = [];
|
|
968
|
+
const promotionFailures = { count: 0 };
|
|
969
|
+
const memoryByRef = new Map(memories.map((memory) => [conceptIdFromTypeName("memory", memory.name), memory]));
|
|
970
|
+
const promoteContext = {
|
|
971
|
+
config,
|
|
972
|
+
stashDir,
|
|
973
|
+
sourceRun: opts.sourceRun ?? `consolidate-${startMs}`,
|
|
974
|
+
proposalsCtx: opts.proposalsCtx,
|
|
975
|
+
target: opts.writeTarget,
|
|
976
|
+
memoryByRef,
|
|
977
|
+
promoted,
|
|
978
|
+
promotedSourceRefs: new Set(),
|
|
979
|
+
promotionFailures,
|
|
980
|
+
warnings,
|
|
981
|
+
pushSkipReason: accounting.pushSkipReason,
|
|
982
|
+
llmConfig: Object.hasOwn(opts, "llmConfig")
|
|
983
|
+
? (opts.llmConfig ?? null)
|
|
984
|
+
: (resolveConsolidateLlmConfig(config, opts.improveProfile) ?? null),
|
|
985
|
+
};
|
|
986
|
+
for (const op of allOps) {
|
|
987
|
+
if (op.op === "promote")
|
|
988
|
+
await emitPromotionProposal(op, promoteContext);
|
|
1280
989
|
}
|
|
1281
|
-
|
|
1282
|
-
const { merged, deleted, contradicted, mergeFloorViolations, mergedSecondaries, promoted } = await applyConsolidationPlan(config, stashDir, opts.writeTarget, sourceRun, memories, warnings, allOps, accounting, opts.improveProfile, Object.hasOwn(opts, "llmConfig")
|
|
1283
|
-
? (opts.llmConfig ?? null)
|
|
1284
|
-
: (resolveConsolidateLlmConfig(config, opts.improveProfile) ?? null));
|
|
1285
|
-
const runDurationMs = Date.now() - startMs;
|
|
1286
|
-
const budgetFraction = opts.runBudgetMs !== undefined && opts.runBudgetMs > 0 ? runDurationMs / opts.runBudgetMs : undefined;
|
|
1287
|
-
return {
|
|
1288
|
-
schemaVersion: 1,
|
|
1289
|
-
ok: true,
|
|
1290
|
-
shape: "consolidate-result",
|
|
1291
|
-
dryRun: false,
|
|
1292
|
-
previewOnly: false,
|
|
990
|
+
return makeConsolidateResult({
|
|
1293
991
|
target: sourceName,
|
|
1294
|
-
processed:
|
|
1295
|
-
merged,
|
|
1296
|
-
deleted,
|
|
1297
|
-
promoted,
|
|
1298
|
-
contradicted,
|
|
1299
|
-
mergeFloorViolations,
|
|
992
|
+
processed: llmPoolSize,
|
|
1300
993
|
failedChunks: accounting.totalChunksFailed,
|
|
1301
994
|
totalChunks,
|
|
1302
995
|
judgedNoAction: accounting.judgedNoAction,
|
|
1303
996
|
skipReasons: accounting.skipReasons,
|
|
1304
|
-
mergedSecondaries,
|
|
997
|
+
mergedSecondaries: 0,
|
|
1305
998
|
failedChunkMemories: accounting.failedChunkMemories,
|
|
999
|
+
deferredMemories,
|
|
1000
|
+
promoted,
|
|
1001
|
+
failedPromotions: promotionFailures.count,
|
|
1002
|
+
planned: allOps,
|
|
1306
1003
|
warnings,
|
|
1307
|
-
durationMs:
|
|
1004
|
+
durationMs: Date.now() - startMs,
|
|
1308
1005
|
perfTelemetry: {
|
|
1309
1006
|
dedupPoolSize,
|
|
1310
1007
|
llmPoolSize,
|
|
1311
1008
|
embedMs: embedTelemetry.embedMs,
|
|
1312
1009
|
embedCacheHits: embedTelemetry.cacheHits,
|
|
1313
1010
|
embedCacheMisses: embedTelemetry.cacheMisses,
|
|
1314
|
-
...(budgetFraction !== undefined ? { estimatedBudgetFractionUsed: budgetFraction } : {}),
|
|
1315
1011
|
},
|
|
1316
|
-
};
|
|
1317
|
-
}
|
|
1318
|
-
/** Execute one `merge` op (behavior-identical to the former inlined branch). */
|
|
1319
|
-
/**
|
|
1320
|
-
* Finalize a merge whose content has already been generated and validated:
|
|
1321
|
-
* provenance/anti-collapse guards (generation guard, lexical-diversity + info-floor
|
|
1322
|
-
* advisories), generation frontmatter injection, then backup/write/archive/journal
|
|
1323
|
-
* and the 2026-05-26 accounting counters. Extracted verbatim from `handleMergeOp`
|
|
1324
|
-
* — logic and every counter increment are byte-identical (the
|
|
1325
|
-
* `processed == actioned + noAction + Σskips` invariant is preserved). All
|
|
1326
|
-
* refusal paths emit a skip reason and return.
|
|
1327
|
-
*/
|
|
1328
|
-
async function finalizeMerge(op, opIndex, ctx, mergedContentArg, emitMergeFailureSkips) {
|
|
1329
|
-
const { stashDir, target, backupDir, memoryByRef, warnings, counts } = ctx;
|
|
1330
|
-
let mergedContent = mergedContentArg;
|
|
1331
|
-
const allParticipants = [op.primary, ...op.secondaries];
|
|
1332
|
-
// Generation and provenance are mandatory merge metadata, independent of
|
|
1333
|
-
// whether the optional anti-collapse refusal/advisory checks are enabled.
|
|
1334
|
-
const participantInfo = allParticipants.map((ref) => {
|
|
1335
|
-
const e = memoryByRef.get(ref);
|
|
1336
|
-
if (!e)
|
|
1337
|
-
return { ref, generation: 0, body: "", sourceRefs: [] };
|
|
1338
|
-
try {
|
|
1339
|
-
const raw = fs.readFileSync(e.filePath, "utf8");
|
|
1340
|
-
const parsed = parseFrontmatter(raw);
|
|
1341
|
-
const fm = parsed.data;
|
|
1342
|
-
const sourceRefs = [
|
|
1343
|
-
...(Array.isArray(fm.xrefs) ? fm.xrefs.map(String) : []),
|
|
1344
|
-
...(Array.isArray(fm.source_refs) ? fm.source_refs.map(String) : []),
|
|
1345
|
-
];
|
|
1346
|
-
return { ref, generation: readAssetGeneration(fm), body: stripFrontmatterBody(raw), sourceRefs };
|
|
1347
|
-
}
|
|
1348
|
-
catch {
|
|
1349
|
-
return { ref, generation: 0, body: "", sourceRefs: [] };
|
|
1350
|
-
}
|
|
1351
|
-
});
|
|
1352
|
-
const sourceGenerations = participantInfo.map((p) => p.generation);
|
|
1353
|
-
// WS-3b: Anti-collapse generation guard (step 8a).
|
|
1354
|
-
// DEFAULT ON since R5 (opt out via antiCollapse.enabled: false). Refuses
|
|
1355
|
-
// to merge two assets both above generation N (default 2) — prevents the
|
|
1356
|
-
// pipeline from building ever-deeper LLM-merged trees that lose the
|
|
1357
|
-
// source fidelity of the original episodes.
|
|
1358
|
-
const antiCollapseConfig = getImproveProcessConfig("consolidate", ctx.improveProfile)?.antiCollapse ?? {};
|
|
1359
|
-
if (antiCollapseConfig.enabled !== false) {
|
|
1360
|
-
const generationCheck = checkGenerationGuard(sourceGenerations, antiCollapseConfig);
|
|
1361
|
-
if (generationCheck.refused) {
|
|
1362
|
-
warnings.push(`Merge: ${generationCheck.reason}`);
|
|
1363
|
-
emitMergeFailureSkips("merge_generation_guard");
|
|
1364
|
-
return;
|
|
1365
|
-
}
|
|
1366
|
-
// WS-3b: Lexical diversity check (step 8b).
|
|
1367
|
-
// Low n-gram diversity ⇒ likely correlated-extraction artifact; raise merge threshold.
|
|
1368
|
-
if (antiCollapseConfig.lexicalDiversityCheck !== false) {
|
|
1369
|
-
const bodies = participantInfo.map((p) => p.body).filter((b) => b.length > 0);
|
|
1370
|
-
const diversityCheck = checkLexicalDiversity(bodies, antiCollapseConfig);
|
|
1371
|
-
if (diversityCheck.lowDiversity) {
|
|
1372
|
-
// Low-diversity cluster: just warn (don't refuse merge since the dedup
|
|
1373
|
-
// path handles exact twins). The warning surfaces in health telemetry.
|
|
1374
|
-
warnings.push(`Merge: cluster around ${op.primary} has low lexical diversity (${diversityCheck.diversity?.toFixed(2) ?? "?"} < 0.30) — likely correlated extraction; merge proceeds but review is recommended.`);
|
|
1375
|
-
}
|
|
1376
|
-
}
|
|
1377
|
-
}
|
|
1378
|
-
// merged.generation = max(sourceGenerations) + 1. xrefs is the UNION of
|
|
1379
|
-
// participants + canonical and legacy provenance already carried by them.
|
|
1380
|
-
const provenanceUnion = [...new Set([...allParticipants, ...participantInfo.flatMap((p) => p.sourceRefs)])];
|
|
1381
|
-
mergedContent = injectGenerationFrontmatter(mergedContent, sourceGenerations, provenanceUnion);
|
|
1382
|
-
if (antiCollapseConfig.enabled !== false) {
|
|
1383
|
-
// R5 §4.2: merge-information floor — ADVISORY in v1. A merge that
|
|
1384
|
-
// shrinks provenance or genericizes below the retention floor is
|
|
1385
|
-
// counted + warned, never refused (promotion path: design doc §7).
|
|
1386
|
-
try {
|
|
1387
|
-
const mergedParsed = parseFrontmatter(mergedContent);
|
|
1388
|
-
const mergedFm = mergedParsed.data;
|
|
1389
|
-
const mergedSourceRefs = Array.isArray(mergedFm.xrefs) ? mergedFm.xrefs.map(String) : [];
|
|
1390
|
-
const floorCheck = checkMergeInformationFloor(mergedParsed.content, mergedSourceRefs, participantInfo, antiCollapseConfig);
|
|
1391
|
-
if (!floorCheck.passed) {
|
|
1392
|
-
counts.mergeFloorViolations++;
|
|
1393
|
-
warnings.push(`Merge: information floor advisory for ${op.primary}: ${floorCheck.reason ?? "unspecified"} — merge proceeds (v1 observe-only).`);
|
|
1394
|
-
}
|
|
1395
|
-
}
|
|
1396
|
-
catch {
|
|
1397
|
-
// Floor measurement is best-effort; never blocks the merge path.
|
|
1398
|
-
}
|
|
1399
|
-
}
|
|
1400
|
-
// Backup secondaries before deleting
|
|
1401
|
-
for (const secRef of op.secondaries) {
|
|
1402
|
-
const secEntry = memoryByRef.get(secRef);
|
|
1403
|
-
if (secEntry && fs.existsSync(secEntry.filePath)) {
|
|
1404
|
-
backupFile(secEntry.filePath, backupDir, secEntry.name);
|
|
1405
|
-
}
|
|
1406
|
-
}
|
|
1407
|
-
// Write merged primary
|
|
1408
|
-
try {
|
|
1409
|
-
const parsedPrimary = parseRefInput(op.primary);
|
|
1410
|
-
await writeAssetToSource(target.source, target.config, parsedPrimary, mergedContent);
|
|
1411
|
-
}
|
|
1412
|
-
catch (e) {
|
|
1413
|
-
warnings.push(`Merge: write failed for ${op.primary}: ${String(e)}`);
|
|
1414
|
-
emitMergeFailureSkips("merge_write_failed");
|
|
1415
|
-
return;
|
|
1416
|
-
}
|
|
1417
|
-
// Archive and delete secondaries (P1-B: soft-invalidation)
|
|
1418
|
-
for (const secRef of op.secondaries) {
|
|
1419
|
-
const secEntry = memoryByRef.get(secRef);
|
|
1420
|
-
if (!secEntry)
|
|
1421
|
-
continue;
|
|
1422
|
-
if (fs.existsSync(secEntry.filePath)) {
|
|
1423
|
-
archiveMemory(secEntry.filePath, stashDir, secRef, "merged into primary", opIndex, op.primary, warnings);
|
|
1424
|
-
}
|
|
1425
|
-
try {
|
|
1426
|
-
const parsedSec = parseRefInput(secRef);
|
|
1427
|
-
await deleteAssetFromSource(target.source, target.config, parsedSec);
|
|
1428
|
-
markJournalCompleted(ctx.txn, secRef);
|
|
1429
|
-
}
|
|
1430
|
-
catch (e) {
|
|
1431
|
-
warnings.push(`Merge: delete failed for ${secRef}: ${String(e)}`);
|
|
1432
|
-
}
|
|
1433
|
-
}
|
|
1434
|
-
markJournalCompleted(ctx.txn, op.primary);
|
|
1435
|
-
counts.merged++;
|
|
1436
|
-
// 2026-05-26 accounting-leak fix: `merged` is op-level, but each
|
|
1437
|
-
// successful merge actions `1 + secondaries.length` memories. Without
|
|
1438
|
-
// this counter the accounting invariant breaks by `secondaries.length`
|
|
1439
|
-
// per successful merge (chunk loop excluded all secondaries from
|
|
1440
|
-
// judgedNoAction via targetRefs, but only the primary is credited to
|
|
1441
|
-
// `merged`). Count only loaded-memory secondaries; phantom secondary
|
|
1442
|
-
// refs never affected any chunk's targetRefs in the first place.
|
|
1443
|
-
for (const secRef of op.secondaries) {
|
|
1444
|
-
if (memoryByRef.has(secRef))
|
|
1445
|
-
counts.mergedSecondaries++;
|
|
1446
|
-
}
|
|
1447
|
-
// Prune consumed refs from memoryByRef so later ops in this run cannot
|
|
1448
|
-
// reference an absorbed secondary as a merge primary and proceed with a
|
|
1449
|
-
// stale entry. Primary is rewritten (not deleted), so we only remove
|
|
1450
|
-
// secondaries; the primary ref remains valid under its new content.
|
|
1451
|
-
for (const secRef of op.secondaries) {
|
|
1452
|
-
memoryByRef.delete(secRef);
|
|
1453
|
-
}
|
|
1454
|
-
}
|
|
1455
|
-
export async function handleMergeOp(op, opIndex, ctx) {
|
|
1456
|
-
const { config, memoryByRef, warnings, pushSkipReason } = ctx;
|
|
1457
|
-
// Accounting helper: emit a per-participant skipReason for failed
|
|
1458
|
-
// merges so primary + every loaded-memory secondary land in the
|
|
1459
|
-
// structured skip histogram. Pre-2026-05-26 only the primary was
|
|
1460
|
-
// counted (1 skipReason per failed merge), leaving N secondaries
|
|
1461
|
-
// unaccounted for in the `processed == actioned + noAction + Σskips`
|
|
1462
|
-
// invariant — the source of the 4–11 silent leaks per run.
|
|
1463
|
-
const emitMergeFailureSkips = (reason) => {
|
|
1464
|
-
if (memoryByRef.has(op.primary))
|
|
1465
|
-
pushSkipReason("merge", op.primary, reason);
|
|
1466
|
-
for (const secRef of op.secondaries) {
|
|
1467
|
-
if (memoryByRef.has(secRef))
|
|
1468
|
-
pushSkipReason("merge", secRef, reason);
|
|
1469
|
-
}
|
|
1470
|
-
};
|
|
1471
|
-
const primaryEntry = memoryByRef.get(op.primary);
|
|
1472
|
-
if (!primaryEntry) {
|
|
1473
|
-
// This fires when a prior op in the same run consumed this ref as a
|
|
1474
|
-
// secondary and Fix-A pruned it from memoryByRef. It should NOT fire
|
|
1475
|
-
// for hallucinated primaries (those are dropped by mergePlans() before
|
|
1476
|
-
// reaching here). If this counter is non-zero, suspect an intra-run
|
|
1477
|
-
// cross-chunk race, not a filter regression.
|
|
1478
|
-
warnings.push(`Merge: primary ${op.primary} not found in loaded memories (pruned by prior op this run) — skipping.`);
|
|
1479
|
-
emitMergeFailureSkips("merge_primary_missing");
|
|
1480
|
-
return;
|
|
1481
|
-
}
|
|
1482
|
-
// Defense-in-depth: even if the entry is in memoryByRef (pre-flight ran
|
|
1483
|
-
// before this run's own ops), the file may have been deleted by a
|
|
1484
|
-
// concurrent process or an edge case the pre-flight filter missed.
|
|
1485
|
-
if (!fs.existsSync(primaryEntry.filePath)) {
|
|
1486
|
-
warnings.push(`Merge: primary ${op.primary} file gone at execution time (stale entry) — skipping.`);
|
|
1487
|
-
emitMergeFailureSkips("merge_primary_file_gone");
|
|
1488
|
-
return;
|
|
1489
|
-
}
|
|
1490
|
-
// Phase B: generate merged content
|
|
1491
|
-
const secondaryBodies = [];
|
|
1492
|
-
for (const secRef of op.secondaries) {
|
|
1493
|
-
const secEntry = memoryByRef.get(secRef);
|
|
1494
|
-
if (!secEntry) {
|
|
1495
|
-
warnings.push(`Merge: secondary ${secRef} not found — skipping merge op.`);
|
|
1496
|
-
// No accounting impact: a missing secondary is a phantom ref and
|
|
1497
|
-
// never contributed to any chunk's targetRefs reduction. We still
|
|
1498
|
-
// continue the loop to gather the remaining valid secondaries.
|
|
1499
|
-
continue;
|
|
1500
|
-
}
|
|
1501
|
-
secondaryBodies.push(secRef);
|
|
1502
|
-
}
|
|
1503
|
-
if (secondaryBodies.length === 0) {
|
|
1504
|
-
warnings.push(`Merge: ${op.primary} has no valid secondaries — skipping.`);
|
|
1505
|
-
emitMergeFailureSkips("merge_no_valid_secondaries");
|
|
1506
|
-
return;
|
|
1507
|
-
}
|
|
1508
|
-
// Pre-flight hot guard — skip the LLM call entirely if any participant
|
|
1509
|
-
// is hot or unparseable. Without this, mixed chunks still send hot merges
|
|
1510
|
-
// to the planner which proposes them; generateMergedContent() is then
|
|
1511
|
-
// called, produces output without `description`, and the skip is
|
|
1512
|
-
// misattributed to merge_missing_description instead of the real cause.
|
|
1513
|
-
const preflightParticipants = [op.primary, ...op.secondaries];
|
|
1514
|
-
const preflightBlocked = preflightParticipants.flatMap((ref) => {
|
|
1515
|
-
const e = memoryByRef.get(ref);
|
|
1516
|
-
if (!e)
|
|
1517
|
-
return [];
|
|
1518
|
-
const verdict = consolidateGuardStatus(e.filePath);
|
|
1519
|
-
if (verdict === "hot" || verdict === "unparseable")
|
|
1520
|
-
return [{ ref, verdict }];
|
|
1521
|
-
return [];
|
|
1522
|
-
});
|
|
1523
|
-
if (preflightBlocked.length > 0) {
|
|
1524
|
-
const detail = preflightBlocked.map((p) => `${p.ref} (${p.verdict})`).join(", ");
|
|
1525
|
-
warnings.push(`Merge: refused for ${op.primary} — ${preflightBlocked.length} participant(s) blocked by hot/unparseable frontmatter guard (pre-flight): ${detail}`);
|
|
1526
|
-
emitMergeFailureSkips("merge_participant_blocked");
|
|
1527
|
-
return;
|
|
1528
|
-
}
|
|
1529
|
-
let primaryBody = "";
|
|
1530
|
-
try {
|
|
1531
|
-
primaryBody = fs.readFileSync(primaryEntry.filePath, "utf8");
|
|
1532
|
-
}
|
|
1533
|
-
catch {
|
|
1534
|
-
warnings.push(`Merge: could not read primary ${op.primary} — skipping.`);
|
|
1535
|
-
emitMergeFailureSkips("merge_read_failed");
|
|
1536
|
-
return;
|
|
1537
|
-
}
|
|
1538
|
-
const mergeResult = await (ctx.generateMergedContentFn ?? generateMergedContent)(config, op.primary, primaryBody, op.secondaries, memoryByRef, ctx.improveProfile, ctx.llmConfig);
|
|
1539
|
-
if ("error" in mergeResult) {
|
|
1540
|
-
warnings.push(`Merge: ${mergeResult.error} for ${mergeResult.detail}.`);
|
|
1541
|
-
emitMergeFailureSkips(mergeResult.error);
|
|
1542
|
-
return;
|
|
1543
|
-
}
|
|
1544
|
-
const mergedContent = mergeResult.content;
|
|
1545
|
-
// Validate frontmatter of merged content — must have a `---` block
|
|
1546
|
-
// with at minimum a `description` field. We parse via the hand-rolled
|
|
1547
|
-
// parser (cheap) AND require non-empty description. This guards against
|
|
1548
|
-
// the historical defect where merged memories were written back with
|
|
1549
|
-
// empty `description` and later polluted the promote path.
|
|
1550
|
-
let parsedMerged;
|
|
1551
|
-
try {
|
|
1552
|
-
parsedMerged = parseFrontmatter(mergedContent);
|
|
1553
|
-
}
|
|
1554
|
-
catch {
|
|
1555
|
-
warnings.push(`Merge: merged content for ${op.primary} has invalid frontmatter — skipping.`);
|
|
1556
|
-
emitMergeFailureSkips("merge_invalid_frontmatter");
|
|
1557
|
-
return;
|
|
1558
|
-
}
|
|
1559
|
-
if (parsedMerged.frontmatter === null) {
|
|
1560
|
-
warnings.push(`Merge: merged content for ${op.primary} has no frontmatter block — skipping.`);
|
|
1561
|
-
emitMergeFailureSkips("merge_invalid_frontmatter");
|
|
1562
|
-
return;
|
|
1563
|
-
}
|
|
1564
|
-
const mergedDesc = parsedMerged.data.description;
|
|
1565
|
-
if (typeof mergedDesc !== "string" || mergedDesc.trim().length === 0) {
|
|
1566
|
-
warnings.push(`Merge: merged content for ${op.primary} missing description — skipping.`);
|
|
1567
|
-
emitMergeFailureSkips("merge_missing_description");
|
|
1568
|
-
return;
|
|
1569
|
-
}
|
|
1570
|
-
const truncReason = detectTruncatedDescription(mergedDesc);
|
|
1571
|
-
if (truncReason) {
|
|
1572
|
-
warnings.push(`Merge: merged content for ${op.primary} has truncated description (${truncReason}) — skipping.`);
|
|
1573
|
-
emitMergeFailureSkips("merge_truncated_description");
|
|
1574
|
-
return;
|
|
1575
|
-
}
|
|
1576
|
-
// captureMode:hot guard — refuse the merge if ANY participating memory
|
|
1577
|
-
// (primary or secondary) was user-captured or has unparseable frontmatter
|
|
1578
|
-
// (could have hidden a hot flag). Hot memories are user-explicit and
|
|
1579
|
-
// must not be deleted/overwritten by the consolidate LLM. 14 user
|
|
1580
|
-
// memories were silent-deleted by consolidate before this guard landed;
|
|
1581
|
-
// recovery required copying from .akm/archive/ by hand.
|
|
1582
|
-
const mergeParticipants = [op.primary, ...op.secondaries];
|
|
1583
|
-
const blockedParticipants = mergeParticipants.flatMap((ref) => {
|
|
1584
|
-
const e = memoryByRef.get(ref);
|
|
1585
|
-
if (!e)
|
|
1586
|
-
return [];
|
|
1587
|
-
const verdict = consolidateGuardStatus(e.filePath);
|
|
1588
|
-
if (verdict === "hot" || verdict === "unparseable")
|
|
1589
|
-
return [{ ref, verdict }];
|
|
1590
|
-
return [];
|
|
1591
1012
|
});
|
|
1592
|
-
if (blockedParticipants.length > 0) {
|
|
1593
|
-
const detail = blockedParticipants.map((p) => `${p.ref} (${p.verdict})`).join(", ");
|
|
1594
|
-
warnings.push(`Merge: refused for ${op.primary} — ${blockedParticipants.length} participant(s) blocked by hot/unparseable frontmatter guard: ${detail}`);
|
|
1595
|
-
emitMergeFailureSkips("merge_participant_blocked");
|
|
1596
|
-
return;
|
|
1597
|
-
}
|
|
1598
|
-
await finalizeMerge(op, opIndex, ctx, mergedContent, emitMergeFailureSkips);
|
|
1599
|
-
}
|
|
1600
|
-
/** Execute one `delete` op (behavior-identical to the former inlined branch). */
|
|
1601
|
-
export async function handleDeleteOp(op, opIndex, ctx) {
|
|
1602
|
-
const { stashDir, target, backupDir, memoryByRef, warnings, pushSkipReason, counts } = ctx;
|
|
1603
|
-
const entry = memoryByRef.get(op.ref);
|
|
1604
|
-
if (!entry) {
|
|
1605
|
-
warnings.push(`Delete: ${op.ref} not found in loaded memories — skipping.`);
|
|
1606
|
-
// Phantom ref: not in the batch so not in processed. Pushing to
|
|
1607
|
-
// skipReasons would inflate Σ(skipReasons) without a matching processed
|
|
1608
|
-
// entry, breaking the accounting invariant. Visibility is preserved via
|
|
1609
|
-
// the warnings array above.
|
|
1610
|
-
return;
|
|
1611
|
-
}
|
|
1612
|
-
// captureMode:hot guard — refuse to delete user-captured memories OR
|
|
1613
|
-
// memories whose frontmatter is unparseable (could have hidden the hot
|
|
1614
|
-
// flag). The consolidate LLM was deleting hot-captured user memos as
|
|
1615
|
-
// "redundant" — 14 such deletes were silently archived between
|
|
1616
|
-
// 2026-05-19 and 2026-05-20 before this guard. Hot memories are
|
|
1617
|
-
// user-explicit and may only be deleted by the user.
|
|
1618
|
-
const guard = consolidateGuardStatus(entry.filePath);
|
|
1619
|
-
if (guard === "hot" || guard === "unparseable") {
|
|
1620
|
-
warnings.push(`Delete: refused for ${op.ref} — ${guard === "hot" ? "captureMode:hot (user-explicit; never auto-delete)" : "frontmatter unparseable (cannot verify hot flag absent)"}. Reason from LLM: "${op.reason ?? "n/a"}"`);
|
|
1621
|
-
pushSkipReason("delete", op.ref, "captureMode_hot_refused");
|
|
1622
|
-
return;
|
|
1623
|
-
}
|
|
1624
|
-
if (fs.existsSync(entry.filePath)) {
|
|
1625
|
-
backupFile(entry.filePath, backupDir, entry.name);
|
|
1626
|
-
// P1-B: soft-invalidation archive before hard delete
|
|
1627
|
-
archiveMemory(entry.filePath, stashDir, op.ref, op.reason, opIndex, undefined, warnings);
|
|
1628
|
-
}
|
|
1629
|
-
try {
|
|
1630
|
-
const parsedRef = parseRefInput(op.ref);
|
|
1631
|
-
await deleteAssetFromSource(target.source, target.config, parsedRef);
|
|
1632
|
-
markJournalCompleted(ctx.txn, op.ref);
|
|
1633
|
-
counts.deleted++;
|
|
1634
|
-
// Prune from memoryByRef so later ops in this run cannot reference a
|
|
1635
|
-
// deleted memory as a merge primary or secondary.
|
|
1636
|
-
memoryByRef.delete(op.ref);
|
|
1637
|
-
}
|
|
1638
|
-
catch (e) {
|
|
1639
|
-
// Distinguish "file already absent" from genuine failures. A prior run
|
|
1640
|
-
// may have deleted the file but the DB was not yet re-indexed, so the
|
|
1641
|
-
// ref still appeared in memoryByRef. The delete goal is already met.
|
|
1642
|
-
const msg = e instanceof Error ? e.message : String(e);
|
|
1643
|
-
if (msg.includes("not found in source")) {
|
|
1644
|
-
warnings.push(`Delete: ${op.ref} — file already absent (stale DB entry); skipping.`);
|
|
1645
|
-
pushSkipReason("delete", op.ref, "delete_already_gone");
|
|
1646
|
-
}
|
|
1647
|
-
else {
|
|
1648
|
-
warnings.push(`Delete: failed for ${op.ref}: ${String(e)}`);
|
|
1649
|
-
pushSkipReason("delete", op.ref, "delete_failed");
|
|
1650
|
-
}
|
|
1651
|
-
}
|
|
1652
1013
|
}
|
|
1653
|
-
/** Execute one
|
|
1654
|
-
|
|
1014
|
+
/** Execute one reconciled promotion by emitting a reviewable proposal. */
|
|
1015
|
+
async function emitPromotionProposal(op, ctx) {
|
|
1655
1016
|
const { config, stashDir, sourceRun, target, memoryByRef, warnings, pushSkipReason, promoted, promotedSourceRefs } = ctx;
|
|
1656
1017
|
const entry = memoryByRef.get(op.ref);
|
|
1657
1018
|
if (!entry) {
|
|
@@ -1669,11 +1030,7 @@ export async function handlePromoteOp(op, ctx) {
|
|
|
1669
1030
|
pushSkipReason("promote", op.ref, "promote_already_promoted_this_run");
|
|
1670
1031
|
return;
|
|
1671
1032
|
}
|
|
1672
|
-
const proposedName = op.knowledgeRef
|
|
1673
|
-
.replace(/^knowledge:/, "")
|
|
1674
|
-
.split("/")
|
|
1675
|
-
.filter(Boolean)
|
|
1676
|
-
.at(-1) ??
|
|
1033
|
+
const proposedName = op.knowledgeRef.split("/").filter(Boolean).at(-1) ??
|
|
1677
1034
|
entry.name.split("/").filter(Boolean).at(-1) ??
|
|
1678
1035
|
"promoted-memory";
|
|
1679
1036
|
const slug = proposedName
|
|
@@ -1686,9 +1043,8 @@ export async function handlePromoteOp(op, ctx) {
|
|
|
1686
1043
|
if (knowledgeRef !== op.knowledgeRef) {
|
|
1687
1044
|
warnings.push(`Normalized generated ref "${op.knowledgeRef}" → "${knowledgeRef}"`);
|
|
1688
1045
|
}
|
|
1689
|
-
//
|
|
1690
|
-
//
|
|
1691
|
-
// exact `{ ref: knowledge:slug }` filter would miss it).
|
|
1046
|
+
// A pending proposal may carry a qualified item_ref, so compare its parsed
|
|
1047
|
+
// conceptId rather than exact display spelling.
|
|
1692
1048
|
if (hasPendingProposalForConcept(stashDir, knowledgeRef)) {
|
|
1693
1049
|
warnings.push(`Skipping promote: pending proposal already exists for ${knowledgeRef}`);
|
|
1694
1050
|
pushSkipReason("promote", op.ref, "promote_pending_proposal_exists");
|
|
@@ -1711,9 +1067,7 @@ export async function handlePromoteOp(op, ctx) {
|
|
|
1711
1067
|
pushSkipReason("promote", op.ref, "promote_read_failed");
|
|
1712
1068
|
return;
|
|
1713
1069
|
}
|
|
1714
|
-
//
|
|
1715
|
-
// consolidate runs may still carry outer code fences or broken YAML.
|
|
1716
|
-
// Strip them here so we never propose a polluted asset.
|
|
1070
|
+
// Validate and normalize source content before proposing a promoted asset.
|
|
1717
1071
|
const promoteSanitized = sanitizeMergedContent(memoryContent);
|
|
1718
1072
|
if (!promoteSanitized.ok) {
|
|
1719
1073
|
warnings.push(`Promote: rejected ${op.ref} — source memory failed sanitization (${promoteSanitized.reason}).`);
|
|
@@ -1816,8 +1170,9 @@ export async function handlePromoteOp(op, ctx) {
|
|
|
1816
1170
|
pushSkipReason("promote", op.ref, "promote_dedup_window");
|
|
1817
1171
|
return;
|
|
1818
1172
|
}
|
|
1819
|
-
const proposalResult = emitProposal({ stashDir }, {
|
|
1173
|
+
const proposalResult = emitProposal({ stashDir, proposalsCtx: ctx.proposalsCtx }, {
|
|
1820
1174
|
ref: knowledgeRef,
|
|
1175
|
+
target: { source: target.source.name, root: target.source.path },
|
|
1821
1176
|
source: "consolidate",
|
|
1822
1177
|
sourceRun,
|
|
1823
1178
|
// §23.6 fingerprint model-id term (WI-6.4).
|
|
@@ -1835,56 +1190,14 @@ export async function handlePromoteOp(op, ctx) {
|
|
|
1835
1190
|
else {
|
|
1836
1191
|
promoted.push(proposalResult.id);
|
|
1837
1192
|
promotedSourceRefs.add(op.ref);
|
|
1838
|
-
markJournalCompleted(ctx.txn, op.ref);
|
|
1839
1193
|
}
|
|
1840
1194
|
}
|
|
1841
1195
|
catch (e) {
|
|
1196
|
+
ctx.promotionFailures.count++;
|
|
1842
1197
|
warnings.push(`Promote: createProposal failed for ${op.ref}: ${String(e)}`);
|
|
1843
1198
|
pushSkipReason("promote", op.ref, "promote_create_failed");
|
|
1844
1199
|
}
|
|
1845
1200
|
}
|
|
1846
|
-
/** Execute one `contradict` op (behavior-identical to the former inlined branch). */
|
|
1847
|
-
export async function handleContradictOp(op, ctx) {
|
|
1848
|
-
const { memoryByRef, warnings, pushSkipReason, counts } = ctx;
|
|
1849
|
-
// Confidence gate: surface-level topic overlap causes false positives
|
|
1850
|
-
// (investigation 2026-06-18). Require ≥0.92 confidence before writing
|
|
1851
|
-
// contradiction edges. Missing confidence field defaults to 1.0 for
|
|
1852
|
-
// backward compatibility with responses that predate this field.
|
|
1853
|
-
const opConfidence = typeof op.confidence === "number" ? op.confidence : 1.0;
|
|
1854
|
-
if (opConfidence < 0.92) {
|
|
1855
|
-
warnings.push(`Contradict: confidence ${opConfidence.toFixed(2)} below 0.92 threshold for ${op.ref} <-> ${op.contradictedByRef} — skipping.`);
|
|
1856
|
-
pushSkipReason("contradict", op.ref, "contradict_low_confidence");
|
|
1857
|
-
return;
|
|
1858
|
-
}
|
|
1859
|
-
// C-3 / #382: Write contradictedBy edges so resolveFamilyContradictions
|
|
1860
|
-
// (the SCC resolver in memory-improve.ts) has edges to work on.
|
|
1861
|
-
// Zep arXiv:2501.13956 §3 — unified belief-revision with contradiction edges.
|
|
1862
|
-
const entry = memoryByRef.get(op.ref);
|
|
1863
|
-
const contradictorEntry = memoryByRef.get(op.contradictedByRef);
|
|
1864
|
-
if (!entry) {
|
|
1865
|
-
warnings.push(`Contradict: ${op.ref} not found in loaded memories — skipping.`);
|
|
1866
|
-
// Phantom ref: not in processed, so no skipReason (same rationale as
|
|
1867
|
-
// delete_ref_missing).
|
|
1868
|
-
return;
|
|
1869
|
-
}
|
|
1870
|
-
if (!contradictorEntry) {
|
|
1871
|
-
warnings.push(`Contradict: ${op.contradictedByRef} not found — skipping.`);
|
|
1872
|
-
// op.ref IS in the batch (entry found above) so the skipReason is
|
|
1873
|
-
// correctly charged against a real processed memory.
|
|
1874
|
-
pushSkipReason("contradict", op.ref, "contradict_target_missing");
|
|
1875
|
-
return;
|
|
1876
|
-
}
|
|
1877
|
-
try {
|
|
1878
|
-
// Write the contradiction edge: op.ref is contradicted by op.contradictedByRef
|
|
1879
|
-
writeContradictEdge(entry.filePath, op.contradictedByRef);
|
|
1880
|
-
counts.contradicted++;
|
|
1881
|
-
markJournalCompleted(ctx.txn, op.ref);
|
|
1882
|
-
}
|
|
1883
|
-
catch (e) {
|
|
1884
|
-
warnings.push(`Contradict: failed to write edge for ${op.ref}: ${String(e)}`);
|
|
1885
|
-
pushSkipReason("contradict", op.ref, "contradict_write_failed");
|
|
1886
|
-
}
|
|
1887
|
-
}
|
|
1888
1201
|
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
1889
1202
|
/**
|
|
1890
1203
|
* Normalise a knowledge slug for variant-aware deduplication. Collapses:
|
|
@@ -1896,7 +1209,7 @@ export async function handleContradictOp(op, ctx) {
|
|
|
1896
1209
|
* Two slugs that normalise to the same string are considered the same asset
|
|
1897
1210
|
* for dedup purposes even if they don't share an exact ref.
|
|
1898
1211
|
*/
|
|
1899
|
-
/** The conceptId a proposal ref maps to
|
|
1212
|
+
/** The conceptId a proposal ref maps to, or undefined for an invalid ref. */
|
|
1900
1213
|
function conceptIdForRef(ref) {
|
|
1901
1214
|
try {
|
|
1902
1215
|
const p = parseRefInput(ref);
|
|
@@ -1906,23 +1219,13 @@ function conceptIdForRef(ref) {
|
|
|
1906
1219
|
return undefined;
|
|
1907
1220
|
}
|
|
1908
1221
|
}
|
|
1909
|
-
/** Is a pending proposal already queued for `conceptRef`'s concept
|
|
1222
|
+
/** Is a pending proposal already queued for `conceptRef`'s concept? */
|
|
1910
1223
|
function hasPendingProposalForConcept(stashDir, conceptRef) {
|
|
1911
1224
|
const want = conceptIdForRef(conceptRef);
|
|
1912
1225
|
return (want !== undefined && listProposals(stashDir, { status: "pending" }).some((p) => conceptIdForRef(p.ref) === want));
|
|
1913
1226
|
}
|
|
1914
1227
|
function normalizeSlugForDedup(ref) {
|
|
1915
|
-
|
|
1916
|
-
// item_ref `bundle//conceptId`, which carries no `type:` colon — the old
|
|
1917
|
-
// `replace(/^[^:]+:/,"")` strip would leave the bundle/type prefix in). The name
|
|
1918
|
-
// is identical across grammars, so the dedup slug stays stable across the flip.
|
|
1919
|
-
let slug;
|
|
1920
|
-
try {
|
|
1921
|
-
slug = parseRefInput(ref).name;
|
|
1922
|
-
}
|
|
1923
|
-
catch {
|
|
1924
|
-
slug = ref.replace(/^[^:]+:/, "");
|
|
1925
|
-
}
|
|
1228
|
+
const slug = parseRefInput(ref).name;
|
|
1926
1229
|
const monthRe = /(?:jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)/i;
|
|
1927
1230
|
const tokens = slug
|
|
1928
1231
|
.toLowerCase()
|
|
@@ -2091,166 +1394,3 @@ function loadMemoriesForSource(source, stashDir, warnings) {
|
|
|
2091
1394
|
}
|
|
2092
1395
|
return memories;
|
|
2093
1396
|
}
|
|
2094
|
-
async function generateMergedContent(config, primaryRef, primaryBody, secondaryRefs, memoryByRef, activeProfile, resolvedLlmConfig) {
|
|
2095
|
-
// Only handle single-secondary merges per design (one call per merge op)
|
|
2096
|
-
const secRef = secondaryRefs[0];
|
|
2097
|
-
const secEntry = secRef !== undefined ? memoryByRef.get(secRef) : undefined;
|
|
2098
|
-
if (!secEntry)
|
|
2099
|
-
return { error: "merge_read_failed", detail: `secondary ${secRef} not in memoryByRef` };
|
|
2100
|
-
let secBody = "";
|
|
2101
|
-
try {
|
|
2102
|
-
secBody = fs.readFileSync(secEntry.filePath, "utf8");
|
|
2103
|
-
}
|
|
2104
|
-
catch {
|
|
2105
|
-
return { error: "merge_read_failed", detail: `could not read secondary ${secRef}` };
|
|
2106
|
-
}
|
|
2107
|
-
const primaryFmKeys = Object.keys(parseFrontmatter(primaryBody).data);
|
|
2108
|
-
const secFmKeys = Object.keys(parseFrontmatter(secBody).data);
|
|
2109
|
-
const requiredFmKeys = [...new Set([...primaryFmKeys, ...secFmKeys])];
|
|
2110
|
-
const prompt = [
|
|
2111
|
-
"Merge these two memory assets into one. Output ONLY the merged markdown (with YAML frontmatter). Do not explain, do not use code fences.",
|
|
2112
|
-
"",
|
|
2113
|
-
"## OUTPUT FORMAT (MANDATORY)",
|
|
2114
|
-
"Return raw markdown content beginning DIRECTLY with the `---` frontmatter delimiter.",
|
|
2115
|
-
"DO NOT wrap your entire response in a code fence.",
|
|
2116
|
-
"",
|
|
2117
|
-
'GOOD: "---\\ndescription: ...\\n---\\nBody content."',
|
|
2118
|
-
'BAD: "```markdown\\n---\\ndescription: ...\\n---\\nBody content.\\n```"',
|
|
2119
|
-
'BAD: "```yaml\\n---\\ndescription: ...\\n---\\nBody content.\\n```"',
|
|
2120
|
-
"",
|
|
2121
|
-
"## FRONTMATTER RULES (MANDATORY)",
|
|
2122
|
-
"- The `updated:` field, if present, MUST be a real ISO date (e.g. `updated: 2026-05-20`). NEVER emit `updated: today`, `updated: now`, or `updated: {today: null}`. If you don't have a real date, OMIT the field — the post-processor will not invent one.",
|
|
2123
|
-
"- REQUIRED: The merged frontmatter MUST include a `description` field with a concise one-sentence summary of the merged asset's content. If neither source has a `description` field, synthesize one from the content.",
|
|
2124
|
-
requiredFmKeys.length > 0
|
|
2125
|
-
? `- CRITICAL: The merged frontmatter MUST include ALL of these keys from both source memories: ${requiredFmKeys.join(", ")}. Do NOT drop any of them.`
|
|
2126
|
-
: null,
|
|
2127
|
-
"",
|
|
2128
|
-
`=== Primary memory (${primaryRef}) ===`,
|
|
2129
|
-
primaryBody,
|
|
2130
|
-
"",
|
|
2131
|
-
`=== Secondary memory (${secRef}) ===`,
|
|
2132
|
-
secBody,
|
|
2133
|
-
]
|
|
2134
|
-
.filter((line) => line !== null)
|
|
2135
|
-
.join("\n");
|
|
2136
|
-
// Use the same per-process profile resolution as the chunk-plan call above
|
|
2137
|
-
// so the merge generation step doesn't silently revert to the default LLM.
|
|
2138
|
-
const llmConfig = resolvedLlmConfig === null ? undefined : (resolvedLlmConfig ?? resolveConsolidateLlmConfig(config, activeProfile));
|
|
2139
|
-
// Same envelope semantics as the chunk-plan call above: the gate runs with
|
|
2140
|
-
// enabled:true, the missing-LLM guard returns the envelope the gated fn
|
|
2141
|
-
// used to return, a transport throw becomes an {ok:false} envelope via
|
|
2142
|
-
// onError, and the fallback fires only on wrapper timeout.
|
|
2143
|
-
const result = !llmConfig
|
|
2144
|
-
? { ok: false, error: "No LLM configured for consolidation" }
|
|
2145
|
-
: await callStructured({
|
|
2146
|
-
feature: "memory_consolidation",
|
|
2147
|
-
akmConfig: config,
|
|
2148
|
-
enabled: true,
|
|
2149
|
-
config: llmConfig,
|
|
2150
|
-
messages: [{ role: "user", content: prompt }],
|
|
2151
|
-
request: { enableThinking: false },
|
|
2152
|
-
parse: (raw) => ({ ok: true, content: raw ?? "" }),
|
|
2153
|
-
onError: (_cls, e) => ({ ok: false, error: String(e) }),
|
|
2154
|
-
fallback: { ok: false, error: `merge content generation failed for ${primaryRef}` },
|
|
2155
|
-
});
|
|
2156
|
-
if (!result.ok) {
|
|
2157
|
-
return {
|
|
2158
|
-
error: "merge_transport_failed",
|
|
2159
|
-
detail: result.error ?? `merge content generation failed for ${primaryRef}`,
|
|
2160
|
-
};
|
|
2161
|
-
}
|
|
2162
|
-
// Sanitize LLM output: strip outer code fences (defends against the
|
|
2163
|
-
// ```markdown … ``` leak observed in production), re-serialise frontmatter
|
|
2164
|
-
// through the yaml lib (fixes quote-escaping mistakes), and reject empty
|
|
2165
|
-
// or fence-only responses.
|
|
2166
|
-
const sanitized = sanitizeMergedContent(result.content ?? "");
|
|
2167
|
-
if (!sanitized.ok) {
|
|
2168
|
-
const reason = sanitized.reason;
|
|
2169
|
-
const isFenceError = reason === "UNBALANCED_CODE_FENCE" ||
|
|
2170
|
-
reason === "MISSING_FRONTMATTER_SENTINEL" ||
|
|
2171
|
-
reason === "MALFORMED_FRONTMATTER_BLOCK" ||
|
|
2172
|
-
reason === "FRONTMATTER_NOT_OBJECT";
|
|
2173
|
-
const mergeReason = isFenceError ? "merge_fence_rejected" : "merge_yaml_invalid";
|
|
2174
|
-
return { error: mergeReason, detail: `${primaryRef} — ${reason}` };
|
|
2175
|
-
}
|
|
2176
|
-
const mergedRaw = sanitized.result.content;
|
|
2177
|
-
// C-4 / #383: Content-preservation lint (mem0 §3.2, arXiv:2504.19413).
|
|
2178
|
-
// Guards against LLM-generated merged content that silently drops information
|
|
2179
|
-
// from the source assets. Two checks:
|
|
2180
|
-
// 1. Body size: merged body must be >= 50% of the larger source body.
|
|
2181
|
-
// 2. Frontmatter superset: merged frontmatter must contain all keys present
|
|
2182
|
-
// in both source frontmatters.
|
|
2183
|
-
// Failures return a discriminated error so the call site can emit a specific
|
|
2184
|
-
// skip-reason key in the histogram.
|
|
2185
|
-
try {
|
|
2186
|
-
const primaryFm = parseFrontmatter(primaryBody);
|
|
2187
|
-
const secFm = parseFrontmatter(secBody);
|
|
2188
|
-
const mergedFm = parseFrontmatter(mergedRaw);
|
|
2189
|
-
// Check body size — blended floor: max(ratio × largerLen, absoluteFloor).
|
|
2190
|
-
// Deduplication is expected, so the ratio is lower than the reflect gate
|
|
2191
|
-
// (0.3 vs 0.5). The absolute floor protects very short memory pairs where
|
|
2192
|
-
// the ratio alone would produce a near-zero threshold.
|
|
2193
|
-
const primaryBodyLen = (primaryFm.content ?? "").trim().length;
|
|
2194
|
-
const secBodyLen = (secFm.content ?? "").trim().length;
|
|
2195
|
-
const mergedBodyLen = (mergedFm.content ?? "").trim().length;
|
|
2196
|
-
const largerBodyLen = Math.max(primaryBodyLen, secBodyLen);
|
|
2197
|
-
const mergeFloor = Math.max(MERGE_SHRINK_RATIO_MIN * largerBodyLen, MERGE_ABSOLUTE_FLOOR_CHARS);
|
|
2198
|
-
if (largerBodyLen > 0 && mergedBodyLen < mergeFloor) {
|
|
2199
|
-
return {
|
|
2200
|
-
error: "merge_content_too_short",
|
|
2201
|
-
detail: `${primaryRef} — merged body (${mergedBodyLen} chars) is less than floor (${Math.round(mergeFloor)} chars; max(${MERGE_SHRINK_RATIO_MIN}×${largerBodyLen}, ${MERGE_ABSOLUTE_FLOOR_CHARS}))`,
|
|
2202
|
-
};
|
|
2203
|
-
}
|
|
2204
|
-
// Check frontmatter superset — attempt repair before rejecting.
|
|
2205
|
-
const primaryKeys = Object.keys(primaryFm.data ?? {});
|
|
2206
|
-
const secKeys = Object.keys(secFm.data ?? {});
|
|
2207
|
-
const mergedKeys = new Set(Object.keys(mergedFm.data ?? {}));
|
|
2208
|
-
const missingKeys = [...new Set([...primaryKeys, ...secKeys])].filter((k) => !mergedKeys.has(k));
|
|
2209
|
-
if (missingKeys.length > 0) {
|
|
2210
|
-
// Inject missing keys from source FMs. Primary value wins on conflict.
|
|
2211
|
-
const repairedFmData = { ...mergedFm.data };
|
|
2212
|
-
for (const key of missingKeys) {
|
|
2213
|
-
repairedFmData[key] =
|
|
2214
|
-
key in primaryFm.data
|
|
2215
|
-
? primaryFm.data[key]
|
|
2216
|
-
: secFm.data[key];
|
|
2217
|
-
}
|
|
2218
|
-
normalizeUpdatedField(repairedFmData);
|
|
2219
|
-
const repairedYaml = serializeFrontmatter(repairedFmData);
|
|
2220
|
-
const bodyPart = typeof mergedFm.content === "string" ? mergedFm.content : "";
|
|
2221
|
-
return { content: assembleAssetFromString(repairedYaml, bodyPart) };
|
|
2222
|
-
}
|
|
2223
|
-
}
|
|
2224
|
-
catch {
|
|
2225
|
-
// parseFrontmatter failures are non-fatal — allow the merge to proceed.
|
|
2226
|
-
}
|
|
2227
|
-
return { content: mergedRaw };
|
|
2228
|
-
}
|
|
2229
|
-
async function promptConfirm(message) {
|
|
2230
|
-
process.stdout.write(message);
|
|
2231
|
-
return new Promise((resolve) => {
|
|
2232
|
-
let settled = false;
|
|
2233
|
-
const done = (answer) => {
|
|
2234
|
-
if (settled)
|
|
2235
|
-
return;
|
|
2236
|
-
settled = true;
|
|
2237
|
-
rl.close();
|
|
2238
|
-
resolve(answer);
|
|
2239
|
-
};
|
|
2240
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
2241
|
-
rl.once("line", (line) => done(line.trim().toLowerCase() === "y"));
|
|
2242
|
-
rl.once("close", () => done(false));
|
|
2243
|
-
});
|
|
2244
|
-
}
|
|
2245
|
-
// Checklist journals are never auto-recovered — recovery is a run-entry
|
|
2246
|
-
// decision (`--consolidate-recovery abort|clean`). Registered so any generic
|
|
2247
|
-
// engine recovery that reaches one aborts with the same guidance instead of
|
|
2248
|
-
// improvising a rollback.
|
|
2249
|
-
registerTxnKind(CONSOLIDATE_TXN_KIND, {
|
|
2250
|
-
phases: CONSOLIDATE_TXN_PHASES,
|
|
2251
|
-
commitPhase: "applying",
|
|
2252
|
-
rollback: () => { },
|
|
2253
|
-
finalize: (txn) => {
|
|
2254
|
-
throw new ConfigError(`Incomplete consolidation run detected at ${txn.journalPath}. Re-run with --consolidate-recovery clean to remove stale journal artifacts.`, "INVALID_CONFIG_FILE");
|
|
2255
|
-
},
|
|
2256
|
-
});
|