akm-cli 0.9.16 → 0.9.17-alpha.10
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 +2101 -0
- package/STABILITY.md +11 -10
- package/dist/akm +124 -193
- package/dist/akm-migrate +38 -19
- package/dist/assets/hints/cli-hints-full.md +6 -7
- package/dist/assets/improve-strategies/catchup.json +0 -3
- package/dist/assets/improve-strategies/consolidate.json +0 -1
- package/dist/assets/improve-strategies/default.json +1 -2
- package/dist/assets/improve-strategies/proactive-maintenance.json +1 -2
- package/dist/assets/improve-strategies/quick.json +1 -2
- package/dist/assets/improve-strategies/reflect-distill.json +1 -2
- package/dist/assets/improve-strategies/thorough.json +0 -3
- package/dist/assets/prompts/consolidate-pair.md +20 -0
- package/dist/assets/prompts/consolidate-system.md +4 -11
- package/dist/assets/prompts/retrieval-relevance-judge.md +6 -0
- package/dist/assets/stash-skeleton/facts/conventions/backlinks.md +20 -20
- package/dist/assets/stash-skeleton/facts/conventions/domains.md +2 -2
- package/dist/assets/templates/html/health.html +3 -5
- package/dist/cli/retired-commands.js +1 -1
- package/dist/cli/shared.js +6 -2
- package/dist/cli/unknown-flags.js +24 -1
- package/dist/cli.js +68 -10
- package/dist/commands/agent/agent-dispatch.js +1 -1
- package/dist/commands/command/command-execution.js +24 -62
- package/dist/commands/feedback-cli.js +0 -1
- package/dist/commands/health/accept-rate.js +6 -0
- package/dist/commands/health/archive-usage.js +92 -0
- package/dist/commands/health/checks.js +83 -74
- package/dist/commands/health/config-skew.js +38 -0
- package/dist/commands/health/data-dir-usage.js +25 -13
- package/dist/commands/health/egress.js +54 -0
- package/dist/commands/health/html-report.js +1 -42
- package/dist/commands/health/improve-metrics.js +136 -591
- package/dist/commands/health/md-report.js +1 -6
- package/dist/commands/health/plugin-staleness.js +53 -3
- package/dist/commands/health/renderers.js +12 -4
- package/dist/commands/health/report-view-model.js +14 -120
- package/dist/commands/health/types-improve.js +4 -19
- package/dist/commands/health/windows.js +64 -74
- package/dist/commands/health.js +145 -143
- package/dist/commands/improve/consolidate/chunking.js +26 -117
- package/dist/commands/improve/consolidate/continuity-check.js +137 -0
- package/dist/commands/improve/consolidate/pair-pass.js +791 -0
- package/dist/commands/improve/consolidate/sanitize.js +54 -149
- package/dist/commands/improve/consolidate.js +589 -1127
- package/dist/commands/improve/content-hash.js +16 -24
- package/dist/commands/improve/distill/content-repair.js +18 -100
- package/dist/commands/improve/distill-guards.js +20 -81
- package/dist/commands/improve/distill-promotion-policy.js +23 -243
- package/dist/commands/improve/distill.js +608 -1041
- package/dist/commands/improve/eligibility.js +126 -390
- package/dist/commands/improve/execution.js +8 -10
- package/dist/commands/improve/extract-prompt.js +1 -2
- package/dist/commands/improve/extract.js +487 -1046
- package/dist/commands/improve/feedback-valence.js +0 -25
- package/dist/commands/improve/improve-cli.js +75 -169
- package/dist/commands/improve/improve-result-file.js +10 -66
- package/dist/commands/improve/improve-strategies.js +52 -4
- package/dist/commands/improve/improve-usage-report.js +18 -64
- package/dist/commands/improve/improve.js +480 -1074
- package/dist/commands/improve/ledger.js +119 -0
- package/dist/commands/improve/locks.js +2 -8
- package/dist/commands/improve/loop-stages.js +415 -1073
- package/dist/commands/improve/memory/derived-ref.js +12 -77
- package/dist/commands/improve/memory/memory-belief.js +16 -118
- package/dist/commands/improve/memory/memory-improve.js +266 -14
- package/dist/commands/improve/outcome-loop.js +28 -156
- package/dist/commands/improve/planner.js +5 -15
- package/dist/commands/improve/preparation.js +779 -2319
- package/dist/commands/improve/proactive-maintenance.js +34 -101
- package/dist/commands/improve/reflect-noise.js +104 -280
- package/dist/commands/improve/reflect.js +642 -1353
- package/dist/commands/improve/retrieval-gate.js +127 -0
- package/dist/commands/improve/retrieval-scope.js +92 -0
- package/dist/commands/improve/salience.js +41 -240
- package/dist/commands/improve/session-asset.js +19 -100
- package/dist/commands/improve/stage.js +322 -0
- package/dist/commands/lint/base-linter.js +37 -15
- package/dist/commands/proposal/drain.js +261 -578
- package/dist/commands/proposal/proposal-cli.js +19 -20
- package/dist/commands/proposal/proposal-types.js +31 -24
- package/dist/commands/proposal/proposal.js +38 -8
- package/dist/commands/proposal/propose.js +134 -160
- package/dist/commands/proposal/repository.js +1097 -1394
- package/dist/commands/proposal/validators/proposal-quality-validators.js +71 -174
- package/dist/commands/proposal/validators/proposal-validators.js +1 -1
- package/dist/commands/proposal/validators/proposals.js +22 -89
- package/dist/commands/read/curate.js +105 -462
- package/dist/commands/read/knowledge.js +3 -2
- package/dist/commands/read/search-cli.js +16 -33
- package/dist/commands/read/search.js +17 -23
- package/dist/commands/read/show.js +57 -108
- package/dist/commands/sources/bundle-cli.js +25 -2
- package/dist/commands/sources/bundle-config-ops.js +4 -0
- package/dist/commands/sources/dangerous-env-audit.js +1 -2
- package/dist/commands/sources/info.js +127 -29
- package/dist/commands/sources/installed-stashes.js +197 -746
- package/dist/commands/sources/schema-repair.js +98 -129
- package/dist/commands/sources/source-add.js +62 -12
- package/dist/commands/sources/source-manage.js +9 -2
- package/dist/commands/sources/stash-cli.js +24 -4
- package/dist/commands/tasks/explain.js +10 -13
- package/dist/commands/tasks/tasks-cli.js +12 -13
- package/dist/commands/tasks/tasks.js +350 -936
- package/dist/commands/tasks/validate.js +26 -24
- package/dist/commands/workflow/plan.js +22 -29
- package/dist/commands/workflow-cli.js +4 -4
- package/dist/core/adapter/adapters/akm-adapter.js +2 -1
- package/dist/core/adapter/adapters/akm-lint.js +2 -3
- package/dist/core/adapter/adapters/akm-metadata.js +42 -12
- package/dist/core/adapter/adapters/akm-task-adapter.js +29 -8
- package/dist/core/adapter/adapters/akm-workflow-adapter.js +1 -1
- package/dist/core/adapter/execution-source.js +17 -29
- package/dist/core/asset/asset-placement.js +4 -13
- package/dist/core/asset/frontmatter.js +106 -1
- package/dist/core/asset/resolve-ref.js +1 -1
- package/dist/core/bundle-id.js +42 -5
- package/dist/core/bundle-rename.js +285 -0
- package/dist/core/config/config-io.js +1 -2
- package/dist/core/config/config-schema.js +9 -34
- package/dist/core/config/config-walker.js +1 -1
- package/dist/core/config/config.js +184 -111
- package/dist/core/config/engine-semantics.js +0 -2
- package/dist/core/config/legacy-source-shape-shim.js +38 -9
- package/dist/core/config/schema/embedding.js +20 -5
- package/dist/core/config/schema/engines.js +5 -0
- package/dist/core/config/schema/execution.js +1 -1
- package/dist/core/config/schema/experimental.js +1 -1
- package/dist/core/config/schema/improve-processes.js +54 -125
- package/dist/core/config/schema/improve.js +4 -42
- package/dist/core/config/schema/index-config.js +9 -48
- package/dist/core/config/schema/scheduler.js +12 -12
- package/dist/core/config/schema/search.js +6 -22
- package/dist/core/env-secret-ref.js +0 -1
- package/dist/core/errors.js +8 -9
- package/dist/core/file-change.js +13 -5
- package/dist/core/file-lock.js +76 -173
- package/dist/core/improve-result.js +35 -7
- package/dist/core/improve-types.js +0 -1
- package/dist/core/logs-db.js +2 -2
- package/dist/core/loopback.js +7 -12
- package/dist/core/non-task-input.js +20 -0
- package/dist/core/parse.js +13 -16
- package/dist/core/paths.js +0 -24
- package/dist/core/redaction.js +109 -2
- package/dist/core/run-lock.js +2 -5
- package/dist/core/spawn-env.js +1 -1
- package/dist/core/state/migrations.js +123 -61
- package/dist/core/state-db-scope.js +2 -4
- package/dist/core/state-db.js +126 -692
- package/dist/core/time.js +0 -20
- package/dist/core/type-presentation.js +1 -9
- package/dist/core/write-source.js +294 -1005
- package/dist/execution/input-contract.js +1 -1
- package/dist/execution/resolved-request.js +135 -689
- package/dist/execution/source.js +63 -257
- package/dist/execution/target-ref.js +1 -1
- package/dist/indexer/bundle-identity-guard.js +2 -2
- package/dist/indexer/db/llm-cache.js +2 -2
- package/dist/indexer/ensure-index.js +77 -73
- package/dist/indexer/index-rebuild-lock.js +3 -11
- package/dist/indexer/index-writer-lock.js +8 -17
- package/dist/indexer/index-written-assets.js +141 -154
- package/dist/indexer/indexer.js +400 -1124
- package/dist/indexer/links/declared-links.js +90 -0
- package/dist/indexer/materialize-embeddings.js +60 -397
- package/dist/indexer/passes/memory-inference.js +96 -90
- package/dist/indexer/passes/metadata.js +132 -219
- package/dist/indexer/read-preflight.js +0 -7
- package/dist/indexer/scan/doc-to-entry.js +2 -3
- package/dist/indexer/scan/drain-dir.js +1 -1
- package/dist/indexer/search/db-search.js +190 -590
- package/dist/indexer/search/fts-query.js +30 -41
- package/dist/indexer/search/ranking.js +28 -154
- package/dist/indexer/search/search-attribution.js +12 -32
- package/dist/indexer/search/search-fields.js +11 -15
- package/dist/indexer/search/search-hit-enrichers.js +54 -85
- package/dist/indexer/search/search-source.js +1 -4
- package/dist/indexer/usage/usage-events.js +36 -7
- package/dist/indexer/walk/walker.js +3 -4
- package/dist/integrations/agent/engine-fallback.js +23 -40
- package/dist/integrations/agent/engine-resolution.js +93 -183
- package/dist/integrations/agent/execution.js +507 -0
- package/dist/integrations/agent/model-map.js +28 -156
- package/dist/integrations/agent/request-lowering.js +66 -141
- package/dist/integrations/agent/runner-dispatch.js +143 -321
- package/dist/integrations/agent/runner.js +54 -14
- package/dist/integrations/lockfile.js +53 -101
- package/dist/llm/client.js +18 -6
- package/dist/llm/embedders/deterministic.js +2 -3
- package/dist/llm/embedders/profile.js +71 -0
- package/dist/llm/embedders/remote.js +11 -17
- package/dist/llm/feature-gate.js +0 -8
- package/dist/llm/index-passes.js +3 -5
- package/dist/llm/memory-infer.js +1 -2
- package/dist/llm/structured-call.js +5 -24
- package/dist/output/generic-render.js +23 -11
- package/dist/output/html-render.js +13 -10
- package/dist/output/render-registry.js +3 -32
- package/dist/output/shapes/helpers.js +25 -38
- package/dist/output/shapes/passthrough.js +1 -9
- package/dist/{indexer/graph/graph-types.js → output/text/bundle-rename.js} +4 -1
- package/dist/output/text/command-format.js +69 -31
- package/dist/output/text/helpers.js +1 -1
- package/dist/output/text/migrate.js +5 -14
- package/dist/output/text/proposal-format.js +48 -3
- package/dist/output/text/show-format.js +13 -17
- package/dist/output/text/workflow-format.js +0 -32
- package/dist/output/text.js +2 -0
- package/dist/registry/factory.js +4 -19
- package/dist/registry/network.js +66 -220
- package/dist/registry/providers/index.js +0 -2
- package/dist/registry/providers/skills-sh.js +3 -14
- package/dist/registry/providers/static-index.js +24 -26
- package/dist/registry/resolve.js +55 -131
- package/dist/scripts/akm-migrate-node.js +42948 -92369
- package/dist/scripts/akm-migrate.js +42935 -92354
- package/dist/setup/registry-stash-loader.js +4 -13
- package/dist/setup/semantic-assets.js +3 -44
- package/dist/setup/setup.js +1 -1
- package/dist/setup/steps/connection.js +5 -6
- package/dist/setup/steps/platforms.js +2 -2
- package/dist/setup/steps/tasks.js +25 -15
- package/dist/sources/provider-factory.js +17 -18
- package/dist/sources/providers/filesystem.js +2 -3
- package/dist/sources/providers/git-install.js +7 -1
- package/dist/sources/providers/git-provider.js +0 -3
- package/dist/sources/providers/git-stash.js +83 -21
- package/dist/sources/providers/npm.js +2 -4
- package/dist/sources/providers/provider-utils.js +5 -10
- package/dist/sources/providers/website.js +0 -2
- package/dist/sources/snapshot-fetchers/website-ingest.js +1 -1
- package/dist/sources/website-url.js +2 -2
- package/dist/storage/database.js +9 -35
- package/dist/storage/repositories/improve-ledger-repository.js +209 -0
- package/dist/storage/repositories/index-connection.js +39 -72
- package/dist/storage/repositories/index-entries-repository.js +131 -129
- package/dist/storage/repositories/index-entry-mapper.js +1 -2
- package/dist/storage/repositories/index-entry-schema.js +101 -268
- package/dist/storage/repositories/index-fts-repository.js +86 -256
- package/dist/storage/repositories/index-links-repository.js +143 -0
- package/dist/storage/repositories/index-llm-cache-repository.js +7 -9
- package/dist/storage/repositories/index-meta-repository.js +6 -4
- package/dist/storage/repositories/index-schema.js +257 -325
- package/dist/storage/repositories/index-utility-repository.js +8 -29
- package/dist/storage/repositories/index-vec-repository.js +133 -414
- package/dist/storage/repositories/outcome-repository.js +2 -1
- package/dist/storage/repositories/proposals-repository.js +104 -1
- package/dist/storage/repositories/registry-index-cache-repository.js +100 -0
- package/dist/storage/repositories/salience-repository.js +1 -19
- package/dist/storage/repositories/task-history-repository.js +26 -4
- package/dist/storage/repositories/workflow-runs-repository.js +53 -244
- package/dist/storage/sqlite-migrations.js +136 -0
- package/dist/storage/sqlite-pragmas.js +11 -9
- package/dist/storage/sqlite-transaction.js +170 -0
- package/dist/storage/state-db-integrity.js +130 -0
- package/dist/tasks/activation-config.js +134 -62
- package/dist/tasks/backends/cron.js +191 -302
- package/dist/tasks/backends/exec-utils.js +2 -5
- package/dist/tasks/backends/launchd.js +141 -748
- package/dist/tasks/backends/schtasks.js +119 -623
- package/dist/tasks/prepare/prepare-support.js +5 -15
- package/dist/tasks/prepare/prepare.js +0 -2
- package/dist/tasks/resolve-akm-bin.js +20 -79
- package/dist/tasks/run/attempt-lifecycle.js +0 -1
- package/dist/tasks/run/load-task.js +1 -1
- package/dist/tasks/scheduler-binding.js +20 -238
- package/dist/tasks/scheduler-invocation.js +136 -244
- package/dist/tasks/scheduler-lock.js +53 -0
- package/dist/tasks/scheduler-sync.js +368 -679
- package/dist/tasks/source/parse-task-source.js +55 -9
- package/dist/tasks/source/task-source-v3-frozen.js +3 -4
- package/dist/tasks/source/task-to-v4.js +464 -88
- package/dist/workflows/authoring/authoring.js +3 -12
- package/dist/workflows/compile.js +211 -0
- package/dist/workflows/concurrency-policy.js +13 -74
- package/dist/workflows/exec/child-invocation.js +3 -17
- package/dist/workflows/exec/child-workflow.js +32 -141
- package/dist/workflows/exec/dispatch-redaction.js +13 -53
- package/dist/workflows/exec/environment.js +98 -0
- package/dist/workflows/exec/exec-unit.js +33 -140
- package/dist/workflows/exec/frozen-judge.js +7 -59
- package/dist/workflows/exec/native-executor.js +82 -341
- package/dist/workflows/exec/param-secrets.js +29 -47
- package/dist/workflows/exec/run-workflow.js +154 -387
- package/dist/workflows/exec/scheduler.js +9 -36
- package/dist/workflows/exec/step-work.js +127 -430
- package/dist/workflows/exec/unit-dispatch.js +11 -63
- package/dist/workflows/exec/unit-writer.js +8 -52
- package/dist/workflows/exec/worktree.js +39 -273
- package/dist/workflows/freeze/child-output-references.js +4 -15
- package/dist/workflows/freeze/environment.js +99 -92
- package/dist/workflows/freeze/freeze.js +172 -0
- package/dist/workflows/freeze/step-values.js +19 -21
- package/dist/workflows/freeze/targets/child-workflow.js +23 -92
- package/dist/workflows/freeze/targets/command.js +10 -33
- package/dist/workflows/freeze/targets/script.js +5 -12
- package/dist/workflows/freeze/targets/shell.js +3 -6
- package/dist/workflows/freeze/targets/task.js +25 -80
- package/dist/workflows/freeze/task-bindings.js +20 -67
- package/dist/workflows/{source-ir/github-yaml.js → github-yaml.js} +88 -206
- package/dist/workflows/ir/params.js +6 -51
- package/dist/workflows/ir/plan-hash.js +2 -34
- package/dist/workflows/parser.js +140 -43
- package/dist/{commands/improve/consolidate/types.js → workflows/plan.js} +2 -1
- package/dist/workflows/renderer.js +36 -69
- package/dist/workflows/resource-limits.js +12 -120
- package/dist/workflows/runtime/agent-identity.js +8 -40
- package/dist/workflows/runtime/run-outputs.js +3 -6
- package/dist/workflows/runtime/run-plan.js +316 -0
- package/dist/workflows/runtime/runs.js +48 -200
- package/dist/workflows/runtime/workflow-asset-loader.js +24 -57
- package/dist/workflows/{source-ir/semantics.js → source-semantics.js} +16 -20
- package/dist/workflows/validate-summary.js +2 -7
- package/docs/integration/bundling-akm.md +49 -42
- package/docs/migration/README.md +1 -0
- package/docs/migration/release-notes/0.9.17.md +43 -0
- package/docs/migration/v0.9.1-to-v0.9.2.md +23 -7
- package/docs/reference/cli.md +232 -135
- package/docs/reference/configuration.md +71 -57
- package/docs/reference/data-and-telemetry.md +20 -21
- package/docs/reference/tasks.md +105 -39
- package/docs/reference/workflow-schema.md +14 -18
- package/docs/reference/workflows.md +6 -9
- package/package.json +1 -1
- package/schemas/akm-config.json +115 -738
- package/schemas/akm-workflow.json +1 -0
- package/dist/assets/improve-strategies/graph-refresh.json +0 -15
- package/dist/assets/prompts/contradiction-judge.md +0 -33
- package/dist/assets/prompts/graph-extract-system.md +0 -1
- package/dist/assets/prompts/graph-extract-user-prompt.md +0 -35
- package/dist/assets/prompts/metadata-enhance-system.md +0 -1
- package/dist/assets/tasks/improve/akm-graph-refresh-weekly.yml +0 -4
- package/dist/commands/health/advisories.js +0 -150
- package/dist/commands/health/metrics.js +0 -329
- package/dist/commands/health/surfaces.js +0 -102
- package/dist/commands/improve/anti-collapse.js +0 -83
- package/dist/commands/improve/collapse-detector.js +0 -432
- package/dist/commands/improve/consolidate/eligibility.js +0 -48
- package/dist/commands/improve/consolidate/merge.js +0 -149
- package/dist/commands/improve/distill/promote-memory.js +0 -291
- package/dist/commands/improve/distill/quality-gate.js +0 -337
- package/dist/commands/improve/eval-cases.js +0 -52
- package/dist/commands/improve/memory/memory-contradiction-detect.js +0 -291
- package/dist/commands/improve/proposal-envelope.js +0 -31
- package/dist/commands/improve/run-context.js +0 -123
- package/dist/commands/improve/shared.js +0 -31
- package/dist/commands/improve/source-identity.js +0 -28
- package/dist/commands/improve/triage.js +0 -96
- package/dist/commands/proposal/drain-policies.js +0 -151
- package/dist/commands/sources/update-transaction.js +0 -220
- package/dist/core/action-contributors.js +0 -28
- package/dist/core/config/config-version-shim.js +0 -101
- package/dist/core/fs-txn.js +0 -405
- package/dist/core/lexical-score.js +0 -25
- package/dist/core/maintenance-barrier.js +0 -167
- package/dist/execution/executable-identity.js +0 -105
- package/dist/execution/guarded-source.js +0 -427
- package/dist/indexer/db/graph-db.js +0 -444
- package/dist/indexer/graph/graph-boost.js +0 -427
- package/dist/indexer/graph/graph-dedup.js +0 -95
- package/dist/indexer/graph/graph-extraction.js +0 -1108
- package/dist/indexer/search/name-match.js +0 -35
- package/dist/indexer/search/ranking-contributors.js +0 -515
- package/dist/indexer/search/ranking-types.js +0 -4
- package/dist/indexer/walk/project-context.js +0 -192
- package/dist/integrations/agent/execution-cascade.js +0 -566
- package/dist/integrations/agent/execution-definitions.js +0 -202
- package/dist/integrations/agent/execution-lowering.js +0 -841
- package/dist/integrations/agent/execution-preparation.js +0 -98
- package/dist/integrations/agent/inline-execution.js +0 -74
- package/dist/llm/graph-extract.js +0 -728
- package/dist/llm/metadata-enhance.js +0 -96
- package/dist/registry/create-provider-registry.js +0 -29
- package/dist/registry/pinned-request-helper.js +0 -247
- package/dist/registry/pinned-transport.js +0 -717
- package/dist/sources/providers/index.js +0 -14
- package/dist/storage/engines/sqlite-migrations.js +0 -271
- package/dist/storage/repositories/canaries-repository.js +0 -107
- package/dist/storage/repositories/embedding-salvage-repository.js +0 -184
- package/dist/storage/repositories/registry-cache.js +0 -113
- package/dist/tasks/scheduler-sync-preview.js +0 -52
- package/dist/tasks/source/task-to-v3.js +0 -507
- package/dist/workflows/freeze/resolve-steps.js +0 -86
- package/dist/workflows/freeze/source-freeze.js +0 -64
- package/dist/workflows/ir/compile.js +0 -321
- package/dist/workflows/ir/environment-v4.js +0 -330
- package/dist/workflows/ir/freeze-v4.js +0 -153
- package/dist/workflows/ir/schema-v4.js +0 -745
- package/dist/workflows/ir/schema.js +0 -354
- package/dist/workflows/program/schema.js +0 -77
- package/dist/workflows/runtime/checkin.js +0 -57
- package/dist/workflows/runtime/plan-classifier.js +0 -196
- package/dist/workflows/runtime/unit-checkin.js +0 -45
- package/dist/workflows/runtime/unit-phases.js +0 -20
- package/dist/workflows/schema.js +0 -4
- package/dist/workflows/source-ir/compile.js +0 -200
- package/dist/workflows/source-ir/program.js +0 -50
- package/dist/workflows/source-ir/result.js +0 -26
- package/dist/workflows/source-ir/schema.js +0 -786
- package/dist/workflows/source-ir/triggers.js +0 -79
- package/dist/workflows/source-ir/uses.js +0 -40
- package/dist/workflows/validator.js +0 -60
|
@@ -0,0 +1,791 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
/**
|
|
5
|
+
* The consolidate pair pass (0.9.17-alpha.9 plan §5.2, brief §A): runs
|
|
6
|
+
* alongside the existing promote pass inside `akmConsolidate`, over new and
|
|
7
|
+
* changed memory-tier assets. Judges each near-duplicate/superseding pair
|
|
8
|
+
* with the calibrated relation prompt and mints a reviewed `retire` proposal
|
|
9
|
+
* for the `duplicate` / `subsumed` / `supersedes` classes — never merges,
|
|
10
|
+
* never writes belief edges for `contradicts`, and is never auto-accepted by
|
|
11
|
+
* triage (`drain.ts`).
|
|
12
|
+
*
|
|
13
|
+
* Calibration (owner grades, 2026-09-28; see the plan's "Human calibration,
|
|
14
|
+
* O5" section): the combined retire class (duplicate ∪ subsumed ∪
|
|
15
|
+
* supersedes) is 20/22 = 0.91 precision against the owner. `T_PAIR` is 0.93,
|
|
16
|
+
* not the initial 0.90, because the 0.90–0.93 band alone graded 3/4 (R1).
|
|
17
|
+
*
|
|
18
|
+
* Initiator eligibility and dating (post-review, alpha.9): "created" is the
|
|
19
|
+
* asset's git first-add time (one `git log` per run, {@link loadGitFirstAddedMap}),
|
|
20
|
+
* not frontmatter or mtime — mtime is only the fallback for a file git does
|
|
21
|
+
* not know. An initiator is eligible when it has no prior `consolidate-pair`
|
|
22
|
+
* ledger row, or its current body hash differs from the row's recorded one;
|
|
23
|
+
* that row is written only once ALL of the initiator's own candidates were
|
|
24
|
+
* judged this run (a run capped mid-way through its candidates leaves it
|
|
25
|
+
* without a row, so the next run picks it back up) — see
|
|
26
|
+
* {@link selectInitiators} and the ledger-write step in
|
|
27
|
+
* {@link runConsolidatePairPass}.
|
|
28
|
+
*/
|
|
29
|
+
import fs from "node:fs";
|
|
30
|
+
import path from "node:path";
|
|
31
|
+
import consolidatePairPrompt from "../../../assets/prompts/consolidate-pair.md" with { type: "text" };
|
|
32
|
+
import { parseFrontmatter } from "../../../core/asset/frontmatter.js";
|
|
33
|
+
import { conceptIdFromTypeName } from "../../../core/asset/resolve-ref.js";
|
|
34
|
+
import { asNonEmptyString } from "../../../core/common.js";
|
|
35
|
+
import { concurrentMap } from "../../../core/concurrent.js";
|
|
36
|
+
import { parseEmbeddedJsonResponse } from "../../../core/parse.js";
|
|
37
|
+
import { DERIVED_SUFFIX } from "../../../core/recognition-util.js";
|
|
38
|
+
import { warnOnce } from "../../../core/warn.js";
|
|
39
|
+
import { assertRunnerCredentials } from "../../../integrations/agent/runner-dispatch.js";
|
|
40
|
+
import { runGit } from "../../../sources/providers/git-install.js";
|
|
41
|
+
import { closeDatabase, openExistingDatabase, openReadonlyExistingDatabase, } from "../../../storage/repositories/index-connection.js";
|
|
42
|
+
import { getAllEntries, getEntryById } from "../../../storage/repositories/index-entries-repository.js";
|
|
43
|
+
import { getNeighborsByEntryId } from "../../../storage/repositories/index-vec-repository.js";
|
|
44
|
+
import { isRetireProposal } from "../../proposal/proposal-types.js";
|
|
45
|
+
import { createRetireProposal, listProposalsReadOnly } from "../../proposal/repository.js";
|
|
46
|
+
import { isHotCapturedMemory } from "../consolidate.js";
|
|
47
|
+
import { contentHash, stripFrontmatterBody } from "../content-hash.js";
|
|
48
|
+
import { loadLedgerSnapshot, PAIR_PASS_LEDGER_SOURCE, recordLedgerAttempt, stripBundle } from "../ledger.js";
|
|
49
|
+
import { isInRetrievalScope, loadRetrievalScope } from "../retrieval-scope.js";
|
|
50
|
+
import { callStage } from "../stage.js";
|
|
51
|
+
import { checkRetirementContinuity, createContinuitySearch } from "./continuity-check.js";
|
|
52
|
+
export { PAIR_PASS_LEDGER_SOURCE };
|
|
53
|
+
/** Neighbours fetched per initiator before filtering (S2: wide enough that self/twin/bundle/tier misses rarely starve the kept 5 below). */
|
|
54
|
+
export const PAIR_NEIGHBOR_FETCH_K = 20;
|
|
55
|
+
/** Passing candidates kept per initiator, nearest-cosine-first. */
|
|
56
|
+
export const PAIR_NEIGHBOR_K = 5;
|
|
57
|
+
/** Calibrated floor (R1): the 0.90–0.93 band alone graded 3/4 against the owner. */
|
|
58
|
+
export const T_PAIR = 0.93;
|
|
59
|
+
/** O2: the existing backlog (an initiator with no prior pair-pass attempt) goes >= 0.95 first. */
|
|
60
|
+
export const BACKFILL_FLOOR = 0.95;
|
|
61
|
+
/**
|
|
62
|
+
* An initiator git first-added within this many days judges at `T_PAIR` even
|
|
63
|
+
* with no prior ledger row: new material earns the same scrutiny as an edit,
|
|
64
|
+
* not the higher bar reserved for working through the pre-existing backlog.
|
|
65
|
+
*/
|
|
66
|
+
export const NEW_MATERIAL_DAYS = 7;
|
|
67
|
+
/** Pairs judged per run, highest cosine first (plan §7's nightly cost budget). */
|
|
68
|
+
export const MAX_PAIRS_PER_RUN = 300;
|
|
69
|
+
/** Body characters sent to the judge per side (plan §4.3: bodies were truncated at this length for calibration). */
|
|
70
|
+
const PAIR_BODY_TRUNCATE_CHARS = 2500;
|
|
71
|
+
const MS_PER_DAY = 86_400_000;
|
|
72
|
+
const RELATION_LABELS = ["duplicate", "subsumed", "supersedes", "contradicts", "overlap", "unrelated"];
|
|
73
|
+
const RETIRE_LABELS = new Set(["duplicate", "subsumed", "supersedes"]);
|
|
74
|
+
/** The classes that mint a `retire` proposal (the calibrated combined retire class, 20/22 precision). */
|
|
75
|
+
function isRetireLabel(label) {
|
|
76
|
+
return RETIRE_LABELS.has(label);
|
|
77
|
+
}
|
|
78
|
+
const PAIR_JUDGE_JSON_SCHEMA = {
|
|
79
|
+
type: "object",
|
|
80
|
+
required: ["relation", "redundant", "stale", "confidence", "reason"],
|
|
81
|
+
additionalProperties: false,
|
|
82
|
+
properties: {
|
|
83
|
+
relation: { type: "string", enum: [...RELATION_LABELS] },
|
|
84
|
+
redundant: { type: ["string", "null"], enum: ["A", "B", null] },
|
|
85
|
+
stale: { type: ["string", "null"], enum: ["A", null] },
|
|
86
|
+
confidence: { type: "number", minimum: 0, maximum: 1 },
|
|
87
|
+
reason: { type: "string", maxLength: 400 },
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
/** Hand-validates the judge's JSON, independent of whatever the provider's own schema enforcement did. */
|
|
91
|
+
export function parsePairJudgeResponse(raw) {
|
|
92
|
+
const parsed = parseEmbeddedJsonResponse(raw);
|
|
93
|
+
if (!parsed)
|
|
94
|
+
return undefined;
|
|
95
|
+
if (typeof parsed.relation !== "string" || !RELATION_LABELS.includes(parsed.relation)) {
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
const redundant = parsed.redundant;
|
|
99
|
+
if (redundant !== "A" && redundant !== "B" && redundant !== null)
|
|
100
|
+
return undefined;
|
|
101
|
+
if (typeof parsed.confidence !== "number" || !Number.isFinite(parsed.confidence))
|
|
102
|
+
return undefined;
|
|
103
|
+
const confidence = Math.max(0, Math.min(1, parsed.confidence));
|
|
104
|
+
const reason = typeof parsed.reason === "string" ? parsed.reason : "";
|
|
105
|
+
return { relation: parsed.relation, redundant, confidence, reason };
|
|
106
|
+
}
|
|
107
|
+
function isFlatName(name) {
|
|
108
|
+
return !name.includes("/");
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* True only for a memory's own direct `.derived` child or parent — never a
|
|
112
|
+
* sibling or an unrelated pair. @internal exported for unit tests.
|
|
113
|
+
*/
|
|
114
|
+
export function isOwnTwinOrParent(a, b) {
|
|
115
|
+
if (a.type !== "memory" || b.type !== "memory")
|
|
116
|
+
return false;
|
|
117
|
+
return a.name === `${b.name}${DERIVED_SUFFIX}` || b.name === `${a.name}${DERIVED_SUFFIX}`;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Every asset eligible to be a pair-pass initiator or candidate: memory (any
|
|
121
|
+
* depth), flat knowledge, or a lesson. @internal exported for unit tests.
|
|
122
|
+
*/
|
|
123
|
+
export function loadPairPassPool(db, bundleId) {
|
|
124
|
+
const assets = [];
|
|
125
|
+
for (const e of getAllEntries(db, "memory")) {
|
|
126
|
+
if (e.bundleId !== bundleId || !fs.existsSync(e.filePath))
|
|
127
|
+
continue;
|
|
128
|
+
assets.push({
|
|
129
|
+
ref: conceptIdFromTypeName("memory", e.entry.name),
|
|
130
|
+
type: "memory",
|
|
131
|
+
name: e.entry.name,
|
|
132
|
+
filePath: e.filePath,
|
|
133
|
+
entryId: e.id,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
for (const e of getAllEntries(db, "knowledge")) {
|
|
137
|
+
if (e.bundleId !== bundleId || !isFlatName(e.entry.name) || !fs.existsSync(e.filePath))
|
|
138
|
+
continue;
|
|
139
|
+
assets.push({
|
|
140
|
+
ref: conceptIdFromTypeName("knowledge", e.entry.name),
|
|
141
|
+
type: "knowledge",
|
|
142
|
+
name: e.entry.name,
|
|
143
|
+
filePath: e.filePath,
|
|
144
|
+
entryId: e.id,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
for (const e of getAllEntries(db, "lesson")) {
|
|
148
|
+
if (e.bundleId !== bundleId || !fs.existsSync(e.filePath))
|
|
149
|
+
continue;
|
|
150
|
+
assets.push({
|
|
151
|
+
ref: conceptIdFromTypeName("lesson", e.entry.name),
|
|
152
|
+
type: "lesson",
|
|
153
|
+
name: e.entry.name,
|
|
154
|
+
filePath: e.filePath,
|
|
155
|
+
entryId: e.id,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
return assets;
|
|
159
|
+
}
|
|
160
|
+
// ── B1: created/updated dates from git, not frontmatter/mtime ──────────────
|
|
161
|
+
/** A path relative to `stashDir`, POSIX-separated — how `git log --name-only` spells it. */
|
|
162
|
+
function repoRelativeKey(stashDir, filePath) {
|
|
163
|
+
return path.relative(stashDir, filePath).replace(/\\/g, "/");
|
|
164
|
+
}
|
|
165
|
+
/** 16 MiB — see the buffer-overflow comment inside {@link loadGitFirstAddedMap}. */
|
|
166
|
+
const GIT_LOG_MAX_BUFFER = 16 * 1024 * 1024;
|
|
167
|
+
/**
|
|
168
|
+
* Every tracked path's first-add time (unix ms), from one `git log` over the
|
|
169
|
+
* whole bundle (~2.1s measured against the owner's real bundle) — never
|
|
170
|
+
* shelled out per pair or per initiator. Follows renames (`-M
|
|
171
|
+
* --diff-filter=AR`, oldest-first via `--reverse`): a renamed path inherits
|
|
172
|
+
* its pre-rename first-add time, not the rename's own timestamp — the owner's
|
|
173
|
+
* 2026-08-24 bulk rename alone re-dated 966 files under `--no-renames`, and
|
|
174
|
+
* 11% of real candidate pairs flipped which side counted as older. A raised
|
|
175
|
+
* `diff.renameLimit` keeps a large bulk-rename commit (exactly this
|
|
176
|
+
* scenario) from silently falling back to detecting no renames at all.
|
|
177
|
+
* `undefined` when `stashDir` is not itself a git root (no `.git` directly
|
|
178
|
+
* inside it): every asset then falls back to mtime in {@link createdMsOf},
|
|
179
|
+
* one fallback code path instead of a second git-aware one for a bundle
|
|
180
|
+
* nested inside a larger repo.
|
|
181
|
+
*/
|
|
182
|
+
export function loadGitFirstAddedMap(stashDir) {
|
|
183
|
+
if (!fs.existsSync(path.join(stashDir, ".git")))
|
|
184
|
+
return undefined;
|
|
185
|
+
let result;
|
|
186
|
+
try {
|
|
187
|
+
result = runGit(["-c", "diff.renameLimit=20000", "log", "--reverse", "-M", "--diff-filter=AR", "--name-status", "--format=@%ct"], {
|
|
188
|
+
cwd: stashDir,
|
|
189
|
+
// spawnSync's default maxBuffer (1 MB) is too small for a bundle
|
|
190
|
+
// with real history — the owner's real bundle alone prints
|
|
191
|
+
// multiple MB here (migration-tool.ts's own git subprocess call
|
|
192
|
+
// uses the same 16 MiB figure). Silently exceeding it looks
|
|
193
|
+
// identical to "git failed" from the caller's side (status stays
|
|
194
|
+
// non-zero) — checked explicitly below instead of folded into the
|
|
195
|
+
// same silent fallback as "no .git", since raising the buffer
|
|
196
|
+
// again is an actual fix and worth telling the operator about.
|
|
197
|
+
maxBuffer: GIT_LOG_MAX_BUFFER,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
return undefined;
|
|
202
|
+
}
|
|
203
|
+
if (result.error) {
|
|
204
|
+
const code = result.error.code;
|
|
205
|
+
if (code === "ENOBUFS" || /maxBuffer/i.test(result.error.message ?? "")) {
|
|
206
|
+
warnOnce("pair-pass-git-log-maxbuffer", `[consolidate] pair pass: git log for first-add dates in ${stashDir} exceeded its ${GIT_LOG_MAX_BUFFER / (1024 * 1024)} MiB buffer — dates fall back to mtime this run.`);
|
|
207
|
+
}
|
|
208
|
+
return undefined;
|
|
209
|
+
}
|
|
210
|
+
if (result.status !== 0 || typeof result.stdout !== "string")
|
|
211
|
+
return undefined;
|
|
212
|
+
// Oldest-first (--reverse): the FIRST time a path is seen, whether as a
|
|
213
|
+
// plain add or as a rename's destination, IS its true first-add time — no
|
|
214
|
+
// backward walk needed. A rename's destination inherits the source's
|
|
215
|
+
// already-recorded time (or, failing that — the source itself predates
|
|
216
|
+
// this log's window — this commit's own time).
|
|
217
|
+
const firstAdd = new Map();
|
|
218
|
+
let currentMs;
|
|
219
|
+
for (const line of result.stdout.split("\n")) {
|
|
220
|
+
if (line.startsWith("@")) {
|
|
221
|
+
const sec = Number(line.slice(1));
|
|
222
|
+
currentMs = Number.isFinite(sec) ? sec * 1000 : undefined;
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
if (currentMs === undefined)
|
|
226
|
+
continue;
|
|
227
|
+
const tab = line.indexOf("\t");
|
|
228
|
+
if (tab < 0)
|
|
229
|
+
continue;
|
|
230
|
+
const status = line.slice(0, tab);
|
|
231
|
+
if (status === "A") {
|
|
232
|
+
const p = line.slice(tab + 1).trim();
|
|
233
|
+
if (p && !firstAdd.has(p))
|
|
234
|
+
firstAdd.set(p, currentMs);
|
|
235
|
+
}
|
|
236
|
+
else if (status.startsWith("R")) {
|
|
237
|
+
const rest = line.slice(tab + 1);
|
|
238
|
+
const tab2 = rest.indexOf("\t");
|
|
239
|
+
if (tab2 < 0)
|
|
240
|
+
continue;
|
|
241
|
+
const oldPath = rest.slice(0, tab2).trim();
|
|
242
|
+
const newPath = rest.slice(tab2 + 1).trim();
|
|
243
|
+
if (oldPath && newPath && !firstAdd.has(newPath)) {
|
|
244
|
+
firstAdd.set(newPath, firstAdd.get(oldPath) ?? currentMs);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return firstAdd;
|
|
249
|
+
}
|
|
250
|
+
/** Created instant (ms): git first-add when known, else file mtime (B1) — the one fallback both dating and the S1 new-material check use. */
|
|
251
|
+
function createdMsOf(asset, gitFirstAdded, stashDir) {
|
|
252
|
+
const known = gitFirstAdded?.get(repoRelativeKey(stashDir, asset.filePath));
|
|
253
|
+
if (known !== undefined)
|
|
254
|
+
return known;
|
|
255
|
+
try {
|
|
256
|
+
return fs.statSync(asset.filePath).mtimeMs;
|
|
257
|
+
}
|
|
258
|
+
catch {
|
|
259
|
+
return 0;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
/** An unordered pair's dedup key, so a pair reachable from either side is judged once. */
|
|
263
|
+
function pairKey(a, b) {
|
|
264
|
+
return [a, b].sort().join("\u0000");
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* A rejected retirement's dedup key (item 0): the exact ref pair plus both
|
|
268
|
+
* content hashes at judge time, so a re-selected initiator (its own ledger
|
|
269
|
+
* row missing because a sibling candidate was dropped or failed, not because
|
|
270
|
+
* this pair changed) does not get this same, already-rejected pair re-judged
|
|
271
|
+
* into a new proposal.
|
|
272
|
+
*/
|
|
273
|
+
function rejectedPairKey(retiredRef, successorRef, retiredHash, successorHash) {
|
|
274
|
+
return [retiredRef, successorRef, retiredHash, successorHash].join("\u0000");
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Initiators (plan §5.2 step 1, S1 post-review): the pool, in the retrieval
|
|
278
|
+
* scope, and content-eligible — no prior `consolidate-pair` ledger row, or a
|
|
279
|
+
* row whose recorded body hash differs from the asset's current one. A row's
|
|
280
|
+
* `next_eligible_at` is never consulted (the pair pass's own source carries
|
|
281
|
+
* no timer at all — see `windowDays` in improve-ledger-repository.ts):
|
|
282
|
+
* eligibility here is purely a function of content, matching the brief's "no
|
|
283
|
+
* row, or changed" rule and the ledger-write step this run finishes with.
|
|
284
|
+
*/
|
|
285
|
+
export function selectInitiators(pool, opts, stashDir, gitFirstAdded) {
|
|
286
|
+
const retrievalScope = loadRetrievalScope({ proposalsCtx: opts.proposalsCtx }, stashDir);
|
|
287
|
+
const ledger = loadLedgerSnapshot({ proposalsCtx: opts.proposalsCtx }, stashDir, [PAIR_PASS_LEDGER_SOURCE]);
|
|
288
|
+
const nowMs = (opts.proposalsCtx?.now ?? Date.now)();
|
|
289
|
+
const initiators = [];
|
|
290
|
+
for (const asset of pool) {
|
|
291
|
+
if (!isInRetrievalScope(retrievalScope, asset.ref, asset.filePath))
|
|
292
|
+
continue;
|
|
293
|
+
const row = ledger.get(`${PAIR_PASS_LEDGER_SOURCE}\0${asset.ref}`);
|
|
294
|
+
let raw;
|
|
295
|
+
try {
|
|
296
|
+
raw = fs.readFileSync(asset.filePath, "utf8");
|
|
297
|
+
}
|
|
298
|
+
catch {
|
|
299
|
+
continue; // unreadable: selectCandidates/judgeOne would skip it anyway
|
|
300
|
+
}
|
|
301
|
+
const bodyHash = contentHash(raw, "body");
|
|
302
|
+
if (row && row.contentHash === bodyHash)
|
|
303
|
+
continue; // unchanged since the last full attempt: not eligible
|
|
304
|
+
const createdMs = createdMsOf(asset, gitFirstAdded, stashDir);
|
|
305
|
+
const newMaterial = nowMs - createdMs <= NEW_MATERIAL_DAYS * MS_PER_DAY;
|
|
306
|
+
initiators.push({ ...asset, backlog: row === undefined, newMaterial, bodyHash });
|
|
307
|
+
}
|
|
308
|
+
return { initiators };
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Candidates (plan §5.2 step 2, S1/S2 post-review): each initiator's nearest
|
|
312
|
+
* neighbours, filtered and thresholded — the FULL set, sorted by cosine
|
|
313
|
+
* descending, uncapped. `runConsolidatePairPass` applies `MAX_PAIRS_PER_RUN`
|
|
314
|
+
* (needing the uncapped per-initiator totals to tell a cap-cut initiator
|
|
315
|
+
* apart from a fully-judged one). S2: fetches `PAIR_NEIGHBOR_FETCH_K` (20)
|
|
316
|
+
* raw neighbours and keeps the first `PAIR_NEIGHBOR_K` (5) that clear every
|
|
317
|
+
* filter, so a few self/twin/bundle/tier misses in the raw top-5 no longer
|
|
318
|
+
* starve an initiator down to zero real candidates.
|
|
319
|
+
*/
|
|
320
|
+
export function selectCandidates(db, initiators, bundleId) {
|
|
321
|
+
const candidates = [];
|
|
322
|
+
const seenPairs = new Set();
|
|
323
|
+
for (const initiator of initiators) {
|
|
324
|
+
// S1: a changed-content or new-material initiator judges at T_PAIR; the
|
|
325
|
+
// rest of the backlog (no row, not recently git-added) needs the higher
|
|
326
|
+
// BACKFILL_FLOOR.
|
|
327
|
+
const floor = initiator.backlog && !initiator.newMaterial ? BACKFILL_FLOOR : T_PAIR;
|
|
328
|
+
let kept = 0;
|
|
329
|
+
for (const hit of getNeighborsByEntryId(db, initiator.entryId, PAIR_NEIGHBOR_FETCH_K)) {
|
|
330
|
+
if (kept >= PAIR_NEIGHBOR_K)
|
|
331
|
+
break;
|
|
332
|
+
if (hit.id === initiator.entryId)
|
|
333
|
+
continue;
|
|
334
|
+
const entry = getEntryById(db, hit.id);
|
|
335
|
+
if (!entry || entry.bundleId !== bundleId)
|
|
336
|
+
continue;
|
|
337
|
+
if (entry.entry.type !== "memory" && entry.entry.type !== "knowledge" && entry.entry.type !== "lesson")
|
|
338
|
+
continue;
|
|
339
|
+
if (entry.entry.type === "knowledge" && !isFlatName(entry.entry.name))
|
|
340
|
+
continue;
|
|
341
|
+
if (!fs.existsSync(entry.filePath))
|
|
342
|
+
continue;
|
|
343
|
+
const other = {
|
|
344
|
+
ref: conceptIdFromTypeName(entry.entry.type, entry.entry.name),
|
|
345
|
+
type: entry.entry.type,
|
|
346
|
+
name: entry.entry.name,
|
|
347
|
+
filePath: entry.filePath,
|
|
348
|
+
entryId: hit.id,
|
|
349
|
+
};
|
|
350
|
+
if (other.ref === initiator.ref || isOwnTwinOrParent(initiator, other))
|
|
351
|
+
continue;
|
|
352
|
+
// distance = sqrt(2 * (1 - cosine)) (index-vec-repository.ts) — invert it back to cosine.
|
|
353
|
+
const cosine = 1 - (hit.distance * hit.distance) / 2;
|
|
354
|
+
if (cosine < floor)
|
|
355
|
+
continue;
|
|
356
|
+
const key = pairKey(initiator.ref, other.ref);
|
|
357
|
+
if (seenPairs.has(key))
|
|
358
|
+
continue;
|
|
359
|
+
seenPairs.add(key);
|
|
360
|
+
candidates.push({ initiator, other, cosine });
|
|
361
|
+
kept++;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
candidates.sort((a, b) => b.cosine - a.cosine);
|
|
365
|
+
return candidates;
|
|
366
|
+
}
|
|
367
|
+
function loadSide(asset, gitFirstAdded, stashDir) {
|
|
368
|
+
let raw;
|
|
369
|
+
try {
|
|
370
|
+
raw = fs.readFileSync(asset.filePath, "utf8");
|
|
371
|
+
}
|
|
372
|
+
catch {
|
|
373
|
+
return undefined;
|
|
374
|
+
}
|
|
375
|
+
let frontmatter;
|
|
376
|
+
try {
|
|
377
|
+
frontmatter = parseFrontmatter(raw).data;
|
|
378
|
+
}
|
|
379
|
+
catch {
|
|
380
|
+
frontmatter = {};
|
|
381
|
+
}
|
|
382
|
+
// B1: created is the git first-add time (mtime only when git does not know
|
|
383
|
+
// the file, or the bundle has none) — frontmatter createdAt/created is not
|
|
384
|
+
// consulted; too few real assets carry it to be a reliable ordering.
|
|
385
|
+
// Updated stays frontmatter `updated` when present, else falls back to created.
|
|
386
|
+
const createdIso = new Date(createdMsOf(asset, gitFirstAdded, stashDir)).toISOString();
|
|
387
|
+
return {
|
|
388
|
+
asset,
|
|
389
|
+
frontmatter,
|
|
390
|
+
raw,
|
|
391
|
+
createdIso,
|
|
392
|
+
updatedIso: asNonEmptyString(frontmatter.updated) ?? createdIso,
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
function sideSection(label, side) {
|
|
396
|
+
return [
|
|
397
|
+
`Asset ${label}:`,
|
|
398
|
+
`Ref: ${side.asset.ref}`,
|
|
399
|
+
`Type: ${side.asset.type}`,
|
|
400
|
+
`Created: ${side.createdIso}`,
|
|
401
|
+
`Updated: ${side.updatedIso}`,
|
|
402
|
+
`Description: ${asNonEmptyString(side.frontmatter.description) ?? "(none)"}`,
|
|
403
|
+
"Content:",
|
|
404
|
+
"```",
|
|
405
|
+
stripFrontmatterBody(side.raw).slice(0, PAIR_BODY_TRUNCATE_CHARS),
|
|
406
|
+
"```",
|
|
407
|
+
"",
|
|
408
|
+
];
|
|
409
|
+
}
|
|
410
|
+
/** Orders two loaded sides by date (older/newer) for the "A (older)"/"B (newer)" labelling the prompt requires. */
|
|
411
|
+
function orderByAge(x, y) {
|
|
412
|
+
const xMs = Date.parse(x.createdIso);
|
|
413
|
+
const yMs = Date.parse(y.createdIso);
|
|
414
|
+
const xIsOlder = Number.isFinite(xMs) && Number.isFinite(yMs) ? xMs <= yMs : true;
|
|
415
|
+
return xIsOlder ? { older: x, newer: y } : { older: y, newer: x };
|
|
416
|
+
}
|
|
417
|
+
/** The user message: dates decide "A (older)" / "B (newer)" (plan Appendix A), matching the calibration sample's own ordering. */
|
|
418
|
+
function buildPairUserPrompt(older, newer) {
|
|
419
|
+
return [...sideSection("A (older)", older), ...sideSection("B (newer)", newer)].join("\n");
|
|
420
|
+
}
|
|
421
|
+
/** The tombstone-vocabulary reason a judge label maps to (`supersedes` -> `superseded`; the rest unchanged). */
|
|
422
|
+
export function tombstoneReason(label) {
|
|
423
|
+
return label === "supersedes" ? "superseded" : label;
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* The calibrated outcome table (owner grades, replacing plan §5.2's
|
|
427
|
+
* "shorter body" rule): `duplicate`/`supersedes` keep the newer copy;
|
|
428
|
+
* `subsumed` keeps the side the judge did NOT name `redundant` (no proposal
|
|
429
|
+
* if that pointer is missing or invalid).
|
|
430
|
+
*/
|
|
431
|
+
export function decideRetirement(label, redundant, older, newer) {
|
|
432
|
+
if (label === "duplicate" || label === "supersedes")
|
|
433
|
+
return { retired: older, successor: newer };
|
|
434
|
+
if (label === "subsumed") {
|
|
435
|
+
if (redundant === "A")
|
|
436
|
+
return { retired: older, successor: newer };
|
|
437
|
+
if (redundant === "B")
|
|
438
|
+
return { retired: newer, successor: older };
|
|
439
|
+
return undefined; // the judge's pointer is missing or invalid — no proposal
|
|
440
|
+
}
|
|
441
|
+
return undefined;
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* One pair: judge it, then (for a retire class) apply the guards and mint
|
|
445
|
+
* the proposal. Never throws — a failure is counted in `failedJudgments` or
|
|
446
|
+
* pushed to `warnings`, never lost silently and never aborting the run.
|
|
447
|
+
*/
|
|
448
|
+
async function judgeOne(ctx, candidate) {
|
|
449
|
+
const initiatorSide = loadSide(candidate.initiator, ctx.gitFirstAdded, ctx.stashDir);
|
|
450
|
+
const otherSide = loadSide(candidate.other, ctx.gitFirstAdded, ctx.stashDir);
|
|
451
|
+
if (!initiatorSide || !otherSide)
|
|
452
|
+
return { failed: false }; // unreadable since selection — skip, not a judge failure
|
|
453
|
+
// Item 0 / S1: this exact pair (same two refs, same two content hashes,
|
|
454
|
+
// EITHER orientation) was already judged and rejected OR reverted. Checked
|
|
455
|
+
// HERE, before the judge call, not after — the judge (not yet run) is what
|
|
456
|
+
// decides which side would be "retired" this time, so both orientations
|
|
457
|
+
// are checked against the current content hashes rather than waiting for
|
|
458
|
+
// a verdict to pick one. A settled pair therefore costs no LLM call.
|
|
459
|
+
const initiatorHash = contentHash(initiatorSide.raw, "body");
|
|
460
|
+
const otherHash = contentHash(otherSide.raw, "body");
|
|
461
|
+
if (ctx.rejectedPairKeys.has(rejectedPairKey(initiatorSide.asset.ref, otherSide.asset.ref, initiatorHash, otherHash)) ||
|
|
462
|
+
ctx.rejectedPairKeys.has(rejectedPairKey(otherSide.asset.ref, initiatorSide.asset.ref, otherHash, initiatorHash))) {
|
|
463
|
+
return { failed: false };
|
|
464
|
+
}
|
|
465
|
+
const { older, newer } = orderByAge(initiatorSide, otherSide);
|
|
466
|
+
const outcome = await callStage({
|
|
467
|
+
feature: "memory_consolidation",
|
|
468
|
+
runner: ctx.llmRunner,
|
|
469
|
+
system: consolidatePairPrompt,
|
|
470
|
+
prompt: buildPairUserPrompt(older, newer),
|
|
471
|
+
gate: { config: ctx.config, enabled: true },
|
|
472
|
+
request: {
|
|
473
|
+
responseSchema: PAIR_JUDGE_JSON_SCHEMA,
|
|
474
|
+
enableThinking: false,
|
|
475
|
+
timeoutMs: ctx.llmRunner.timeoutMs,
|
|
476
|
+
signal: ctx.opts.signal,
|
|
477
|
+
...(ctx.chat ? { chat: ctx.chat } : {}),
|
|
478
|
+
},
|
|
479
|
+
...(ctx.opts.onNotices ? { onNotices: ctx.opts.onNotices } : {}),
|
|
480
|
+
});
|
|
481
|
+
if (!outcome.ok)
|
|
482
|
+
return { failed: true };
|
|
483
|
+
const verdict = parsePairJudgeResponse(outcome.raw);
|
|
484
|
+
if (!verdict)
|
|
485
|
+
return { failed: true };
|
|
486
|
+
ctx.labelCounts[verdict.relation]++;
|
|
487
|
+
if (verdict.relation === "contradicts")
|
|
488
|
+
return { failed: false }; // counted; stays human — no proposal, no belief write
|
|
489
|
+
if (!isRetireLabel(verdict.relation))
|
|
490
|
+
return { failed: false }; // overlap / unrelated: judged_no_action
|
|
491
|
+
const decision = decideRetirement(verdict.relation, verdict.redundant, older, newer);
|
|
492
|
+
if (!decision)
|
|
493
|
+
return { failed: false };
|
|
494
|
+
const { retired, successor } = decision;
|
|
495
|
+
// Guards (plan §5.2 step 4 / brief §A "Guards").
|
|
496
|
+
if (retired.asset.type === "memory" && isHotCapturedMemory(retired.asset.filePath)) {
|
|
497
|
+
return { failed: false }; // never propose retiring a captureMode: hot memory — leave the pair alone
|
|
498
|
+
}
|
|
499
|
+
if (retired.asset.type === "memory" && retired.asset.name.endsWith(DERIVED_SUFFIX)) {
|
|
500
|
+
// S3: derive the parent path from the FULL file path, not from name +
|
|
501
|
+
// dirname — for a subfolder memory (e.g. memories/sub/foo.derived) the
|
|
502
|
+
// name already carries "sub/", so joining dirname(filePath) (which ALSO
|
|
503
|
+
// ends in "sub") with it used to double the subfolder segment.
|
|
504
|
+
const parentPath = retired.asset.filePath.replace(/\.derived\.md$/, ".md");
|
|
505
|
+
if (fs.existsSync(parentPath))
|
|
506
|
+
return { failed: false }; // never retire a .derived memory whose parent still exists
|
|
507
|
+
}
|
|
508
|
+
const retiredHash = contentHash(retired.raw, "body");
|
|
509
|
+
const successorHash = contentHash(successor.raw, "body");
|
|
510
|
+
// B2: an asset retired (or already spent as a successor) earlier in this
|
|
511
|
+
// run cannot be retired or reused as a successor again — the same-run half
|
|
512
|
+
// of the chain guard (the accept-time hash/existence check is the other,
|
|
513
|
+
// durable half).
|
|
514
|
+
const retiredKey = stripBundle(retired.asset.ref);
|
|
515
|
+
const successorKey = stripBundle(successor.asset.ref);
|
|
516
|
+
// Must-fix 1 (third review round): a retire-worthy verdict the same-run
|
|
517
|
+
// chain guard drops is not a settled "no action" — a LATER run, once the
|
|
518
|
+
// conflicting retirement has been reviewed, may well mint it. Counted as
|
|
519
|
+
// failed so its initiator gets no row (real data: night 1 alone judged 177
|
|
520
|
+
// duplicate verdicts into only 59 proposals — 118 silently abandoned).
|
|
521
|
+
if (ctx.retiredThisRun.has(retiredKey) || ctx.retiredThisRun.has(successorKey))
|
|
522
|
+
return { failed: true };
|
|
523
|
+
ctx.retiredThisRun.add(retiredKey);
|
|
524
|
+
ctx.retiredThisRun.add(successorKey);
|
|
525
|
+
const reason = tombstoneReason(verdict.relation);
|
|
526
|
+
if (ctx.opts.dryRun) {
|
|
527
|
+
ctx.retired.push(`${retired.asset.ref} -> ${successor.asset.ref}`);
|
|
528
|
+
ctx.perInitiatorProposed.add(candidate.initiator.ref);
|
|
529
|
+
return { failed: false };
|
|
530
|
+
}
|
|
531
|
+
// Continuity check (plan §5.4, rule R3): replay the retired asset's own
|
|
532
|
+
// past queries and flag, but do not block, a pair where the successor
|
|
533
|
+
// would not have shown up where the retired asset did.
|
|
534
|
+
const continuityRisk = await checkRetirementContinuity({
|
|
535
|
+
stashDir: ctx.stashDir,
|
|
536
|
+
config: ctx.config,
|
|
537
|
+
retiredRef: retired.asset.ref,
|
|
538
|
+
successorRef: successor.asset.ref,
|
|
539
|
+
retiredRaw: retired.raw,
|
|
540
|
+
successorRaw: successor.raw,
|
|
541
|
+
ledgerAccess: { proposalsCtx: ctx.opts.proposalsCtx },
|
|
542
|
+
search: ctx.continuitySearch,
|
|
543
|
+
});
|
|
544
|
+
const retirement = {
|
|
545
|
+
retiredRef: retired.asset.ref,
|
|
546
|
+
successorRef: successor.asset.ref,
|
|
547
|
+
cosine: candidate.cosine,
|
|
548
|
+
judgeLabel: verdict.relation,
|
|
549
|
+
judgeReason: verdict.reason,
|
|
550
|
+
retiredContentHash: retiredHash,
|
|
551
|
+
successorContentHash: successorHash,
|
|
552
|
+
reason,
|
|
553
|
+
...(continuityRisk ? { continuityRisk } : {}),
|
|
554
|
+
};
|
|
555
|
+
try {
|
|
556
|
+
const proposal = createRetireProposal(ctx.stashDir, {
|
|
557
|
+
ref: retired.asset.ref,
|
|
558
|
+
// S6: its own generator, kept apart from the promote pass's
|
|
559
|
+
// "consolidate" proposals — `accept --generator consolidate` (bulk
|
|
560
|
+
// promotion review) never sweeps a retire proposal, and the reverse.
|
|
561
|
+
source: "consolidate-pair",
|
|
562
|
+
sourceRun: ctx.opts.sourceRun,
|
|
563
|
+
...(ctx.opts.writeTarget
|
|
564
|
+
? { target: { source: ctx.opts.writeTarget.source.name, root: ctx.opts.writeTarget.source.path } }
|
|
565
|
+
: {}),
|
|
566
|
+
confidence: verdict.confidence,
|
|
567
|
+
retirement,
|
|
568
|
+
}, ctx.opts.proposalsCtx);
|
|
569
|
+
ctx.retired.push(proposal.id);
|
|
570
|
+
ctx.perInitiatorProposed.add(candidate.initiator.ref);
|
|
571
|
+
return { failed: false };
|
|
572
|
+
}
|
|
573
|
+
catch (error) {
|
|
574
|
+
ctx.warnings.push(`Pair pass: could not mint a retire proposal for ${retired.asset.ref}: ${error instanceof Error ? error.message : String(error)}`);
|
|
575
|
+
// Must-fix 1: a mint failure is transient (a lock, a disk error, a
|
|
576
|
+
// validation hiccup) — treated the same as a same-run drop, so no row is
|
|
577
|
+
// written and the pair is retried next run instead of abandoned.
|
|
578
|
+
return { failed: true };
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
const emptyLabelCounts = () => ({
|
|
582
|
+
duplicate: 0,
|
|
583
|
+
subsumed: 0,
|
|
584
|
+
supersedes: 0,
|
|
585
|
+
contradicts: 0,
|
|
586
|
+
overlap: 0,
|
|
587
|
+
unrelated: 0,
|
|
588
|
+
});
|
|
589
|
+
/**
|
|
590
|
+
* The pair pass (alpha.9): initiators -> candidates -> one judge call per
|
|
591
|
+
* pair -> retire proposals for the calibrated classes. Runs alongside the
|
|
592
|
+
* promote pass, sharing its gate, its frozen LLM runner and its engine
|
|
593
|
+
* concurrency. `bundleId` is the target bundle's id (`undefined` skips the
|
|
594
|
+
* pass — nothing to scope candidates to).
|
|
595
|
+
*/
|
|
596
|
+
export async function runConsolidatePairPass(opts, config, stashDir, bundleId, warnings,
|
|
597
|
+
/** Test seams: a transport override for the judge call, and for the continuity check's search call. Production callers omit both. */
|
|
598
|
+
seams = {}) {
|
|
599
|
+
const empty = {
|
|
600
|
+
initiators: 0,
|
|
601
|
+
initiatorsBacklog: 0,
|
|
602
|
+
pairsJudged: 0,
|
|
603
|
+
labelCounts: emptyLabelCounts(),
|
|
604
|
+
retired: [],
|
|
605
|
+
failedJudgments: 0,
|
|
606
|
+
};
|
|
607
|
+
const llmRunner = opts.llmRunner ?? undefined;
|
|
608
|
+
if (!bundleId || !llmRunner)
|
|
609
|
+
return empty;
|
|
610
|
+
let initiators;
|
|
611
|
+
let candidates;
|
|
612
|
+
let gitFirstAdded;
|
|
613
|
+
let db;
|
|
614
|
+
try {
|
|
615
|
+
db = opts.dryRun ? openReadonlyExistingDatabase(undefined, { isolatedSnapshot: true }) : openExistingDatabase();
|
|
616
|
+
if (!db)
|
|
617
|
+
return empty;
|
|
618
|
+
gitFirstAdded = loadGitFirstAddedMap(stashDir);
|
|
619
|
+
const pool = loadPairPassPool(db, bundleId);
|
|
620
|
+
initiators = selectInitiators(pool, opts, stashDir, gitFirstAdded).initiators;
|
|
621
|
+
candidates = selectCandidates(db, initiators, bundleId);
|
|
622
|
+
}
|
|
623
|
+
catch (error) {
|
|
624
|
+
warnings.push(`Pair pass: index unavailable — skipped (${error instanceof Error ? error.message : String(error)}).`);
|
|
625
|
+
return empty;
|
|
626
|
+
}
|
|
627
|
+
finally {
|
|
628
|
+
if (db)
|
|
629
|
+
closeDatabase(db);
|
|
630
|
+
}
|
|
631
|
+
const initiatorsBacklog = initiators.filter((i) => i.backlog).length;
|
|
632
|
+
// Never judge a pair when either side already has a pending retire
|
|
633
|
+
// proposal, as the retired ref OR its successor (B2 widens this from the
|
|
634
|
+
// retired ref alone): an asset spoken for by one pending decision cannot
|
|
635
|
+
// also be judged as part of another until that decision resolves.
|
|
636
|
+
const pendingRetireRefs = new Set();
|
|
637
|
+
try {
|
|
638
|
+
for (const p of listProposalsReadOnly(stashDir, { status: "pending" })) {
|
|
639
|
+
if (!isRetireProposal(p))
|
|
640
|
+
continue;
|
|
641
|
+
pendingRetireRefs.add(stripBundle(p.ref));
|
|
642
|
+
if (p.retirement?.successorRef)
|
|
643
|
+
pendingRetireRefs.add(stripBundle(p.retirement.successorRef));
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
catch {
|
|
647
|
+
// Best-effort de-dup only; a failed read never blocks judging.
|
|
648
|
+
}
|
|
649
|
+
const isPendingBlocked = (c) => pendingRetireRefs.has(stripBundle(c.initiator.ref)) || pendingRetireRefs.has(stripBundle(c.other.ref));
|
|
650
|
+
// Item 0 / S1: every rejected OR reverted consolidate-pair retirement on
|
|
651
|
+
// record, keyed by its exact ref pair and both content hashes — read once
|
|
652
|
+
// per run, the same shape as pendingRetireRefs above. `reverted` is
|
|
653
|
+
// included alongside `rejected`: a person undoing an accept via `akm
|
|
654
|
+
// proposal revert` is the same "no, not this" signal as a reject — without
|
|
655
|
+
// it, the next run would re-mint the identical retirement, and a bulk
|
|
656
|
+
// accept could re-apply a decision the person just undid.
|
|
657
|
+
const rejectedPairKeys = new Set();
|
|
658
|
+
try {
|
|
659
|
+
for (const status of ["rejected", "reverted"]) {
|
|
660
|
+
for (const p of listProposalsReadOnly(stashDir, { status, includeArchive: true })) {
|
|
661
|
+
if (!isRetireProposal(p) || !p.retirement)
|
|
662
|
+
continue;
|
|
663
|
+
rejectedPairKeys.add(rejectedPairKey(p.retirement.retiredRef, p.retirement.successorRef, p.retirement.retiredContentHash, p.retirement.successorContentHash));
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
catch {
|
|
668
|
+
// Best-effort de-dup only; a failed read never blocks judging.
|
|
669
|
+
}
|
|
670
|
+
// Blocker 2: admit WHOLE initiators under MAX_PAIRS_PER_RUN, never
|
|
671
|
+
// individual pairs — the old flat "top 300 candidates by cosine" cap let a
|
|
672
|
+
// pending-blocked pair spend a budget slot doing nothing, and left
|
|
673
|
+
// whichever initiators landed past slot 300 partially judged forever (no
|
|
674
|
+
// row per S1's own rule, so the SAME pairs got re-judged every night with
|
|
675
|
+
// no way to ever finish; the reviewer's simulation measured 30 nights
|
|
676
|
+
// making 9,000 calls but completing only 455 distinct pairs). A group with
|
|
677
|
+
// any pending-blocked pair is skipped before it can spend any budget at
|
|
678
|
+
// all. New-or-changed initiators (T_PAIR floor) are admitted before ANY
|
|
679
|
+
// backlog initiator regardless of cosine, then backlog initiators by their
|
|
680
|
+
// own best cosine — within a tier, a later, smaller group that still fits
|
|
681
|
+
// is admitted even after an earlier, larger one did not (first-fit), so
|
|
682
|
+
// the budget is not left idle just because the next-best group overflows
|
|
683
|
+
// it. Simulated, this drains the real backlog in ~12 nights instead of
|
|
684
|
+
// never.
|
|
685
|
+
const byInitiator = new Map();
|
|
686
|
+
for (const c of candidates) {
|
|
687
|
+
const list = byInitiator.get(c.initiator.ref);
|
|
688
|
+
if (list)
|
|
689
|
+
list.push(c);
|
|
690
|
+
else
|
|
691
|
+
byInitiator.set(c.initiator.ref, [c]);
|
|
692
|
+
}
|
|
693
|
+
const isNewOrChanged = (i) => !i.backlog || i.newMaterial;
|
|
694
|
+
const groups = [...byInitiator.values()]
|
|
695
|
+
.filter((group) => !group.some(isPendingBlocked))
|
|
696
|
+
.sort((a, b) => {
|
|
697
|
+
const tierA = isNewOrChanged(a[0].initiator) ? 0 : 1;
|
|
698
|
+
const tierB = isNewOrChanged(b[0].initiator) ? 0 : 1;
|
|
699
|
+
if (tierA !== tierB)
|
|
700
|
+
return tierA - tierB;
|
|
701
|
+
return b[0].cosine - a[0].cosine; // candidates is cosine-desc, so group[0] is this initiator's best.
|
|
702
|
+
});
|
|
703
|
+
const judgeable = [];
|
|
704
|
+
for (const group of groups) {
|
|
705
|
+
if (judgeable.length + group.length > MAX_PAIRS_PER_RUN)
|
|
706
|
+
continue; // first-fit: a smaller later group may still fit.
|
|
707
|
+
judgeable.push(...group);
|
|
708
|
+
}
|
|
709
|
+
const ctx = {
|
|
710
|
+
opts,
|
|
711
|
+
config,
|
|
712
|
+
stashDir,
|
|
713
|
+
llmRunner,
|
|
714
|
+
gitFirstAdded,
|
|
715
|
+
labelCounts: emptyLabelCounts(),
|
|
716
|
+
perInitiatorProposed: new Set(),
|
|
717
|
+
retired: [],
|
|
718
|
+
warnings,
|
|
719
|
+
rejectedPairKeys,
|
|
720
|
+
retiredThisRun: new Set(),
|
|
721
|
+
...(seams.chat ? { chat: seams.chat } : {}),
|
|
722
|
+
// S2: one instance for the whole run (not one per proposal), so its
|
|
723
|
+
// "fell back once, go keyword-only from here" throttle actually covers
|
|
724
|
+
// every remaining query in this run, not just one proposal's own five.
|
|
725
|
+
continuitySearch: seams.continuitySearch ?? createContinuitySearch(stashDir, config),
|
|
726
|
+
};
|
|
727
|
+
let failedJudgments = 0;
|
|
728
|
+
// Should-fix 3: an initiator with any failed (or never-sent) judge call
|
|
729
|
+
// gets no ledger row — a failure attempted nothing conclusive, so writing
|
|
730
|
+
// one would mean the pair is never retried.
|
|
731
|
+
const failedInitiators = new Set();
|
|
732
|
+
if (judgeable.length > 0) {
|
|
733
|
+
// The promote pass validates opts.llmRunner's credentials once, but only
|
|
734
|
+
// when it has memories to dispatch — the pair pass can still have work
|
|
735
|
+
// when that pool is empty, so it validates independently before its
|
|
736
|
+
// first real dispatch. A test-injected chat seam bypasses the transport
|
|
737
|
+
// entirely and needs no credential.
|
|
738
|
+
if (!seams.chat)
|
|
739
|
+
assertRunnerCredentials(llmRunner);
|
|
740
|
+
const results = await concurrentMap(judgeable, (candidate) => judgeOne(ctx, candidate), llmRunner.connection.concurrency ?? 1, { signal: opts.signal });
|
|
741
|
+
results.forEach((r, idx) => {
|
|
742
|
+
// Must-fix 3: `concurrentMap` leaves an entry `undefined` for a call an
|
|
743
|
+
// aborted run never sent at all — `r?.failed === true` reads that as
|
|
744
|
+
// `undefined === true` (false), so an unsent call counted as a clean
|
|
745
|
+
// "no action" verdict. Explicit `undefined` check closes that gap.
|
|
746
|
+
if (r === undefined || r.failed === true) {
|
|
747
|
+
failedJudgments++;
|
|
748
|
+
failedInitiators.add(judgeable[idx].initiator.ref);
|
|
749
|
+
}
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
// S1: a ledger row is written for an initiator only once ALL of its OWN
|
|
753
|
+
// candidates were admitted (whole-initiator admission above makes this a
|
|
754
|
+
// simple membership check: judgeable either has every one of an
|
|
755
|
+
// initiator's candidates, or none of them) and none of them failed
|
|
756
|
+
// (should-fix 3) — including an initiator with zero candidates, which
|
|
757
|
+
// trivially satisfies both. One left out by the cap or a pending-proposal
|
|
758
|
+
// collision gets no row at all, so the next run reconsiders it rather
|
|
759
|
+
// than treating it as settled.
|
|
760
|
+
if (!opts.dryRun) {
|
|
761
|
+
const admittedRefs = new Set(judgeable.map((c) => c.initiator.ref));
|
|
762
|
+
const ledgerInputs = initiators
|
|
763
|
+
.filter((i) => {
|
|
764
|
+
const hasCandidates = byInitiator.has(i.ref);
|
|
765
|
+
if (!hasCandidates)
|
|
766
|
+
return true;
|
|
767
|
+
if (!admittedRefs.has(i.ref))
|
|
768
|
+
return false;
|
|
769
|
+
return !failedInitiators.has(i.ref);
|
|
770
|
+
})
|
|
771
|
+
.map((i) => ({
|
|
772
|
+
stashDir,
|
|
773
|
+
ref: i.ref,
|
|
774
|
+
source: PAIR_PASS_LEDGER_SOURCE,
|
|
775
|
+
outcome: ctx.perInitiatorProposed.has(i.ref) ? "proposed" : "judged_no_action",
|
|
776
|
+
contentHash: i.bodyHash,
|
|
777
|
+
}));
|
|
778
|
+
recordLedgerAttempt({ proposalsCtx: opts.proposalsCtx }, ledgerInputs);
|
|
779
|
+
}
|
|
780
|
+
return {
|
|
781
|
+
initiators: initiators.length,
|
|
782
|
+
initiatorsBacklog,
|
|
783
|
+
pairsConsidered: candidates.length,
|
|
784
|
+
// Must-fix 3: parsed verdicts only — judgeable.length counted pairs that
|
|
785
|
+
// were admitted, not pairs a verdict actually came back for.
|
|
786
|
+
pairsJudged: judgeable.length - failedJudgments,
|
|
787
|
+
labelCounts: ctx.labelCounts,
|
|
788
|
+
retired: ctx.retired,
|
|
789
|
+
failedJudgments,
|
|
790
|
+
};
|
|
791
|
+
}
|