akm-cli 0.9.6 → 0.9.8-beta.1

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.
Files changed (135) hide show
  1. package/CHANGELOG.md +408 -0
  2. package/dist/assets/hints/cli-hints-full.md +3 -3
  3. package/dist/assets/improve-strategies/catchup.json +40 -11
  4. package/dist/assets/improve-strategies/thorough.json +45 -7
  5. package/dist/assets/tasks/improve/akm-improve-frequent.yml +2 -2
  6. package/dist/commands/agent/contribute-cli.js +11 -0
  7. package/dist/commands/env/env.js +2 -21
  8. package/dist/commands/health/checks.js +0 -25
  9. package/dist/commands/health/improve-metrics.js +8 -34
  10. package/dist/commands/health/windows.js +0 -4
  11. package/dist/commands/health.js +1 -35
  12. package/dist/commands/improve/consolidate/eligibility.js +11 -5
  13. package/dist/commands/improve/extract.js +36 -32
  14. package/dist/commands/improve/improve-cli.js +1 -1
  15. package/dist/commands/improve/improve-strategies.js +0 -4
  16. package/dist/commands/improve/memory/memory-belief.js +15 -5
  17. package/dist/commands/improve/memory/memory-contradiction-detect.js +16 -25
  18. package/dist/commands/improve/memory/memory-improve.js +9 -20
  19. package/dist/commands/improve/preparation.js +53 -37
  20. package/dist/commands/improve/reflect.js +14 -18
  21. package/dist/commands/lint/base-linter.js +182 -19
  22. package/dist/commands/lint/index.js +21 -9
  23. package/dist/commands/migrate/config-extra-params.js +61 -0
  24. package/dist/commands/migrate/dead-residue.js +113 -0
  25. package/dist/commands/migrate/stale-txn.js +49 -0
  26. package/dist/commands/migrate-cli.js +42 -1
  27. package/dist/commands/proposal/proposal.js +1 -21
  28. package/dist/commands/proposal/repository.js +0 -4
  29. package/dist/commands/proposal/validators/proposal-quality-validators.js +0 -6
  30. package/dist/commands/read/curate.js +51 -18
  31. package/dist/commands/read/search-cli.js +24 -1
  32. package/dist/commands/read/show.js +2 -1
  33. package/dist/commands/sources/installed-stashes.js +5 -1
  34. package/dist/commands/sources/self-update.js +38 -1
  35. package/dist/commands/sources/sources-cli.js +17 -1
  36. package/dist/commands/tasks/tasks.js +0 -8
  37. package/dist/commands/url-checker.js +20 -28
  38. package/dist/core/adapter/adapters/agent-skills-adapter.js +2 -6
  39. package/dist/core/adapter/adapters/akm-adapter.js +2 -3
  40. package/dist/core/adapter/adapters/akm-lint.js +2 -20
  41. package/dist/core/adapter/adapters/akm-task-adapter.js +2 -6
  42. package/dist/core/adapter/adapters/akm-workflow-adapter.js +2 -6
  43. package/dist/core/adapter/adapters/dotenv-adapter.js +2 -22
  44. package/dist/core/adapter/adapters/generic-files-adapter.js +3 -9
  45. package/dist/core/adapter/adapters/llm-wiki-adapter.js +2 -6
  46. package/dist/core/adapter/adapters/okf-adapter.js +7 -18
  47. package/dist/core/adapter/adapters/shared.js +2 -0
  48. package/dist/core/adapter/adapters/tool-dir-shared.js +3 -9
  49. package/dist/core/adapter/adapters/website-snapshot-adapter.js +2 -6
  50. package/dist/core/adapter/validate-context.js +1 -3
  51. package/dist/core/asset/asset-placement.js +14 -5
  52. package/dist/core/asset/frontmatter.js +212 -0
  53. package/dist/core/asset/memory-archive.js +97 -0
  54. package/dist/core/common.js +66 -2
  55. package/dist/core/config/config-walker.js +6 -10
  56. package/dist/core/config/config.js +11 -8
  57. package/dist/core/config/engine-semantics.js +0 -2
  58. package/dist/core/extra-params.js +17 -13
  59. package/dist/core/improve-result.js +1 -3
  60. package/dist/core/json-schema.js +9 -11
  61. package/dist/core/state/migrations.js +52 -2
  62. package/dist/core/state-db.js +2 -1
  63. package/dist/execution/executable-identity.js +1 -3
  64. package/dist/execution/guarded-source.js +1 -6
  65. package/dist/indexer/bundle-identity-guard.js +6 -1
  66. package/dist/indexer/db/graph-db.js +139 -154
  67. package/dist/indexer/ensure-index.js +11 -19
  68. package/dist/indexer/graph/graph-boost.js +23 -34
  69. package/dist/indexer/graph/graph-extraction.js +12 -2
  70. package/dist/indexer/indexer.js +1 -1
  71. package/dist/indexer/lookup/adapter-concept-owner.js +12 -9
  72. package/dist/indexer/passes/memory-inference.js +7 -2
  73. package/dist/indexer/scan/drain-dir.js +2 -4
  74. package/dist/indexer/search/db-search.js +3 -3
  75. package/dist/indexer/search/fts-query.js +10 -15
  76. package/dist/indexer/search/search-source.js +0 -13
  77. package/dist/indexer/usage/usage-events.js +9 -1
  78. package/dist/indexer/walk/walker.js +11 -6
  79. package/dist/integrations/harnesses/claude/result-extractor.js +30 -15
  80. package/dist/integrations/harnesses/codex/result-extractor.js +43 -6
  81. package/dist/integrations/harnesses/copilot/result-extractor.js +39 -12
  82. package/dist/integrations/harnesses/gemini/result-extractor.js +40 -12
  83. package/dist/integrations/harnesses/index.js +0 -4
  84. package/dist/integrations/harnesses/openhands/result-extractor.js +51 -19
  85. package/dist/integrations/harnesses/pi/result-extractor.js +45 -12
  86. package/dist/integrations/lockfile.js +0 -14
  87. package/dist/integrations/session-logs/index.js +0 -81
  88. package/dist/llm/client.js +0 -12
  89. package/dist/llm/memory-infer.js +1 -3
  90. package/dist/llm/usage-telemetry.js +1 -3
  91. package/dist/registry/create-provider-registry.js +4 -0
  92. package/dist/registry/factory.js +4 -0
  93. package/dist/registry/resolve.js +23 -8
  94. package/dist/runtime.js +0 -12
  95. package/dist/scripts/akm-migrate-node.js +155 -251
  96. package/dist/scripts/akm-migrate.js +155 -251
  97. package/dist/setup/setup.js +6 -12
  98. package/dist/sources/providers/git-install.js +7 -2
  99. package/dist/sources/providers/tar-utils.js +1 -7
  100. package/dist/sources/snapshot-fetchers/content-extract.js +0 -17
  101. package/dist/sources/snapshot-fetchers/website-ingest.js +126 -0
  102. package/dist/storage/managed-db.js +20 -7
  103. package/dist/storage/repositories/events-repository.js +0 -81
  104. package/dist/storage/repositories/index-connection.js +45 -3
  105. package/dist/storage/repositories/index-entries-repository.js +0 -17
  106. package/dist/storage/repositories/index-fts-repository.js +24 -30
  107. package/dist/storage/repositories/index-utility-repository.js +0 -57
  108. package/dist/storage/repositories/index-vec-repository.js +25 -27
  109. package/dist/storage/repositories/task-history-repository.js +9 -3
  110. package/dist/tasks/backends/cron.js +49 -9
  111. package/dist/tasks/backends/launchd.js +8 -18
  112. package/dist/tasks/resolve-akm-bin.js +17 -2
  113. package/dist/tasks/run/task-history.js +21 -31
  114. package/dist/tasks/scheduler-invocation.js +8 -1
  115. package/dist/tasks/scheduler-sync.js +1 -6
  116. package/dist/tasks/source/bounded-document.js +1 -14
  117. package/dist/tasks/source/parse-task-source.js +23 -9
  118. package/dist/workflows/exec/child-workflow.js +1 -1
  119. package/dist/workflows/exec/native-executor.js +2 -2
  120. package/dist/workflows/exec/step-work.js +5 -17
  121. package/dist/workflows/exec/worktree.js +40 -6
  122. package/dist/workflows/freeze/task-bindings.js +2 -4
  123. package/dist/workflows/ir/compile.js +3 -14
  124. package/dist/workflows/ir/schema-v4.js +4 -6
  125. package/dist/workflows/ir/schema.js +2 -5
  126. package/dist/workflows/parser.js +23 -26
  127. package/dist/workflows/source-files.js +8 -13
  128. package/dist/workflows/source-ir/schema.js +1 -19
  129. package/docs/migration/v0.9.0-troubleshooting.md +12 -2
  130. package/docs/reference/cli.md +7 -1
  131. package/docs/reference/workflow-schema.md +24 -0
  132. package/package.json +2 -5
  133. package/dist/assets/improve-strategies/frequent.json +0 -15
  134. package/dist/assets/improve-strategies/memory-focus.json +0 -15
  135. package/dist/workflows/source-ir/compare.js +0 -17
@@ -33,42 +33,16 @@ export function taskFailureDetail(row) {
33
33
  }
34
34
  }
35
35
  /**
36
- * D8 read-boundary predicate (spec docs/plans/specs/p1b-model-extraction.md
37
- * §5.3) for `akm health`'s `agentFailureRate`: true for a `task_history` row
38
- * that represents a prepared command (agent/LLM) result, across both
39
- * vocabularies. Mirrors src/tasks/run/task-history.ts's
40
- * `taskHistoryRowToResult` read mapping:
41
- * - NEW rows mark themselves with metadata `targetVocab: 2` and store
42
- * `target_kind: "command"` for the agent/LLM arm.
43
- * - LEGACY rows (no marker, written before P1b's F-2 re-code) stored
44
- * `target_kind: "prompt"` for the same arm — and `"command"` for the
45
- * UNRELATED native shell/script arm, which must NOT be counted here.
46
- * So an unmarked `"command"` row is a legacy shell/script run, not an
47
- * agent/LLM one; a marked `"command"` row (or an unmarked `"prompt"` row) is.
36
+ * `akm health`'s `agentFailureRate` predicate: true for a `task_history` row
37
+ * that represents a prepared command (agent/LLM) result. `target_kind` is
38
+ * read in the current (post-D8) vocabulary the
39
+ * `025-task-history-vocabulary-backfill` state migration rewrites every
40
+ * legacy-vocabulary row (which stored the agent/LLM arm as `"prompt"` and
41
+ * the native shell/script arm as `"command"`) before this ever runs against
42
+ * it, so a `"command"` row here is unambiguously the agent/LLM arm.
48
43
  */
49
44
  export function isAgentTaskHistoryRow(row) {
50
- // Check target_kind BEFORE decoding metadata: some rows in the wild
51
- // (e.g. improve-pipeline task_history rows, target_kind "improve") carry
52
- // metadata_json that predates the metadataVersion:2 shape entirely, and
53
- // decodeTaskHistoryMetadata throws on that — exactly like the pre-fix
54
- // `target_kind === "prompt"` filter, which never called it for a row this
55
- // function isn't going to count anyway. Only decode for the two target
56
- // kinds this predicate can return true for.
57
- if (row.target_kind !== "command" && row.target_kind !== "prompt")
58
- return false;
59
- // An undecodable metadata_json is by definition unmarked: pre-P1b rows can
60
- // carry shapes decodeTaskHistoryMetadata rejects, and `akm health` must
61
- // classify them as legacy rather than throw (round-3 review advisory).
62
- let marked = false;
63
- try {
64
- marked = decodeTaskHistoryMetadata(row.metadata_json).targetVocab === 2;
65
- }
66
- catch {
67
- marked = false;
68
- }
69
- if (row.target_kind === "command")
70
- return marked;
71
- return !marked;
45
+ return row.target_kind === "command";
72
46
  }
73
47
  function createUnknownImproveMetrics() {
74
48
  return {
@@ -148,10 +148,6 @@ export function buildWindowMetrics(db, stateDbPath, since, until, now = () => Da
148
148
  const failedTaskRows = taskRows.filter((row) => row.status === "failed");
149
149
  const activeRows = taskRows.filter((row) => row.status === "active" && row.completed_at === null);
150
150
  const stuckActiveRuns = activeRows.filter((row) => now() - new Date(row.started_at).getTime() > ACTIVE_RUN_WARN_MS).length;
151
- // D8 (spec §5.3): a marked "command" row or a legacy (unmarked) "prompt"
152
- // row is the agent/LLM arm; an unmarked "command" row is the legacy
153
- // native shell/script arm and must not be counted here (see
154
- // isAgentTaskHistoryRow's header comment for the full mapping).
155
151
  const agentRows = taskRows.filter((row) => isAgentTaskHistoryRow(row));
156
152
  const agentFailures = agentRows.filter((row) => {
157
153
  const detail = taskFailureDetail(row);
@@ -7,10 +7,9 @@ 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
- import { getConfigPath, getDataDir, getDbPath, getStateDbPathInDataDir } from "../core/paths.js";
12
+ import { getConfigPath, getDbPath, getStateDbPathInDataDir } from "../core/paths.js";
14
13
  import { listExistingTableNames, openStateDatabase } from "../core/state-db.js";
15
14
  import { DURATION_UNITS, parseDuration, parseSinceToIso } from "../core/time.js";
16
15
  import { closeDatabase, openReadonlyExistingDatabase } from "../storage/repositories/index-connection.js";
@@ -150,37 +149,6 @@ function gatherTaskHistoryPhase(db, logsDb, since, stateDbPath, now) {
150
149
  agentFailureRate,
151
150
  };
152
151
  }
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
152
  /**
185
153
  * Config fields the surfaces advisory needs. Best-effort: an unloadable
186
154
  * config leaves the field undefined and the caller falls back to a generic
@@ -512,7 +480,6 @@ export function akmHealth(options = {}) {
512
480
  try {
513
481
  const taskHistory = gatherTaskHistoryPhase(db, logsDb, since, stateDbPath, now);
514
482
  const { tableNames, missingTables, probe } = taskHistory;
515
- const staleTxnJournals = gatherStaleTxnJournalsPhase(now);
516
483
  const { egressConfigView } = gatherEgressConfigPhase();
517
484
  const { improveSummary } = gatherImproveSummaryPhase(db, stateDbPath, since, now);
518
485
  advisories.push(...gatherAncillaryAdvisories(db, stateDbPath, since, improveSummary, options, egressConfigView));
@@ -535,7 +502,6 @@ export function akmHealth(options = {}) {
535
502
  stuckActiveRuns: taskHistory.stuckActiveRuns,
536
503
  stuckActiveTasks: taskHistory.stuckActiveTasks,
537
504
  worstTaskFailRate: taskHistory.worstTaskFailRate,
538
- staleTxnJournals,
539
505
  sessionExtraction: improveSummary.sessionExtraction,
540
506
  autoAccept: improveSummary.autoAccept,
541
507
  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 not-hot, but the broader
22
- * consolidate flow already guards against unparseable memories elsewhere).
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
- return false;
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, sessionIndexing, signal, standardsContext, } = runCtx;
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",
@@ -114,7 +114,7 @@ export const improveCommand = defineCommand({
114
114
  },
115
115
  strategy: {
116
116
  type: "string",
117
- description: "Named improve strategy from improve.strategies or built-in strategies (catchup, consolidate, default, frequent, graph-refresh, memory-focus, proactive-maintenance, quick, reflect-distill, thorough). Controls which sub-processes run and which asset types are processed.",
117
+ description: "Named improve strategy from improve.strategies or built-in strategies (catchup, consolidate, default, graph-refresh, proactive-maintenance, quick, reflect-distill, thorough). Controls which sub-processes run and which asset types are processed.",
118
118
  },
119
119
  sync: {
120
120
  type: "boolean",
@@ -4,9 +4,7 @@
4
4
  import catchup from "../../assets/improve-strategies/catchup.json" with { type: "json" };
5
5
  import consolidate from "../../assets/improve-strategies/consolidate.json" with { type: "json" };
6
6
  import defaultStrategy from "../../assets/improve-strategies/default.json" with { type: "json" };
7
- import frequent from "../../assets/improve-strategies/frequent.json" with { type: "json" };
8
7
  import graphRefresh from "../../assets/improve-strategies/graph-refresh.json" with { type: "json" };
9
- import memoryFocus from "../../assets/improve-strategies/memory-focus.json" with { type: "json" };
10
8
  import proactiveMaintenance from "../../assets/improve-strategies/proactive-maintenance.json" with { type: "json" };
11
9
  import quick from "../../assets/improve-strategies/quick.json" with { type: "json" };
12
10
  import reflectDistill from "../../assets/improve-strategies/reflect-distill.json" with { type: "json" };
@@ -45,9 +43,7 @@ const BUILTIN_STRATEGIES = {
45
43
  default: defaultStrategy,
46
44
  quick,
47
45
  thorough,
48
- "memory-focus": memoryFocus,
49
46
  "graph-refresh": graphRefresh,
50
- frequent,
51
47
  consolidate,
52
48
  catchup,
53
49
  "reflect-distill": reflectDistill,
@@ -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
- * This is the shared primitive used by:
56
- * - `akmConsolidate` when its LLM plan includes a `contradict` op
57
- * - `memory-contradiction-detect.ts` for the M-1 automated contradiction pass
58
- * - `resolveFamilyContradictions` in `memory-improve.ts` for SCC resolution
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, write `contradictedBy` edges directly to
21
- * the losing memory's frontmatter (same mechanism as `persistBeliefStateTransition`).
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 { mutateFrontmatter, parseFrontmatter } from "../../../core/asset/frontmatter.js";
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 = writeContradictedByEdge(loser.filePath, winnerRef);
278
+ const wrote = writeContradictEdge(loser.filePath, winnerRef);
288
279
  result.edgesWritten += wrote ? 1 : 0;
289
280
  }
290
281
  catch (err) {
@@ -5,10 +5,13 @@ import fs from "node:fs";
5
5
  import path from "node:path";
6
6
  import { assembleAsset } from "../../../core/asset/asset-serialize.js";
7
7
  import { mutateFrontmatter, parseFrontmatter } from "../../../core/asset/frontmatter.js";
8
+ import { MEMORY_ARCHIVE_REL } from "../../../core/asset/memory-archive.js";
8
9
  import { conceptIdFromTypeName } from "../../../core/asset/resolve-ref.js";
9
10
  import { asNonEmptyString, groupBy, stringArray } from "../../../core/common.js";
10
11
  import { DERIVED_SUFFIX } from "../../../core/recognition-util.js";
12
+ import { warn } from "../../../core/warn.js";
11
13
  import { recordWrittenPath } from "../../../core/write-provenance.js";
14
+ import { walkMarkdownFiles } from "../../../indexer/walk/walker.js";
12
15
  import { isDerivedMemory, memoryIdentityRef, parseMemoryName, resolveParentRef } from "./derived-ref.js";
13
16
  export function analyzeMemoryCleanup(stashDir, options = {}) {
14
17
  const records = collectDerivedMemories(stashDir, options.parentRef);
@@ -553,7 +556,7 @@ function priorBeliefStateForArchive(candidate) {
553
556
  }
554
557
  function createArchiveDir(stashDir, ref, archivedAt) {
555
558
  const baseName = `${archivedAt.replace(/[:.]/g, "-")}-${sanitizeRef(ref)}`;
556
- const root = path.join(stashDir, ".akm", "memory-cleanup", "archive");
559
+ const root = path.join(stashDir, MEMORY_ARCHIVE_REL);
557
560
  fs.mkdirSync(root, { recursive: true });
558
561
  let attempt = 0;
559
562
  while (true) {
@@ -573,7 +576,11 @@ function collectDerivedMemories(stashDir, parentRefFilter) {
573
576
  if (!fs.existsSync(memoriesDir))
574
577
  return [];
575
578
  const records = [];
576
- for (const filePath of walkMarkdownFiles(memoriesDir)) {
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) {
577
584
  const name = toMemoryName(memoriesDir, filePath);
578
585
  if (!name)
579
586
  continue;
@@ -777,24 +784,6 @@ function firstNonEmpty(values) {
777
784
  }
778
785
  return undefined;
779
786
  }
780
- function* walkMarkdownFiles(root) {
781
- let entries;
782
- try {
783
- entries = fs.readdirSync(root, { withFileTypes: true });
784
- }
785
- catch {
786
- return;
787
- }
788
- for (const entry of entries) {
789
- const full = path.join(root, entry.name);
790
- if (entry.isDirectory()) {
791
- yield* walkMarkdownFiles(full);
792
- }
793
- else if (entry.isFile() && entry.name.toLowerCase().endsWith(".md")) {
794
- yield full;
795
- }
796
- }
797
- }
798
787
  function toMemoryName(memoriesDir, filePath) {
799
788
  const rel = path.relative(memoriesDir, filePath);
800
789
  if (!rel || rel.startsWith(".."))
@@ -760,7 +760,7 @@ async function runPreparationPrelude(args) {
760
760
  };
761
761
  }
762
762
  export async function runImprovePreparationStage(args) {
763
- const { scope, options, plannedRefs, primaryStashDir, eventsCtx, initialCleanupWarnings, improveProfile, resolvedPlan, planOnly = options.dryRun === true, } = args;
763
+ const { scope, options, primaryStashDir, eventsCtx, initialCleanupWarnings, improveProfile, resolvedPlan, planOnly = options.dryRun === true, } = args;
764
764
  const actions = [];
765
765
  const cleanupWarnings = initialCleanupWarnings ? [...initialCleanupWarnings] : [];
766
766
  const { memoryIndexHealth, consolidationPass, extractPlan, extractResults, appliedCleanup, postCleanupRefs, cleanupGate, validationFailures, validationFailureRefs, schemaRepairs, } = await runPreparationPrelude({ ...args, planOnly, actions, cleanupWarnings });
@@ -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: gathered.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: gathered.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, and lane attribution stamping.
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
- // ── Attribution tagging: stamp each ref with the eligibility lane that
1156
- // selected it ──────────────────────────────────────────────────────────────
1157
- // Every reflect/distill proposal must record WHICH lane chose its source asset
1158
- // so downstream accept/reject/revert/retrieval outcomes can be sliced by lane
1159
- // (does the PROACTIVE lane produce value vs the reactive lanes?). We build the
1160
- // lane map here — the one place all three lanes are known — and stamp it onto
1161
- // each ImproveEligibleRef object. Because the ref objects are shared by
1162
- // reference across buckets, the stamp travels with the ref through the sort,
1163
- // disk-check, and loop stages down to the reflect/distill event emit sites and
1164
- // createProposal calls. See EligibilitySource for the lane vocabulary.
1165
- //
1166
- // Precedence (prefer the most specific reactive signal):
1167
- // scope > signal-delta > proactive > high-salience
1168
- // A ref with real feedback is attributed to feedback even if it was also due
1169
- // for proactive maintenance or had high encoding salience. We apply lanes
1170
- // weakest-first so the strongest overwrites; the explicit --scope <ref> bypass
1171
- // wins outright (user intent).
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. Best-effort — returns `[]` when the proposals dir is absent
137
- * or the ref is undefined. Used to inject Reflexion-style verbal-RL context
138
- * into the reflect prompt so the agent avoids re-proposing already-refused
139
- * content (arXiv:2303.11366).
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
- try {
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
- }));
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.
@@ -790,7 +786,7 @@ function parseDirectReflectOutput(raw, mode, targetRef) {
790
786
  export async function runReflectViaLlm(opts) {
791
787
  const start = Date.now();
792
788
  let repairAttempts = 0;
793
- const connection = opts.runner.connection;
789
+ const _connection = opts.runner.connection;
794
790
  const messages = [{ role: "user", content: opts.prompt ?? "" }];
795
791
  const configuredTimeout = Object.hasOwn(opts, "timeoutMs")
796
792
  ? (opts.timeoutMs ?? null)