akm-cli 0.9.7 → 0.9.8-beta.2
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 +310 -0
- package/dist/commands/env/env.js +2 -21
- package/dist/commands/health/checks.js +0 -25
- package/dist/commands/health/data-dir-usage.js +165 -0
- package/dist/commands/health/improve-metrics.js +8 -34
- package/dist/commands/health/windows.js +0 -4
- package/dist/commands/health.js +18 -37
- package/dist/commands/improve/consolidate/eligibility.js +11 -5
- package/dist/commands/improve/extract.js +36 -32
- package/dist/commands/improve/memory/memory-belief.js +15 -5
- package/dist/commands/improve/memory/memory-contradiction-detect.js +16 -25
- package/dist/commands/improve/memory/memory-improve.js +7 -19
- package/dist/commands/improve/preparation.js +52 -36
- package/dist/commands/improve/reflect.js +13 -17
- package/dist/commands/lint/base-linter.js +42 -2
- package/dist/commands/lint/index.js +4 -5
- package/dist/commands/migrate/config-extra-params.js +61 -0
- package/dist/commands/migrate/dead-residue.js +113 -0
- package/dist/commands/migrate/stale-txn.js +49 -0
- package/dist/commands/migrate-cli.js +42 -1
- package/dist/commands/proposal/proposal.js +1 -21
- package/dist/commands/proposal/repository.js +0 -4
- package/dist/commands/proposal/validators/proposal-quality-validators.js +0 -6
- package/dist/commands/read/curate.js +4 -18
- package/dist/commands/read/show.js +2 -1
- package/dist/commands/sources/installed-stashes.js +5 -1
- package/dist/commands/sources/self-update.js +38 -1
- package/dist/commands/sources/sources-cli.js +17 -1
- package/dist/commands/tasks/tasks.js +0 -8
- package/dist/commands/url-checker.js +20 -28
- package/dist/commands/workflow-cli.js +11 -61
- package/dist/core/adapter/adapters/agent-skills-adapter.js +2 -6
- package/dist/core/adapter/adapters/akm-adapter.js +2 -3
- package/dist/core/adapter/adapters/akm-lint.js +2 -20
- package/dist/core/adapter/adapters/akm-task-adapter.js +2 -6
- package/dist/core/adapter/adapters/akm-workflow-adapter.js +2 -6
- package/dist/core/adapter/adapters/dotenv-adapter.js +2 -22
- package/dist/core/adapter/adapters/generic-files-adapter.js +3 -9
- package/dist/core/adapter/adapters/llm-wiki-adapter.js +2 -6
- package/dist/core/adapter/adapters/okf-adapter.js +7 -18
- package/dist/core/adapter/adapters/shared.js +2 -0
- package/dist/core/adapter/adapters/tool-dir-shared.js +3 -9
- package/dist/core/adapter/adapters/website-snapshot-adapter.js +2 -6
- package/dist/core/adapter/validate-context.js +1 -3
- package/dist/core/asset/asset-placement.js +1 -3
- package/dist/core/asset/frontmatter.js +96 -0
- package/dist/core/common.js +66 -2
- package/dist/core/config/config-io.js +21 -8
- package/dist/core/config/config-walker.js +6 -10
- package/dist/core/config/config.js +11 -8
- package/dist/core/extra-params.js +17 -13
- package/dist/core/improve-result.js +1 -3
- package/dist/core/json-schema.js +9 -11
- package/dist/core/state/migrations.js +74 -2
- package/dist/core/state-db.js +2 -1
- package/dist/core/warn.js +15 -0
- package/dist/execution/executable-identity.js +1 -3
- package/dist/execution/guarded-source.js +1 -6
- package/dist/indexer/bundle-identity-guard.js +6 -1
- package/dist/indexer/db/graph-db.js +139 -154
- package/dist/indexer/ensure-index.js +11 -19
- package/dist/indexer/graph/graph-boost.js +23 -34
- package/dist/indexer/graph/graph-extraction.js +12 -2
- package/dist/indexer/indexer.js +43 -22
- package/dist/indexer/lookup/adapter-concept-owner.js +12 -9
- package/dist/indexer/passes/dir-staleness.js +30 -9
- package/dist/indexer/passes/memory-inference.js +7 -2
- package/dist/indexer/scan/drain-dir.js +2 -4
- package/dist/indexer/search/db-search.js +3 -3
- package/dist/indexer/search/fts-query.js +10 -15
- package/dist/indexer/search/search-source.js +0 -13
- package/dist/indexer/usage/usage-events.js +9 -1
- package/dist/indexer/walk/walker.js +11 -6
- package/dist/integrations/harnesses/claude/result-extractor.js +30 -15
- package/dist/integrations/harnesses/codex/result-extractor.js +43 -6
- package/dist/integrations/harnesses/copilot/result-extractor.js +39 -12
- package/dist/integrations/harnesses/gemini/result-extractor.js +40 -12
- package/dist/integrations/harnesses/index.js +0 -4
- package/dist/integrations/harnesses/openhands/result-extractor.js +51 -19
- package/dist/integrations/harnesses/pi/result-extractor.js +45 -12
- package/dist/integrations/lockfile.js +0 -14
- package/dist/integrations/session-logs/index.js +0 -81
- package/dist/llm/client.js +0 -12
- package/dist/llm/memory-infer.js +1 -3
- package/dist/llm/usage-telemetry.js +1 -3
- package/dist/registry/create-provider-registry.js +4 -0
- package/dist/registry/factory.js +4 -0
- package/dist/registry/resolve.js +23 -8
- package/dist/runtime.js +0 -12
- package/dist/scripts/akm-migrate-node.js +179 -248
- package/dist/scripts/akm-migrate.js +179 -248
- package/dist/setup/setup.js +6 -12
- package/dist/sources/providers/git-install.js +7 -2
- package/dist/sources/providers/tar-utils.js +1 -7
- package/dist/sources/snapshot-fetchers/content-extract.js +0 -17
- package/dist/storage/managed-db.js +20 -7
- package/dist/storage/repositories/events-repository.js +0 -81
- package/dist/storage/repositories/index-entries-repository.js +24 -28
- package/dist/storage/repositories/index-fts-repository.js +24 -30
- package/dist/storage/repositories/index-meta-repository.js +6 -4
- package/dist/storage/repositories/index-schema.js +16 -1
- package/dist/storage/repositories/index-utility-repository.js +0 -57
- package/dist/storage/repositories/index-vec-repository.js +25 -27
- package/dist/storage/repositories/proposals-repository.js +4 -1
- package/dist/storage/repositories/task-history-repository.js +9 -3
- package/dist/tasks/backends/launchd.js +8 -18
- package/dist/tasks/resolve-akm-bin.js +15 -0
- package/dist/tasks/run/task-history.js +21 -31
- package/dist/tasks/scheduler-sync.js +1 -6
- package/dist/tasks/source/bounded-document.js +1 -14
- package/dist/tasks/source/task-to-v3.js +10 -2
- package/dist/workflows/exec/child-workflow.js +1 -1
- package/dist/workflows/exec/native-executor.js +2 -2
- package/dist/workflows/exec/step-work.js +5 -17
- package/dist/workflows/exec/worktree.js +40 -6
- package/dist/workflows/freeze/task-bindings.js +2 -4
- package/dist/workflows/ir/compile.js +3 -14
- package/dist/workflows/ir/schema-v4.js +4 -6
- package/dist/workflows/ir/schema.js +2 -5
- package/dist/workflows/parser.js +23 -26
- package/dist/workflows/source-files.js +8 -13
- package/dist/workflows/source-ir/schema.js +1 -19
- package/docs/migration/v0.9.0-troubleshooting.md +12 -2
- package/docs/migration/v0.9.1-to-v0.9.2.md +18 -4
- package/docs/reference/cli.md +3 -1
- package/docs/reference/data-and-telemetry.md +1 -0
- package/docs/reference/tasks.md +11 -0
- package/docs/reference/workflow-schema.md +24 -0
- package/package.json +2 -9
- package/dist/workflows/source-ir/compare.js +0 -17
package/dist/commands/health.js
CHANGED
|
@@ -7,7 +7,6 @@ import { resolveStashDir } from "../core/common.js";
|
|
|
7
7
|
import { loadConfig } from "../core/config/config.js";
|
|
8
8
|
import { ConfigError, UsageError } from "../core/errors.js";
|
|
9
9
|
import { readEvents } from "../core/events.js";
|
|
10
|
-
import { listTxnJournalsTolerant, TXN_SWEEP_GRACE_MS } from "../core/fs-txn.js";
|
|
11
10
|
import { openLogsDatabase } from "../core/logs-db.js";
|
|
12
11
|
import { classifyPathAccess, describeInaccessiblePath } from "../core/path-access.js";
|
|
13
12
|
import { getConfigPath, getDataDir, getDbPath, getStateDbPathInDataDir } from "../core/paths.js";
|
|
@@ -19,6 +18,7 @@ import { queryTaskHistory } from "../storage/repositories/task-history-repositor
|
|
|
19
18
|
import { pkgVersion } from "../version.js";
|
|
20
19
|
import { collectImproveAdvisories } from "./health/advisories.js";
|
|
21
20
|
import { HEALTH_CHECKS, runHealthEngineProbes } from "./health/checks.js";
|
|
21
|
+
import { collectDataDirUsageAdvisory } from "./health/data-dir-usage.js";
|
|
22
22
|
import { buildImproveSkipSummary, computeWallTimeStats, isAgentTaskHistoryRow, roundRate, summarizeImproveCompleted, summarizeImproveRuns, taskFailureDetail, } from "./health/improve-metrics.js";
|
|
23
23
|
import { emptyLlmUsageAggregate, readLlmUsageAggregate } from "./health/llm-usage.js";
|
|
24
24
|
import { computeDegradationMetrics, computeDenominatorFixedCoverage, computeEnrichmentMintingRollup, probeStateDbRoundTrip, } from "./health/metrics.js";
|
|
@@ -150,37 +150,6 @@ function gatherTaskHistoryPhase(db, logsDb, since, stateDbPath, now) {
|
|
|
150
150
|
agentFailureRate,
|
|
151
151
|
};
|
|
152
152
|
}
|
|
153
|
-
/**
|
|
154
|
-
* Item 4: leftover durable-transaction journals under `$DATA/txn` (stranded
|
|
155
|
-
* recovery state seen twice in a real 0.9 migration) had zero health
|
|
156
|
-
* visibility. Uses the tolerant scan variant so one corrupt journal.json is
|
|
157
|
-
* counted rather than aborting the whole gather; a journal younger than
|
|
158
|
-
* {@link TXN_SWEEP_GRACE_MS} is presumed to belong to a currently-running
|
|
159
|
-
* operation and is not counted as stale. Best-effort: any unexpected error
|
|
160
|
-
* (e.g. a permissions issue under `$DATA/txn`) degrades to "no signal" rather
|
|
161
|
-
* than aborting the health report.
|
|
162
|
-
*/
|
|
163
|
-
function gatherStaleTxnJournalsPhase(now) {
|
|
164
|
-
const dir = path.join(getDataDir(), "txn");
|
|
165
|
-
try {
|
|
166
|
-
const { matches, unreadableMtimes } = listTxnJournalsTolerant(() => true);
|
|
167
|
-
const nowMs = now();
|
|
168
|
-
const staleAges = [
|
|
169
|
-
...matches.map((m) => nowMs - m.mtimeMs),
|
|
170
|
-
...unreadableMtimes.map((mtimeMs) => nowMs - mtimeMs),
|
|
171
|
-
].filter((ageMs) => ageMs >= TXN_SWEEP_GRACE_MS);
|
|
172
|
-
const staleUnreadableCount = unreadableMtimes.filter((mtimeMs) => nowMs - mtimeMs >= TXN_SWEEP_GRACE_MS).length;
|
|
173
|
-
return {
|
|
174
|
-
dir,
|
|
175
|
-
count: staleAges.length,
|
|
176
|
-
oldestAgeMs: staleAges.length > 0 ? Math.max(...staleAges) : null,
|
|
177
|
-
unreadable: staleUnreadableCount,
|
|
178
|
-
};
|
|
179
|
-
}
|
|
180
|
-
catch {
|
|
181
|
-
return { dir, count: 0, oldestAgeMs: null, unreadable: 0 };
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
153
|
/**
|
|
185
154
|
* Config fields the surfaces advisory needs. Best-effort: an unloadable
|
|
186
155
|
* config leaves the field undefined and the caller falls back to a generic
|
|
@@ -229,9 +198,10 @@ function gatherImproveSummaryPhase(db, stateDbPath, since, now) {
|
|
|
229
198
|
return { improveSummary, perRunSummaries };
|
|
230
199
|
}
|
|
231
200
|
/**
|
|
232
|
-
* The
|
|
233
|
-
*
|
|
234
|
-
* (binary-config-skew, egress-endpoints),
|
|
201
|
+
* The best-effort advisory groups beyond the health-check registry: improve
|
|
202
|
+
* advisories, the `stash-git-exposure` probe, the 08 surfaces group
|
|
203
|
+
* (binary-config-skew, egress-endpoints), `type-directory-disagreement`
|
|
204
|
+
* (#831), `data-dir-usage` (#896), and `plugin-version` (itlackey/akm#832).
|
|
235
205
|
* Order matches emission order in the returned array. A probe/filesystem
|
|
236
206
|
* failure in any try/catch must not abort the health report — each group
|
|
237
207
|
* degrades to "no advisory" independently.
|
|
@@ -283,6 +253,19 @@ function gatherAncillaryAdvisories(db, stateDbPath, since, improveSummary, optio
|
|
|
283
253
|
catch {
|
|
284
254
|
// Non-fatal.
|
|
285
255
|
}
|
|
256
|
+
// #896: report the data dir's total size and its largest top-level
|
|
257
|
+
// subdirectory, so a disk-usage blowup (e.g. unpruned migration snapshot
|
|
258
|
+
// backups, #897) is self-diagnosing instead of requiring `du` archaeology.
|
|
259
|
+
// Best-effort — an unreadable/missing data dir must not abort the health
|
|
260
|
+
// report.
|
|
261
|
+
try {
|
|
262
|
+
const dataDirUsage = collectDataDirUsageAdvisory(getDataDir());
|
|
263
|
+
if (dataDirUsage)
|
|
264
|
+
advisories.push(dataDirUsage);
|
|
265
|
+
}
|
|
266
|
+
catch {
|
|
267
|
+
// Non-fatal.
|
|
268
|
+
}
|
|
286
269
|
// itlackey/akm#832: report installed Claude Code harness plugin version(s)
|
|
287
270
|
// and warn when stale or when the plugin's own akm-cli version range no
|
|
288
271
|
// longer admits this CLI. Best-effort — no plugin installed, an unreadable
|
|
@@ -512,7 +495,6 @@ export function akmHealth(options = {}) {
|
|
|
512
495
|
try {
|
|
513
496
|
const taskHistory = gatherTaskHistoryPhase(db, logsDb, since, stateDbPath, now);
|
|
514
497
|
const { tableNames, missingTables, probe } = taskHistory;
|
|
515
|
-
const staleTxnJournals = gatherStaleTxnJournalsPhase(now);
|
|
516
498
|
const { egressConfigView } = gatherEgressConfigPhase();
|
|
517
499
|
const { improveSummary } = gatherImproveSummaryPhase(db, stateDbPath, since, now);
|
|
518
500
|
advisories.push(...gatherAncillaryAdvisories(db, stateDbPath, since, improveSummary, options, egressConfigView));
|
|
@@ -535,7 +517,6 @@ export function akmHealth(options = {}) {
|
|
|
535
517
|
stuckActiveRuns: taskHistory.stuckActiveRuns,
|
|
536
518
|
stuckActiveTasks: taskHistory.stuckActiveTasks,
|
|
537
519
|
worstTaskFailRate: taskHistory.worstTaskFailRate,
|
|
538
|
-
staleTxnJournals,
|
|
539
520
|
sessionExtraction: improveSummary.sessionExtraction,
|
|
540
521
|
autoAccept: improveSummary.autoAccept,
|
|
541
522
|
engineProbes,
|
|
@@ -18,8 +18,8 @@ export function isConsolidationEligibleMemoryName(name) {
|
|
|
18
18
|
*
|
|
19
19
|
* Reads the file once per check; consolidate runs against ~10 memories per
|
|
20
20
|
* chunk so the IO cost is trivial. Returns false on any read/parse error
|
|
21
|
-
* (fail-safe: an unparseable file is treated as
|
|
22
|
-
*
|
|
21
|
+
* (fail-safe: an unreadable or unparseable file is treated as HOT — protected
|
|
22
|
+
* — because a deletion shield must not fail open; a missing file is not-hot).
|
|
23
23
|
*
|
|
24
24
|
* Defends against four observed defect classes (see
|
|
25
25
|
* `memories/akm-improve-critical-review-2026-05-20`):
|
|
@@ -29,14 +29,20 @@ export function isConsolidationEligibleMemoryName(name) {
|
|
|
29
29
|
* - Cascade deletes (LLM uses ref:X as `contradictedBy` for ref:Y then deletes both)
|
|
30
30
|
*/
|
|
31
31
|
export function isHotCapturedMemory(filePath) {
|
|
32
|
+
if (!fs.existsSync(filePath))
|
|
33
|
+
return false;
|
|
32
34
|
try {
|
|
33
|
-
if (!fs.existsSync(filePath))
|
|
34
|
-
return false;
|
|
35
35
|
const content = fs.readFileSync(filePath, "utf8");
|
|
36
36
|
const parsed = parseFrontmatter(content);
|
|
37
37
|
return hasHotCaptureMode(parsed.data);
|
|
38
38
|
}
|
|
39
39
|
catch {
|
|
40
|
-
|
|
40
|
+
// Fail CLOSED. This predicate is a deletion shield: "hot" memories are
|
|
41
|
+
// protected from consolidate's merge/delete. Returning false on a read or
|
|
42
|
+
// parse failure marked exactly the memories we could not inspect as fair
|
|
43
|
+
// game — the one direction a protection check must never fail. A missing
|
|
44
|
+
// file stays false (nothing to protect); an unreadable one is protected
|
|
45
|
+
// until someone can actually read it.
|
|
46
|
+
return true;
|
|
41
47
|
}
|
|
42
48
|
}
|
|
@@ -632,8 +632,40 @@ function unavailableExtractionResult(args) {
|
|
|
632
632
|
...args.notices,
|
|
633
633
|
};
|
|
634
634
|
}
|
|
635
|
+
// #561 — ADDITIVE session indexing. Generate + write the session asset
|
|
636
|
+
// (`sessions/<harness>/<id>.md`). FAIL-OPEN: any failure only returns a
|
|
637
|
+
// warning; it NEVER changes the proposal/skip outcome of extract. Returns the
|
|
638
|
+
// frontmatter fields to merge into the per-session result for state-db
|
|
639
|
+
// correlation. When disabled this makes NO LLM call and writes NOTHING.
|
|
640
|
+
async function maybeWriteSessionAsset(runCtx, session) {
|
|
641
|
+
const { stashDir, lease, sessionIndexing, dryRun } = runCtx;
|
|
642
|
+
const { data } = session.gate;
|
|
643
|
+
if (!sessionIndexing.enabled || dryRun)
|
|
644
|
+
return {};
|
|
645
|
+
if (!sessionMeetsDurationGate(data, sessionIndexing.minDurationMinutes))
|
|
646
|
+
return {};
|
|
647
|
+
try {
|
|
648
|
+
const result = await writeSessionAsset(data, stashDir, (summaryData) => sessionIndexing.generate(summaryData, lease));
|
|
649
|
+
if (result.written) {
|
|
650
|
+
// Write-path indexing (itself fail-open): standalone `akm extract`
|
|
651
|
+
// (session-end hook) has no post-loop reindex to pick this file up.
|
|
652
|
+
if (result.filePath)
|
|
653
|
+
await indexWrittenAssets(stashDir, [result.filePath]);
|
|
654
|
+
return {
|
|
655
|
+
...(result.ref ? { sessionAssetRef: result.ref } : {}),
|
|
656
|
+
...(result.logPath ? { sessionLogPath: result.logPath } : {}),
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
catch (err) {
|
|
661
|
+
if (err instanceof ConfigError)
|
|
662
|
+
throw err;
|
|
663
|
+
return { warning: `session asset write failed: ${err instanceof Error ? err.message : String(err)}` };
|
|
664
|
+
}
|
|
665
|
+
return {};
|
|
666
|
+
}
|
|
635
667
|
async function processSession(runCtx, session) {
|
|
636
|
-
const { harness, stashDir, config, llmRunner, lease, onNotices, getNotices, chat, ctx, eventsCtx, sourceRun, dryRun, timeoutMs,
|
|
668
|
+
const { harness, stashDir, config, llmRunner, lease, onNotices, getNotices, chat, ctx, eventsCtx, sourceRun, dryRun, timeoutMs, signal, standardsContext, } = runCtx;
|
|
637
669
|
const { sessionRef, gate } = session;
|
|
638
670
|
const warnings = [];
|
|
639
671
|
const { data, filtered, contentHash } = gate;
|
|
@@ -645,36 +677,6 @@ async function processSession(runCtx, session) {
|
|
|
645
677
|
inlineRefs: data.inlineRefs,
|
|
646
678
|
...(standardsContext.trim() ? { standardsContext } : {}),
|
|
647
679
|
});
|
|
648
|
-
// #561 — ADDITIVE session indexing. Generate + write the session asset
|
|
649
|
-
// (`sessions/<harness>/<id>.md`). FAIL-OPEN: any failure only records a
|
|
650
|
-
// warning; it NEVER changes the proposal/skip outcome of extract. Returns the
|
|
651
|
-
// frontmatter fields to merge into the per-session result for state-db
|
|
652
|
-
// correlation. When disabled this closure makes NO LLM call and writes NOTHING.
|
|
653
|
-
const maybeWriteSessionAsset = async () => {
|
|
654
|
-
if (!sessionIndexing.enabled || dryRun)
|
|
655
|
-
return {};
|
|
656
|
-
if (!sessionMeetsDurationGate(data, sessionIndexing.minDurationMinutes))
|
|
657
|
-
return {};
|
|
658
|
-
try {
|
|
659
|
-
const result = await writeSessionAsset(data, stashDir, (summaryData) => sessionIndexing.generate(summaryData, lease));
|
|
660
|
-
if (result.written) {
|
|
661
|
-
// Write-path indexing (itself fail-open): standalone `akm extract`
|
|
662
|
-
// (session-end hook) has no post-loop reindex to pick this file up.
|
|
663
|
-
if (result.filePath)
|
|
664
|
-
await indexWrittenAssets(stashDir, [result.filePath]);
|
|
665
|
-
return {
|
|
666
|
-
...(result.ref ? { sessionAssetRef: result.ref } : {}),
|
|
667
|
-
...(result.logPath ? { sessionLogPath: result.logPath } : {}),
|
|
668
|
-
};
|
|
669
|
-
}
|
|
670
|
-
}
|
|
671
|
-
catch (err) {
|
|
672
|
-
if (err instanceof ConfigError)
|
|
673
|
-
throw err;
|
|
674
|
-
warnings.push(`session asset write failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
675
|
-
}
|
|
676
|
-
return {};
|
|
677
|
-
};
|
|
678
680
|
const extraction = await runSessionExtractionLlmCall({
|
|
679
681
|
config,
|
|
680
682
|
llmRunner,
|
|
@@ -708,7 +710,9 @@ async function processSession(runCtx, session) {
|
|
|
708
710
|
const { payload } = extraction;
|
|
709
711
|
const proposalIds = [];
|
|
710
712
|
// Provenance refs are added only after the cited session asset exists.
|
|
711
|
-
const sessionAsset = await maybeWriteSessionAsset();
|
|
713
|
+
const sessionAsset = await maybeWriteSessionAsset(runCtx, session);
|
|
714
|
+
if (sessionAsset.warning)
|
|
715
|
+
warnings.push(sessionAsset.warning);
|
|
712
716
|
if (payload.candidates.length === 0) {
|
|
713
717
|
appendEvent({
|
|
714
718
|
eventType: "extract_invoked",
|
|
@@ -52,10 +52,17 @@ function readEdgeList(value) {
|
|
|
52
52
|
* Write `contradictedBy` and `beliefState: contradicted` edges to a memory
|
|
53
53
|
* file's frontmatter (C-3 / #382).
|
|
54
54
|
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
55
|
+
* The shared primitive for APPENDING one contradiction edge. Used by
|
|
56
|
+
* `memory-contradiction-detect.ts`'s automated contradiction pass.
|
|
57
|
+
*
|
|
58
|
+
* NOT used by `persistBeliefStateTransition` in `memory-improve.ts`, and that
|
|
59
|
+
* is deliberate (#885): the SCC resolver is a state-TRANSITION writer, not an
|
|
60
|
+
* edge appender. It replaces `contradictedBy` wholesale from recomputed
|
|
61
|
+
* `currentBeliefRefs`, deletes the key when a memory transitions away from
|
|
62
|
+
* `contradicted`, and moves a memory to any target state including back to
|
|
63
|
+
* active. An append-only primitive that never weakens a demotion cannot
|
|
64
|
+
* express those, and routing it through this would break the resolver's
|
|
65
|
+
* ability to clear an edge.
|
|
59
66
|
*
|
|
60
67
|
* Idempotent: if the `contradictedByRef` is already in `contradictedBy` AND
|
|
61
68
|
* the file already carries the demotion state, the file is not rewritten. The
|
|
@@ -72,9 +79,12 @@ function readEdgeList(value) {
|
|
|
72
79
|
*
|
|
73
80
|
* @param filePath - Absolute path to the memory markdown file.
|
|
74
81
|
* @param contradictedByRef - The ref that contradicts this memory.
|
|
82
|
+
* @returns `true` when the file was rewritten, `false` when the edge and the
|
|
83
|
+
* demotion were already present (the idempotent no-op). Callers count
|
|
84
|
+
* edges written from this.
|
|
75
85
|
*/
|
|
76
86
|
export function writeContradictEdge(filePath, contradictedByRef) {
|
|
77
|
-
mutateFrontmatter(filePath, (parsed) => {
|
|
87
|
+
return mutateFrontmatter(filePath, (parsed) => {
|
|
78
88
|
const existing = readEdgeList(parsed.data.contradictedBy);
|
|
79
89
|
const currentState = parsed.data.beliefState;
|
|
80
90
|
const nextState = currentState === "archived" ? currentState : "contradicted";
|
|
@@ -17,8 +17,19 @@
|
|
|
17
17
|
* 2. For each family, enumerate candidate pairs.
|
|
18
18
|
* 3. For each pair, call the LLM to judge whether the two memories are in
|
|
19
19
|
* direct factual conflict.
|
|
20
|
-
* 4. For confirmed contradictions,
|
|
21
|
-
*
|
|
20
|
+
* 4. For confirmed contradictions, append a `contradictedBy` edge to the
|
|
21
|
+
* losing memory's frontmatter via `writeContradictEdge`
|
|
22
|
+
* (`./memory-belief.ts`).
|
|
23
|
+
*
|
|
24
|
+
* That last step used to call a private near-copy of `writeContradictEdge`
|
|
25
|
+
* living in this file. The copy had drifted (#885): it read `contradictedBy`
|
|
26
|
+
* with `Array.isArray` only, so a SCALAR edge — live data the indexer accepts
|
|
27
|
+
* and lint never flags — read as "no edges" and was overwritten out of
|
|
28
|
+
* existence; and it set `beliefState: "contradicted"` unconditionally,
|
|
29
|
+
* promoting an `archived` memory back up (archived ranks BELOW contradicted).
|
|
30
|
+
* Both behaviors had tests, but the tests exercised the shared primitive,
|
|
31
|
+
* which nothing called — so they guarded dead code while the live path
|
|
32
|
+
* carried the bugs.
|
|
22
33
|
*
|
|
23
34
|
* # LLM Feature Gate
|
|
24
35
|
*
|
|
@@ -36,12 +47,13 @@
|
|
|
36
47
|
import fs from "node:fs";
|
|
37
48
|
import path from "node:path";
|
|
38
49
|
import contradictionJudgeTemplate from "../../../assets/prompts/contradiction-judge.md" with { type: "text" };
|
|
39
|
-
import {
|
|
50
|
+
import { parseFrontmatter } from "../../../core/asset/frontmatter.js";
|
|
40
51
|
import { parseEmbeddedJsonResponse } from "../../../core/parse.js";
|
|
41
52
|
import { disposeLoweredExecutionDispatchLease } from "../../../integrations/agent/execution-lowering.js";
|
|
42
53
|
import { callStructured, preflightStructuredLlmRunner } from "../../../llm/structured-call.js";
|
|
43
54
|
import { resolveImproveLlmExecution } from "../execution.js";
|
|
44
55
|
import { isDerivedMemory, memoryIdentityRef, resolveParentRef } from "./derived-ref.js";
|
|
56
|
+
import { writeContradictEdge } from "./memory-belief.js";
|
|
45
57
|
// ── Constants ────────────────────────────────────────────────────────────────
|
|
46
58
|
/**
|
|
47
59
|
* Minimum confidence required to write a contradiction edge. Below this
|
|
@@ -89,27 +101,6 @@ function toMemoryRef(memoriesDir, filePath) {
|
|
|
89
101
|
return memoryIdentityRef(name);
|
|
90
102
|
}
|
|
91
103
|
// ── Edge writing ─────────────────────────────────────────────────────────────
|
|
92
|
-
/**
|
|
93
|
-
* Write a `contradictedBy` edge to the losing memory's frontmatter file.
|
|
94
|
-
* Preserves all existing frontmatter keys; only adds/updates `contradictedBy`
|
|
95
|
-
* and `beliefState: contradicted`.
|
|
96
|
-
*/
|
|
97
|
-
/** Returns true if the edge was newly written, false if it already existed. */
|
|
98
|
-
function writeContradictedByEdge(filePath, contradictedByRef) {
|
|
99
|
-
return mutateFrontmatter(filePath, (parsed) => {
|
|
100
|
-
const existing = Array.isArray(parsed.data.contradictedBy)
|
|
101
|
-
? parsed.data.contradictedBy
|
|
102
|
-
: [];
|
|
103
|
-
if (existing.includes(contradictedByRef))
|
|
104
|
-
return null; // Edge already written.
|
|
105
|
-
const updatedContradictedBy = [...new Set([...existing, contradictedByRef])].sort();
|
|
106
|
-
return {
|
|
107
|
-
...parsed.data,
|
|
108
|
-
contradictedBy: updatedContradictedBy,
|
|
109
|
-
beliefState: "contradicted",
|
|
110
|
-
};
|
|
111
|
-
});
|
|
112
|
-
}
|
|
113
104
|
/**
|
|
114
105
|
* Deterministically pick, for a confirmed-contradiction pair, the LOSER memory
|
|
115
106
|
* that receives the single directed `contradictedBy` edge (SCC-resolved to
|
|
@@ -284,7 +275,7 @@ export async function detectAndWriteContradictions(stashDir, config, chat, strat
|
|
|
284
275
|
// a 2-cycle that the SCC resolver refreshes back to active, erasing the
|
|
285
276
|
// contradiction every run (see pickContradictionLoser).
|
|
286
277
|
try {
|
|
287
|
-
const wrote =
|
|
278
|
+
const wrote = writeContradictEdge(loser.filePath, winnerRef);
|
|
288
279
|
result.edgesWritten += wrote ? 1 : 0;
|
|
289
280
|
}
|
|
290
281
|
catch (err) {
|
|
@@ -9,7 +9,9 @@ import { MEMORY_ARCHIVE_REL } from "../../../core/asset/memory-archive.js";
|
|
|
9
9
|
import { conceptIdFromTypeName } from "../../../core/asset/resolve-ref.js";
|
|
10
10
|
import { asNonEmptyString, groupBy, stringArray } from "../../../core/common.js";
|
|
11
11
|
import { DERIVED_SUFFIX } from "../../../core/recognition-util.js";
|
|
12
|
+
import { warn } from "../../../core/warn.js";
|
|
12
13
|
import { recordWrittenPath } from "../../../core/write-provenance.js";
|
|
14
|
+
import { walkMarkdownFiles } from "../../../indexer/walk/walker.js";
|
|
13
15
|
import { isDerivedMemory, memoryIdentityRef, parseMemoryName, resolveParentRef } from "./derived-ref.js";
|
|
14
16
|
export function analyzeMemoryCleanup(stashDir, options = {}) {
|
|
15
17
|
const records = collectDerivedMemories(stashDir, options.parentRef);
|
|
@@ -574,7 +576,11 @@ function collectDerivedMemories(stashDir, parentRefFilter) {
|
|
|
574
576
|
if (!fs.existsSync(memoriesDir))
|
|
575
577
|
return [];
|
|
576
578
|
const records = [];
|
|
577
|
-
|
|
579
|
+
const walked = walkMarkdownFiles(memoriesDir);
|
|
580
|
+
if (!walked.complete) {
|
|
581
|
+
warn(`memory improve: directory scan under ${memoriesDir} is incomplete — some derived memories may be missing`);
|
|
582
|
+
}
|
|
583
|
+
for (const filePath of walked.files) {
|
|
578
584
|
const name = toMemoryName(memoriesDir, filePath);
|
|
579
585
|
if (!name)
|
|
580
586
|
continue;
|
|
@@ -778,24 +784,6 @@ function firstNonEmpty(values) {
|
|
|
778
784
|
}
|
|
779
785
|
return undefined;
|
|
780
786
|
}
|
|
781
|
-
function* walkMarkdownFiles(root) {
|
|
782
|
-
let entries;
|
|
783
|
-
try {
|
|
784
|
-
entries = fs.readdirSync(root, { withFileTypes: true });
|
|
785
|
-
}
|
|
786
|
-
catch {
|
|
787
|
-
return;
|
|
788
|
-
}
|
|
789
|
-
for (const entry of entries) {
|
|
790
|
-
const full = path.join(root, entry.name);
|
|
791
|
-
if (entry.isDirectory()) {
|
|
792
|
-
yield* walkMarkdownFiles(full);
|
|
793
|
-
}
|
|
794
|
-
else if (entry.isFile() && entry.name.toLowerCase().endsWith(".md")) {
|
|
795
|
-
yield full;
|
|
796
|
-
}
|
|
797
|
-
}
|
|
798
|
-
}
|
|
799
787
|
function toMemoryName(memoriesDir, filePath) {
|
|
800
788
|
const rel = path.relative(memoriesDir, filePath);
|
|
801
789
|
if (!rel || rel.startsWith(".."))
|
|
@@ -789,6 +789,14 @@ export async function runImprovePreparationStage(args) {
|
|
|
789
789
|
snapshot,
|
|
790
790
|
persist: !planOnly,
|
|
791
791
|
});
|
|
792
|
+
const eligibilitySourceByRef = stampEligibilitySource({
|
|
793
|
+
scope,
|
|
794
|
+
processableRefs: gathered.processableRefs,
|
|
795
|
+
mergedRefs: gathered.mergedRefs,
|
|
796
|
+
signalFiltered: gathered.signalFiltered,
|
|
797
|
+
proactiveRefs: gathered.proactiveRefs,
|
|
798
|
+
highSalienceRefs: gathered.highSalienceRefs,
|
|
799
|
+
});
|
|
792
800
|
// Shared admission boundary for every synthetic fallback lane. Cleanup and
|
|
793
801
|
// structural validation are exclusive selectors: no later rank/replay state
|
|
794
802
|
// may re-create a candidate they removed. Keep the exact surviving objects
|
|
@@ -800,7 +808,7 @@ export async function runImprovePreparationStage(args) {
|
|
|
800
808
|
primaryStashDir,
|
|
801
809
|
eventsCtx,
|
|
802
810
|
mergedRefs: gathered.mergedRefs,
|
|
803
|
-
eligibilitySourceByRef
|
|
811
|
+
eligibilitySourceByRef,
|
|
804
812
|
feedbackSummary: gathered.feedbackSummary,
|
|
805
813
|
retrievalCounts: gathered.retrievalCounts,
|
|
806
814
|
signalFiltered: gathered.signalFiltered,
|
|
@@ -820,7 +828,7 @@ export async function runImprovePreparationStage(args) {
|
|
|
820
828
|
eventsCtx,
|
|
821
829
|
mergedRefs: scored.mergedRefs,
|
|
822
830
|
salienceMap: scored.salienceMap,
|
|
823
|
-
eligibilitySourceByRef
|
|
831
|
+
eligibilitySourceByRef,
|
|
824
832
|
distillOnlyRefs: gathered.distillOnlyRefs,
|
|
825
833
|
validationFailureRefs,
|
|
826
834
|
summary: {
|
|
@@ -1054,7 +1062,9 @@ export function buildSnapshotManifest(args) {
|
|
|
1054
1062
|
/**
|
|
1055
1063
|
* Pass: candidate-gather — the signal-delta partition, the bulk feedback
|
|
1056
1064
|
* summary, retrieval signals, the Layer-2 proactive and Layer-3 high-salience
|
|
1057
|
-
* rescue lanes, the merged candidate set
|
|
1065
|
+
* rescue lanes, and the merged candidate set. Lane attribution stamping is a
|
|
1066
|
+
* separate pass — see `stampEligibilitySource` — run by the caller once
|
|
1067
|
+
* `mergedRefs` is known.
|
|
1058
1068
|
*/
|
|
1059
1069
|
function gatherCandidates(args) {
|
|
1060
1070
|
const { scope, options, primaryStashDir, eventsCtx, improveProfile, resolvedPlan, postCleanupRefs, persist } = args;
|
|
@@ -1152,23 +1162,44 @@ function gatherCandidates(args) {
|
|
|
1152
1162
|
// that want feedback-only runs.
|
|
1153
1163
|
const signalAndRetrievalRefs = dedupeRefs([...signalFiltered, ...proactiveRefs, ...highSalienceRefs]);
|
|
1154
1164
|
const mergedRefs = scope.mode === "ref" ? processableRefs : options.requireFeedbackSignal ? signalFiltered : signalAndRetrievalRefs;
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1165
|
+
return {
|
|
1166
|
+
distillCooledRefs,
|
|
1167
|
+
preCooldownCount,
|
|
1168
|
+
distillOnlyRefs,
|
|
1169
|
+
feedbackSummary,
|
|
1170
|
+
signalFiltered,
|
|
1171
|
+
signalBearingSet,
|
|
1172
|
+
retrievalCounts,
|
|
1173
|
+
proactiveRefs,
|
|
1174
|
+
proactiveMaintenanceSummary,
|
|
1175
|
+
proactivePlan: proactive.proactivePlan,
|
|
1176
|
+
highSalienceRefs,
|
|
1177
|
+
signalAndRetrievalRefs,
|
|
1178
|
+
mergedRefs,
|
|
1179
|
+
processableRefs,
|
|
1180
|
+
};
|
|
1181
|
+
}
|
|
1182
|
+
/**
|
|
1183
|
+
* Attribution tagging: stamp each ref with the eligibility lane that selected
|
|
1184
|
+
* it. Every reflect/distill proposal must record WHICH lane chose its source
|
|
1185
|
+
* asset so downstream accept/reject/revert/retrieval outcomes can be sliced by
|
|
1186
|
+
* lane (does the PROACTIVE lane produce value vs the reactive lanes?). We
|
|
1187
|
+
* build the lane map here — the one place all three lanes are known — and
|
|
1188
|
+
* stamp it onto each ImproveEligibleRef object. Because the ref objects are
|
|
1189
|
+
* shared by reference across buckets, the stamp travels with the ref through
|
|
1190
|
+
* the sort, disk-check, and loop stages down to the reflect/distill event
|
|
1191
|
+
* emit sites and createProposal calls. See EligibilitySource for the lane
|
|
1192
|
+
* vocabulary.
|
|
1193
|
+
*
|
|
1194
|
+
* Precedence (prefer the most specific reactive signal):
|
|
1195
|
+
* scope > signal-delta > proactive > high-salience
|
|
1196
|
+
* A ref with real feedback is attributed to feedback even if it was also due
|
|
1197
|
+
* for proactive maintenance or had high encoding salience. We apply lanes
|
|
1198
|
+
* weakest-first so the strongest overwrites; the explicit --scope <ref> bypass
|
|
1199
|
+
* wins outright (user intent).
|
|
1200
|
+
*/
|
|
1201
|
+
function stampEligibilitySource(args) {
|
|
1202
|
+
const { scope, processableRefs, mergedRefs, signalFiltered, proactiveRefs, highSalienceRefs } = args;
|
|
1172
1203
|
const eligibilitySourceByRef = new Map();
|
|
1173
1204
|
for (const r of highSalienceRefs)
|
|
1174
1205
|
eligibilitySourceByRef.set(r.ref, "high-salience");
|
|
@@ -1188,22 +1219,7 @@ function gatherCandidates(args) {
|
|
|
1188
1219
|
// mergedRefs is always a subset of the four lanes above).
|
|
1189
1220
|
r.eligibilitySource = eligibilitySourceByRef.get(r.ref) ?? "unknown";
|
|
1190
1221
|
}
|
|
1191
|
-
return
|
|
1192
|
-
distillCooledRefs,
|
|
1193
|
-
preCooldownCount,
|
|
1194
|
-
distillOnlyRefs,
|
|
1195
|
-
feedbackSummary,
|
|
1196
|
-
signalFiltered,
|
|
1197
|
-
signalBearingSet,
|
|
1198
|
-
retrievalCounts,
|
|
1199
|
-
proactiveRefs,
|
|
1200
|
-
proactiveMaintenanceSummary,
|
|
1201
|
-
proactivePlan: proactive.proactivePlan,
|
|
1202
|
-
highSalienceRefs,
|
|
1203
|
-
signalAndRetrievalRefs,
|
|
1204
|
-
mergedRefs,
|
|
1205
|
-
eligibilitySourceByRef,
|
|
1206
|
-
};
|
|
1222
|
+
return eligibilitySourceByRef;
|
|
1207
1223
|
}
|
|
1208
1224
|
/**
|
|
1209
1225
|
* The signal-delta partition of postCleanupRefs into the four buckets (pass:
|
|
@@ -133,27 +133,23 @@ export const REFLECT_ALLOWED_TYPES = new Set([
|
|
|
133
133
|
const PROTECTED_FRONTMATTER_FIELDS = new Set(["name", "ref", "id", "slug", "type"]);
|
|
134
134
|
/**
|
|
135
135
|
* Read the last 1–3 archived rejected proposals for a given ref from the
|
|
136
|
-
* proposal store.
|
|
137
|
-
* or the ref is undefined
|
|
138
|
-
*
|
|
139
|
-
*
|
|
136
|
+
* proposal store. Returns `[]` when the proposals store is absent (not yet
|
|
137
|
+
* created) or the ref is undefined — `listProposalsReadOnly` already handles
|
|
138
|
+
* that case; a genuine read failure propagates instead of being swallowed,
|
|
139
|
+
* since silently dropping this Reflexion-style context risks re-proposing
|
|
140
|
+
* content that was already rejected (arXiv:2303.11366).
|
|
140
141
|
*/
|
|
141
142
|
function readRejectedProposals(stash, ref, proposalsCtx) {
|
|
142
143
|
if (!ref)
|
|
143
144
|
return [];
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
}));
|
|
153
|
-
}
|
|
154
|
-
catch {
|
|
155
|
-
return [];
|
|
156
|
-
}
|
|
145
|
+
return listProposalsReadOnly(stash, { ref, status: "rejected", includeArchive: true }, proposalsCtx)
|
|
146
|
+
.sort((a, b) => new Date(b.updatedAt ?? 0).getTime() - new Date(a.updatedAt ?? 0).getTime())
|
|
147
|
+
.slice(0, MAX_REJECTED_PROPOSALS)
|
|
148
|
+
.map((p) => ({
|
|
149
|
+
ref: p.ref,
|
|
150
|
+
reason: p.review?.reason ?? "no reason given",
|
|
151
|
+
contentPreview: proposalContent(p).slice(0, 500),
|
|
152
|
+
}));
|
|
157
153
|
}
|
|
158
154
|
/**
|
|
159
155
|
* Synthesize a tmp draft-file path for the agent/sdk file-write contract.
|
|
@@ -36,10 +36,10 @@ import path from "node:path";
|
|
|
36
36
|
import { isScalar, parseDocument } from "yaml";
|
|
37
37
|
import { assetPathCandidatesForName, assetPathForName, stashDirFor } from "../../core/asset/asset-placement.js";
|
|
38
38
|
import { BUNDLE_REF_RE } from "../../core/asset/asset-ref.js";
|
|
39
|
-
import { removeFrontmatterListValues, spliceFrontmatterLine } from "../../core/asset/frontmatter.js";
|
|
39
|
+
import { removeFrontmatterListValues, rewriteFrontmatterListValue, spliceFrontmatterLine, } from "../../core/asset/frontmatter.js";
|
|
40
40
|
import { checkUnquotedDescriptionColon } from "../../core/asset/frontmatter-lint.js";
|
|
41
41
|
import { isArchivedRelPath } from "../../core/asset/memory-archive.js";
|
|
42
|
-
import { typeNameFromConceptId } from "../../core/asset/resolve-ref.js";
|
|
42
|
+
import { conceptIdFromTypeName, typeNameFromConceptId } from "../../core/asset/resolve-ref.js";
|
|
43
43
|
import { localDateStamp } from "../../core/common.js";
|
|
44
44
|
import { findFenceRegions } from "./markdown-insertion.js";
|
|
45
45
|
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
@@ -786,6 +786,46 @@ function runMissingRefChecks(ctx, raw, shouldRun, pendingFixes) {
|
|
|
786
786
|
const values = readRefStringOrArray(ctx.data?.[key]);
|
|
787
787
|
if (values === null)
|
|
788
788
|
continue;
|
|
789
|
+
// `lint --fix` migration for the retired `type:slug` xref grammar
|
|
790
|
+
// (see legacyTypeSlugParts): rewrite a value to its conceptId form
|
|
791
|
+
// ONLY when the rewritten spelling still resolves to a real asset —
|
|
792
|
+
// a dangling legacy ref must stay reported as `missing-ref` in its
|
|
793
|
+
// original spelling, never rewritten into a differently-spelled
|
|
794
|
+
// dangling ref. Runs before the missing-ref scan below so a value
|
|
795
|
+
// this loop rewrites is checked (and reported, if still dangling)
|
|
796
|
+
// under its ORIGINAL spelling by that scan, and never double-fixed.
|
|
797
|
+
if (ctx.fix) {
|
|
798
|
+
const legacyRewrites = new Map();
|
|
799
|
+
for (const raw of values) {
|
|
800
|
+
const value = raw.trim();
|
|
801
|
+
const parts = legacyTypeSlugParts(value);
|
|
802
|
+
if (parts === undefined)
|
|
803
|
+
continue;
|
|
804
|
+
const relPath = refToRelPath(parts.type, parts.name);
|
|
805
|
+
if (relPath === null)
|
|
806
|
+
continue;
|
|
807
|
+
if (!refExistsInAnyStash(relPath, parts.type, parts.name, [ctx.stashRoot, ...(ctx.extraStashRoots ?? [])]))
|
|
808
|
+
continue; // dangling — leave reported as missing-ref, unrewritten
|
|
809
|
+
legacyRewrites.set(value, conceptIdFromTypeName(parts.type, parts.name));
|
|
810
|
+
}
|
|
811
|
+
if (legacyRewrites.size > 0) {
|
|
812
|
+
const rewritten = rewriteFrontmatterListValue(currentRaw, key, legacyRewrites);
|
|
813
|
+
if (rewritten !== null) {
|
|
814
|
+
currentRaw = rewritten;
|
|
815
|
+
modified = true;
|
|
816
|
+
for (const [oldValue, newValue] of legacyRewrites) {
|
|
817
|
+
const issue = {
|
|
818
|
+
file: ctx.relPath,
|
|
819
|
+
issue: "missing-ref",
|
|
820
|
+
detail: `legacy xref grammar migrated: ${key} ${oldValue} -> ${newValue}`,
|
|
821
|
+
fixed: true,
|
|
822
|
+
};
|
|
823
|
+
issues.push(issue);
|
|
824
|
+
pendingFixes.push(issue);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
}
|
|
789
829
|
const missingXrefs = checkMissingRefsInList(values, ctx.stashRoot, ctx.extraStashRoots);
|
|
790
830
|
// #884 opt-in repair. Scoped to the BELIEF channels only: an edge whose
|
|
791
831
|
// target has neither a file nor a prune tombstone asserts a
|