akm-cli 0.9.5 → 0.9.7

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 (130) hide show
  1. package/CHANGELOG.md +295 -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-cli.js +1 -2
  8. package/dist/commands/env/secret-cli.js +1 -5
  9. package/dist/commands/feedback-cli.js +0 -4
  10. package/dist/commands/health/checks.js +0 -32
  11. package/dist/commands/health/surfaces.js +2 -2
  12. package/dist/commands/health.js +6 -15
  13. package/dist/commands/improve/autonomy-gate.js +1 -1
  14. package/dist/commands/improve/consolidate.js +25 -26
  15. package/dist/commands/improve/distill.js +2 -2
  16. package/dist/commands/improve/extract.js +8 -1
  17. package/dist/commands/improve/improve-cli.js +1 -1
  18. package/dist/commands/improve/improve-strategies.js +0 -4
  19. package/dist/commands/improve/improve.js +3 -5
  20. package/dist/commands/improve/locks.js +13 -5
  21. package/dist/commands/improve/loop-stages.js +8 -9
  22. package/dist/commands/improve/memory/memory-contradiction-detect.js +1 -19
  23. package/dist/commands/improve/memory/memory-improve.js +2 -1
  24. package/dist/commands/improve/outcome-loop.js +0 -6
  25. package/dist/commands/improve/preparation.js +1 -1
  26. package/dist/commands/improve/reflect.js +21 -10
  27. package/dist/commands/lint/base-linter.js +141 -18
  28. package/dist/commands/lint/index.js +21 -10
  29. package/dist/commands/proposal/validators/proposal-quality-validators.js +0 -7
  30. package/dist/commands/read/curate.js +47 -0
  31. package/dist/commands/read/search-cli.js +24 -1
  32. package/dist/commands/sources/bundle-config-ops.js +9 -4
  33. package/dist/commands/sources/info.js +14 -10
  34. package/dist/commands/tasks/tasks.js +12 -16
  35. package/dist/commands/workflow-cli.js +2 -2
  36. package/dist/core/asset/asset-placement.js +13 -2
  37. package/dist/core/asset/frontmatter.js +116 -0
  38. package/dist/core/asset/memory-archive.js +97 -0
  39. package/dist/core/common.js +61 -24
  40. package/dist/core/config/config-io.js +2 -2
  41. package/dist/core/config/config-sources.js +32 -2
  42. package/dist/core/config/config.js +1 -1
  43. package/dist/core/config/engine-semantics.js +0 -2
  44. package/dist/core/config/schema/engines.js +9 -7
  45. package/dist/core/config/schema/primitives.js +0 -5
  46. package/dist/core/config/schema/search.js +2 -1
  47. package/dist/core/file-lock.js +2 -1
  48. package/dist/core/maintenance-barrier.js +2 -14
  49. package/dist/core/paths.js +0 -3
  50. package/dist/core/redaction.js +2 -2
  51. package/dist/core/spawn-env.js +8 -12
  52. package/dist/core/state/migrations.js +1 -12
  53. package/dist/core/state-db.js +9 -27
  54. package/dist/core/write-source.js +12 -19
  55. package/dist/execution/directory-identity.js +36 -10
  56. package/dist/indexer/graph/graph-boost.js +0 -4
  57. package/dist/indexer/index-writer-lock.js +43 -24
  58. package/dist/indexer/index-written-assets.js +5 -6
  59. package/dist/indexer/indexer.js +2 -39
  60. package/dist/indexer/materialize-embeddings.js +85 -41
  61. package/dist/indexer/search/db-search.js +15 -48
  62. package/dist/indexer/search/ranking-contributors.js +0 -25
  63. package/dist/indexer/search/ranking.js +3 -13
  64. package/dist/integrations/agent/builder-shared.js +0 -25
  65. package/dist/integrations/agent/model-map.js +2 -60
  66. package/dist/integrations/harnesses/aider/agent-builder.js +1 -3
  67. package/dist/integrations/harnesses/amazonq/agent-builder.js +2 -7
  68. package/dist/integrations/harnesses/claude/agent-builder.js +1 -4
  69. package/dist/integrations/harnesses/codex/agent-builder.js +1 -4
  70. package/dist/integrations/harnesses/copilot/agent-builder.js +2 -6
  71. package/dist/integrations/harnesses/gemini/agent-builder.js +2 -7
  72. package/dist/integrations/harnesses/opencode/agent-builder.js +1 -4
  73. package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +2 -16
  74. package/dist/integrations/harnesses/openhands/agent-builder.js +1 -3
  75. package/dist/integrations/harnesses/pi/agent-builder.js +2 -4
  76. package/dist/integrations/session-logs/index.js +0 -9
  77. package/dist/llm/client.js +75 -42
  78. package/dist/llm/embedder.js +7 -3
  79. package/dist/llm/embedders/remote.js +141 -42
  80. package/dist/registry/network.js +5 -37
  81. package/dist/runtime.js +2 -10
  82. package/dist/scripts/akm-migrate-node.js +73 -133
  83. package/dist/scripts/akm-migrate.js +73 -133
  84. package/dist/setup/engine-config.js +2 -5
  85. package/dist/setup/registry-stash-loader.js +0 -8
  86. package/dist/setup/setup.js +9 -46
  87. package/dist/setup/steps/connection-shared.js +10 -13
  88. package/dist/sources/providers/git-install.js +1 -1
  89. package/dist/sources/snapshot-fetchers/website-ingest.js +126 -0
  90. package/dist/storage/engines/sqlite-migrations.js +20 -1
  91. package/dist/storage/repositories/index-connection.js +45 -3
  92. package/dist/storage/repositories/index-entries-repository.js +0 -15
  93. package/dist/tasks/backends/cron.js +49 -9
  94. package/dist/tasks/backends/launchd.js +15 -20
  95. package/dist/tasks/backends/schtasks.js +18 -8
  96. package/dist/tasks/resolve-akm-bin.js +17 -2
  97. package/dist/tasks/run/run-native-task.js +8 -6
  98. package/dist/tasks/scheduler-invocation.js +8 -1
  99. package/dist/tasks/source/bounded-document.js +2 -4
  100. package/dist/tasks/source/parse-task-source.js +23 -9
  101. package/dist/tasks/source/task-source-v3-frozen.js +5 -7
  102. package/dist/tasks/source/task-source-v4.js +5 -10
  103. package/dist/tasks/source/task-to-v3.js +5 -10
  104. package/dist/tasks/source/task-to-v4.js +1 -4
  105. package/dist/tasks/source-v3.js +6 -6
  106. package/dist/workflows/exec/native-executor.js +21 -31
  107. package/dist/workflows/exec/run-workflow.js +5 -6
  108. package/dist/workflows/exec/scheduler.js +3 -19
  109. package/dist/workflows/exec/step-work.js +1 -4
  110. package/dist/workflows/exec/unit-dispatch.js +2 -2
  111. package/dist/workflows/exec/worktree.js +1 -13
  112. package/dist/workflows/freeze/targets/child-workflow.js +2 -10
  113. package/dist/workflows/ir/plan-hash.js +4 -6
  114. package/dist/workflows/ir/schema-v4.js +0 -12
  115. package/dist/workflows/ir/schema.js +20 -31
  116. package/dist/workflows/parser.js +11 -52
  117. package/dist/workflows/renderer.js +2 -3
  118. package/dist/workflows/resource-limits.js +11 -41
  119. package/dist/workflows/runtime/runs.js +3 -4
  120. package/dist/workflows/source-ir/schema.js +14 -30
  121. package/dist/workflows/validator.js +1 -7
  122. package/docs/reference/cli.md +5 -1
  123. package/docs/reference/configuration.md +1 -1
  124. package/package.json +1 -1
  125. package/schemas/akm-config.json +4 -23
  126. package/schemas/akm-task.json +1 -2
  127. package/schemas/akm-workflow.json +1 -13
  128. package/dist/assets/improve-strategies/frequent.json +0 -15
  129. package/dist/assets/improve-strategies/memory-focus.json +0 -15
  130. package/dist/indexer/search/semantic-status.js +0 -142
@@ -29,7 +29,7 @@ export function describeGatedLanes(lanes) {
29
29
  }
30
30
  /** Configured capabilities that tasks doctor reports behind the autonomy gate. */
31
31
  export function configuredDirectAutonomyLanes() {
32
- return ["memoryCleanup"];
32
+ return [...DIRECT_AUTONOMY_LANES];
33
33
  }
34
34
  /**
35
35
  * True when a lane may mutate. Used by the lane that bypasses the strategy
@@ -27,6 +27,7 @@ import { findEntryIdByRef, getAllEntries, getEntryById } from "../../storage/rep
27
27
  import { getNeighborsByEntryId } from "../../storage/repositories/index-vec-repository.js";
28
28
  import { isProposalSkipped, listProposals, listProposalsReadOnly, proposalContent, } from "../proposal/repository.js";
29
29
  import { hasSupersededStatus, validateProposalFrontmatter } from "../proposal/validators/proposal-quality-validators.js";
30
+ import { DEFAULT_RANDOM_CLUSTER_FRACTION } from "./anti-collapse.js";
30
31
  import { cacheHash } from "./content-hash.js";
31
32
  import { resolveImproveLlmExecution } from "./execution.js";
32
33
  import { resolveImproveStrategy, resolveProcessEnabled } from "./improve-strategies.js";
@@ -190,14 +191,15 @@ async function clusterMemoriesBySimilarity(memories, config, stateDb, signal) {
190
191
  finally {
191
192
  embedMs += Date.now() - embedStart;
192
193
  }
193
- // Upsert newly computed vectors into the cache.
194
+ // Upsert newly computed vectors into the cache. A skipped document
195
+ // (embedBatch reports it via `undefined` rather than throwing, #874) has
196
+ // no vector to cache — omit it rather than writing a bogus embedding.
194
197
  if (stateDb && missVecs.length === missTexts.length) {
195
198
  try {
196
- const toUpsert = missIndices.map((idx, pos) => ({
197
- contentHash: contentHashes[idx],
198
- embedding: missVecs[pos],
199
- modelId,
200
- }));
199
+ const toUpsert = missIndices.flatMap((idx, pos) => {
200
+ const embedding = missVecs[pos];
201
+ return embedding ? [{ contentHash: contentHashes[idx], embedding, modelId }] : [];
202
+ });
201
203
  upsertBodyEmbeddings(stateDb, toUpsert);
202
204
  }
203
205
  catch {
@@ -783,26 +785,23 @@ async function judgeConsolidationChunks(args) {
783
785
  ...(opts.onNotices ? { onNotices: opts.onNotices } : {}),
784
786
  });
785
787
  };
786
- let raw = await callChunkLlm(`chunk ${chunkIdx + 1} failed`);
788
+ // callChunkLlm already retries once internally (llm/client.ts's
789
+ // chatCompletion, jittered 200-800ms backoff) — a second, outer retry
790
+ // here stacked an uncoordinated fixed 2s backoff on top of it. Removed;
791
+ // only mark the chunk failed once the single retry the client already
792
+ // performs has been exhausted.
793
+ const raw = await callChunkLlm(`chunk ${chunkIdx + 1} failed`);
787
794
  if (!raw.ok) {
788
- // Single retry with 2s backoff before recording chunk as lost.
789
- // Recovers transient Shredder LM Studio timeouts without significantly
790
- // extending run time. Only marks failed if both attempts fail.
791
- await new Promise((r) => setTimeout(r, 2_000));
792
- const retry = await callChunkLlm(`chunk ${chunkIdx + 1} retry failed`);
793
- if (!retry.ok) {
794
- warn(retry.error ?? `chunk ${chunkIdx + 1} failed after retry`);
795
- warnings.push(retry.error ?? `chunk ${chunkIdx + 1} failed after retry`);
796
- totalChunksProcessed++;
797
- accounting.totalChunksFailed++;
798
- // Account for the chunk's memories under the failed-chunk bucket.
799
- // judgedNoAction does NOT run on this path (it's after the success
800
- // guards) so without this the accounting invariant breaks on every
801
- // chunk-level transport/parse failure.
802
- accounting.failedChunkMemories += chunk.length;
803
- continue;
804
- }
805
- raw = retry;
795
+ warn(raw.error ?? `chunk ${chunkIdx + 1} failed`);
796
+ warnings.push(raw.error ?? `chunk ${chunkIdx + 1} failed`);
797
+ totalChunksProcessed++;
798
+ accounting.totalChunksFailed++;
799
+ // Account for the chunk's memories under the failed-chunk bucket.
800
+ // judgedNoAction does NOT run on this path (it's after the success
801
+ // guards) so without this the accounting invariant breaks on every
802
+ // chunk-level transport/parse failure.
803
+ accounting.failedChunkMemories += chunk.length;
804
+ continue;
806
805
  }
807
806
  // C9 action 1: AKM_DEBUG_LLM was a separate, undocumented env var for this
808
807
  // one diagnostic; folded into the standard AKM_VERBOSE gate (warnVerbose)
@@ -922,7 +921,7 @@ async function planConsolidation(opts, config, stashDir, _startMs, memories, war
922
921
  const antiCollapseForCluster = getImproveProcessConfig("consolidate", opts.improveProfile)?.antiCollapse ??
923
922
  {};
924
923
  if (antiCollapseForCluster.enabled !== false && clusteredMemories.length > 2) {
925
- const fraction = antiCollapseForCluster.randomClusterFraction ?? 0.05;
924
+ const fraction = antiCollapseForCluster.randomClusterFraction ?? DEFAULT_RANDOM_CLUSTER_FRACTION;
926
925
  const randomCount = Math.max(1, Math.floor(clusteredMemories.length * fraction));
927
926
  // Pick `randomCount` positions to inject random (un-clustered) members.
928
927
  // Use a seeded-ish shuffle: sort by hash of the name so it's deterministic
@@ -73,7 +73,7 @@ import { stripFrontmatterBody as stripBodyForFidelity } from "./content-hash.js"
73
73
  import { autoRepairLessonFrontmatter, autoSwapDescriptionWhenToUse, collectLessonQualityFindings, repairLessonDescriptionTruncation, } from "./distill/content-repair.js";
74
74
  import { memoryKnowledgePromotionRequiresDispatch, planMemoryKnowledgePromotion, promoteMemoryToKnowledge, } from "./distill/promote-memory.js";
75
75
  import { fetchTopSimilarLessons, persistOutputEncodingSalience, runLessonQualityJudge, writeQualityRejection, } from "./distill/quality-gate.js";
76
- import { buildClsContext, checkDistillFidelity } from "./distill-guards.js";
76
+ import { buildClsContext, checkDistillFidelity, DEFAULT_CLS_ADJACENT_COUNT } from "./distill-guards.js";
77
77
  import { deriveKnowledgeRef } from "./distill-promotion-policy.js";
78
78
  import { buildRefVocabulary, scoreEncodingSalience } from "./encoding-salience.js";
79
79
  import { resolveImproveLlmExecution } from "./execution.js";
@@ -1239,7 +1239,7 @@ async function buildDistillMessages(args) {
1239
1239
  let clsContext = "";
1240
1240
  if (clsConfig.enabled) {
1241
1241
  try {
1242
- const adjacentCount = clsConfig.adjacentCount ?? 3;
1242
+ const adjacentCount = clsConfig.adjacentCount ?? DEFAULT_CLS_ADJACENT_COUNT;
1243
1243
  // Use the asset content or input ref as the query for adjacent retrieval.
1244
1244
  const clsQuery = assetContent ? assetContent.slice(0, 500) : inputRef;
1245
1245
  const adjacentItems = await fetchSimilarLessonsFn(clsQuery, adjacentCount);
@@ -44,6 +44,7 @@ import { indexWrittenAssets } from "../../indexer/index-written-assets.js";
44
44
  import { disposeLoweredExecutionDispatchLease, } from "../../integrations/agent/execution-lowering.js";
45
45
  import { getAvailableHarnesses } from "../../integrations/session-logs/index.js";
46
46
  import { preFilterSession } from "../../integrations/session-logs/pre-filter.js";
47
+ import { isJsonSchemaKnownUnsupported } from "../../llm/client.js";
47
48
  import { callStructured, preflightStructuredLlmRunner } from "../../llm/structured-call.js";
48
49
  import { sha256Hex } from "../../runtime.js";
49
50
  import { getExtractedSessionsMap, getLastExtractRunAt, shouldSkipAlreadyExtractedSession, upsertExtractedSession, } from "../../storage/repositories/extract-sessions-repository.js";
@@ -563,7 +564,13 @@ async function runSessionExtractionLlmCall(args) {
563
564
  return payload.parseFailure ? undefined : payload;
564
565
  },
565
566
  validate: (payload) => ({ ok: true, value: payload }),
566
- maxAttempts: llmRunner.connection.supportsJsonSchema === true ? 1 : 2,
567
+ // One attempt when structured output is expected to work (not explicitly
568
+ // disabled, and this connection hasn't already proven otherwise this
569
+ // process — see `isJsonSchemaKnownUnsupported`); two when it's known
570
+ // unsupported and extraction is relying on looser prompt-contract JSON.
571
+ maxAttempts: llmRunner.connection.supportsJsonSchema !== false && !isJsonSchemaKnownUnsupported(llmRunner.connection)
572
+ ? 1
573
+ : 2,
567
574
  buildFeedback: () => "Your previous response did not contain a valid extraction payload. Respond with ONLY a JSON object matching the requested schema, with a candidates array and no prose or code fences.",
568
575
  });
569
576
  if (result.ok)
@@ -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,
@@ -35,7 +35,7 @@ import { akmDistill } from "./distill.js";
35
35
  import { buildLatestProposalTsMap, collectEligibleRefs, collectEligibleRefsReadOnly, memoryCleanupParentRef, resolveImproveScope, shouldAnalyzeMemoryCleanup, } from "./eligibility.js";
36
36
  import { countEvalCases } from "./eval-cases.js";
37
37
  import { resolveImprovePlan, resolveImproveStrategy } from "./improve-strategies.js";
38
- import { improveLockPath, MIN_IMPROVE_LOCK_STALE_MS, releaseImproveLock, tryAcquireImproveLock } from "./locks.js";
38
+ import { improveLockPath, releaseImproveLock, tryAcquireImproveLock } from "./locks.js";
39
39
  // The cycle loop / post-loop / maintenance stages live in ./loop-stages.
40
40
  import { runImproveLoopStage, runImprovePostLoopStage } from "./loop-stages.js";
41
41
  import { analyzeMemoryCleanup } from "./memory/memory-improve.js";
@@ -115,7 +115,7 @@ function createRunWriteJournal() {
115
115
  export async function akmImprove(options = {}) {
116
116
  const setup = resolveImproveRunSetup(options);
117
117
  options = setup.options;
118
- const { budgetMs, budgetAbortController, scope, selectedStrategy, syncRepoDir, resolvedStateDbPath, resolvedLockPath, lockStaleAfterMs, } = setup;
118
+ const { budgetMs, budgetAbortController, scope, selectedStrategy, syncRepoDir, resolvedStateDbPath, resolvedLockPath, } = setup;
119
119
  let clearBudgetTimer = () => { };
120
120
  let initialGitPaths = new Set();
121
121
  const runJournal = createRunWriteJournal();
@@ -164,7 +164,7 @@ export async function akmImprove(options = {}) {
164
164
  }
165
165
  try {
166
166
  if (!options.dryRun) {
167
- const acquisition = tryAcquireImproveLock(resolvedLockPath, lockStaleAfterMs, options.skipIfLocked, {
167
+ const acquisition = tryAcquireImproveLock(resolvedLockPath, options.skipIfLocked, {
168
168
  // R25: C2 boundary-pinned path — the long-lived handle doesn't exist yet.
169
169
  dbPath: resolvedStateDbPath,
170
170
  });
@@ -484,7 +484,6 @@ function resolveImproveRunSetup(options) {
484
484
  // triage, indexing, proposal work, maintenance, and final stash sync.
485
485
  const lockBaseDir = primaryStashDir ? path.join(primaryStashDir, ".akm") : path.join(options.stashDir ?? ".", ".akm");
486
486
  const resolvedLockPath = improveLockPath(lockBaseDir);
487
- const lockStaleAfterMs = Math.max(MIN_IMPROVE_LOCK_STALE_MS, budgetMs + 10 * 60 * 1000);
488
487
  const effectiveSync = { ...improveProfile.sync, ...options.sync };
489
488
  return {
490
489
  startMs,
@@ -514,7 +513,6 @@ function resolveImproveRunSetup(options) {
514
513
  syncRepoDir,
515
514
  resolvedStateDbPath,
516
515
  resolvedLockPath,
517
- lockStaleAfterMs,
518
516
  effectiveSync,
519
517
  };
520
518
  }
@@ -9,13 +9,12 @@ import { createLockPayload, probeLock, reclaimStaleLock, releaseLock, tryAcquire
9
9
  import { tryWithMaintenanceStartBarrier, withMaintenanceStartBarrier } from "../../core/maintenance-barrier.js";
10
10
  import { describeInaccessiblePath } from "../../core/path-access.js";
11
11
  import { warn } from "../../core/warn.js";
12
- export const MIN_IMPROVE_LOCK_STALE_MS = 4 * 60 * 60 * 1000;
13
12
  export function improveLockPath(lockBaseDir) {
14
13
  return path.join(lockBaseDir, "improve.lock");
15
14
  }
16
- export function tryAcquireImproveLock(lockPath, staleAfterMs, skipIfLocked, eventsCtx) {
15
+ export function tryAcquireImproveLock(lockPath, skipIfLocked, eventsCtx) {
17
16
  let recoveryEvent;
18
- const acquire = () => tryAcquireImproveLockUnlocked(lockPath, staleAfterMs, skipIfLocked, (event) => {
17
+ const acquire = () => tryAcquireImproveLockUnlocked(lockPath, skipIfLocked, (event) => {
19
18
  recoveryEvent = event;
20
19
  });
21
20
  const result = skipIfLocked ? tryWithMaintenanceStartBarrier(acquire) : withMaintenanceStartBarrier(acquire);
@@ -37,14 +36,23 @@ export function tryAcquireImproveLock(lockPath, staleAfterMs, skipIfLocked, even
37
36
  }
38
37
  return result;
39
38
  }
40
- function tryAcquireImproveLockUnlocked(lockPath, staleAfterMs, skipIfLocked, onRecovered) {
39
+ function tryAcquireImproveLockUnlocked(lockPath, skipIfLocked, onRecovered) {
41
40
  fs.mkdirSync(path.dirname(lockPath), { recursive: true });
42
41
  const lockPayload = () => createLockPayload({ startedAt: new Date().toISOString() });
43
42
  let ownership = tryAcquireLockSync(lockPath, lockPayload());
44
43
  if (ownership) {
45
44
  return { state: "acquired", ownership };
46
45
  }
47
- const probe = probeLock(lockPath, { staleAfterMs });
46
+ // No `staleAfterMs`: only a verifiably dead holder is ever reclaimed. A
47
+ // wedged-but-alive `akm improve` (SQLite WAL + busy_timeout + BEGIN
48
+ // IMMEDIATE already serialize concurrent writes to state.db at the
49
+ // correctness layer, so this lock only avoids duplicate LOGICAL work) must
50
+ // not have its lease silently taken away purely because a clock elapsed —
51
+ // that was the #872-shaped hazard here: a live holder passed the
52
+ // PID-liveness check forever, so only a multi-hour age window could ever
53
+ // free it, stranding every `akm improve --skip-if-locked` invocation for
54
+ // up to that long while reporting success.
55
+ const probe = probeLock(lockPath);
48
56
  // Race: the holder released the lock between our failed `tryAcquireLockSync`
49
57
  // and this probe, so the probe sees no file (`absent`). Retry acquisition once
50
58
  // rather than falling through to the contended skip/throw below — otherwise we
@@ -13,7 +13,6 @@ import { getDbPath } from "../../core/paths.js";
13
13
  import { withStateDb } from "../../core/state-db.js";
14
14
  import { info } from "../../core/warn.js";
15
15
  import { DEFAULT_GRAPH_EXTRACTION_INCLUDE_TYPES, runGraphExtractionPass, } from "../../indexer/graph/graph-extraction.js";
16
- import { withIndexWriterLease } from "../../indexer/index-writer-lock.js";
17
16
  import { deriveWritableBundleIds } from "../../indexer/installations.js";
18
17
  import { collectPendingMemories, runMemoryInferencePass, } from "../../indexer/passes/memory-inference.js";
19
18
  import { resolveSourceEntries } from "../../indexer/search/search-source.js";
@@ -679,13 +678,13 @@ export async function runImproveMaintenancePasses(args) {
679
678
  graphExtractionFn,
680
679
  reindexWithIndexDbReleased,
681
680
  };
682
- const collected = await withIndexWriterLease({ purpose: "improve-maintenance", signal: budgetSignal }, () => runMaintenancePassesUnderLease(ctx, dbCell, {
681
+ const collected = await runMaintenancePassesUnderLease(ctx, dbCell, {
683
682
  actionableRefs: args.actionableRefs,
684
683
  memoryRefsForInference,
685
684
  consolidationRan: args.consolidationRan,
686
685
  allWarnings,
687
686
  openIndexDb,
688
- }));
687
+ });
689
688
  return {
690
689
  ...(collected.memoryInference ? { memoryInference: collected.memoryInference } : {}),
691
690
  ...(collected.graphExtraction ? { graphExtraction: collected.graphExtraction } : {}),
@@ -697,12 +696,12 @@ export async function runImproveMaintenancePasses(args) {
697
696
  };
698
697
  }
699
698
  /**
700
- * The maintenance sequence run under the index-writer lease (formerly the
701
- * ~389-line anonymous `withIndexWriterLease` callback): memory inference
702
- * reindex-after-inference → graph extraction → proposal hygiene (orphan purge,
703
- * expiration) → retention purges. Each pass returns its results and warnings;
704
- * this orchestrator folds warnings into the caller's `allWarnings` sink at the
705
- * same points the inline code pushed them.
699
+ * The maintenance sequence (formerly the ~389-line anonymous
700
+ * `withIndexWriterLease` callback, before #872 removed the index-rebuild
701
+ * lease): memory inference → reindex-after-inference → graph extraction →
702
+ * proposal hygiene (orphan purge, expiration) → retention purges. Each pass
703
+ * returns its results and warnings; this orchestrator folds warnings into the
704
+ * caller's `allWarnings` sink at the same points the inline code pushed them.
706
705
  */
707
706
  async function runMaintenancePassesUnderLease(ctx, dbCell, args) {
708
707
  const { allWarnings } = args;
@@ -14,7 +14,7 @@
14
14
  * # Algorithm
15
15
  *
16
16
  * 1. Collect all derived memories grouped by `parentRef` family.
17
- * 2. For each family, enumerate candidate pairs (limited to MAX_FAMILY_SIZE).
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
20
  * 4. For confirmed contradictions, write `contradictedBy` edges directly to
@@ -43,16 +43,6 @@ import { callStructured, preflightStructuredLlmRunner } from "../../../llm/struc
43
43
  import { resolveImproveLlmExecution } from "../execution.js";
44
44
  import { isDerivedMemory, memoryIdentityRef, resolveParentRef } from "./derived-ref.js";
45
45
  // ── Constants ────────────────────────────────────────────────────────────────
46
- /**
47
- * Maximum family size for pairwise contradiction checking. Families larger
48
- * than this are skipped to bound the LLM call count (O(n²) pairs).
49
- */
50
- const MAX_FAMILY_SIZE = 8;
51
- /**
52
- * Maximum number of contradiction pairs to check per improve run, across all
53
- * families. Prevents runaway LLM usage on stashes with many memories.
54
- */
55
- const MAX_PAIRS_PER_RUN = 20;
56
46
  /**
57
47
  * Minimum confidence required to write a contradiction edge. Below this
58
48
  * threshold the LLM may be flagging topic-overlap rather than genuine logical
@@ -218,15 +208,9 @@ export async function detectAndWriteContradictions(stashDir, config, chat, strat
218
208
  for (const [, family] of byParent) {
219
209
  if (family.length < 2)
220
210
  continue;
221
- if (family.length > MAX_FAMILY_SIZE) {
222
- result.warnings.push(`Skipping contradiction check for family of ${family.length} members (exceeds MAX_FAMILY_SIZE=${MAX_FAMILY_SIZE})`);
223
- continue;
224
- }
225
211
  result.familiesExamined++;
226
212
  for (let i = 0; i < family.length - 1; i++) {
227
213
  for (let j = i + 1; j < family.length; j++) {
228
- if (candidatePairs.length >= MAX_PAIRS_PER_RUN)
229
- break;
230
214
  const a = family[i];
231
215
  const b = family[j];
232
216
  if (!a || !b)
@@ -245,8 +229,6 @@ export async function detectAndWriteContradictions(stashDir, config, chat, strat
245
229
  continue;
246
230
  candidatePairs.push({ a, b, loser, winnerRef });
247
231
  }
248
- if (candidatePairs.length >= MAX_PAIRS_PER_RUN)
249
- break;
250
232
  }
251
233
  }
252
234
  if (candidatePairs.length === 0) {
@@ -5,6 +5,7 @@ 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";
@@ -553,7 +554,7 @@ function priorBeliefStateForArchive(candidate) {
553
554
  }
554
555
  function createArchiveDir(stashDir, ref, archivedAt) {
555
556
  const baseName = `${archivedAt.replace(/[:.]/g, "-")}-${sanitizeRef(ref)}`;
556
- const root = path.join(stashDir, ".akm", "memory-cleanup", "archive");
557
+ const root = path.join(stashDir, MEMORY_ARCHIVE_REL);
557
558
  fs.mkdirSync(root, { recursive: true });
558
559
  let attempt = 0;
559
560
  while (true) {
@@ -9,12 +9,6 @@ import { getAllAssetOutcomes, getAssetOutcome, getOutcomeScoresByRef, upsertAsse
9
9
  * At α = 0.3 the window is ≈ 3 cycles.
10
10
  */
11
11
  export const OUTCOME_EMA_ALPHA = 0.3;
12
- /**
13
- * Maximum K improve cycles for the eligibility-trace window. Retrievals older
14
- * than K cycles contribute via the EMA decay naturally (they are already baked
15
- * into `expected_retrieval_rate`).
16
- */
17
- export const OUTCOME_TRACE_CYCLES = 5;
18
12
  /**
19
13
  * Warm-start cap: the maximum `outcome_score` a brand-new row can be seeded with
20
14
  * (from the utility EMA). Prevents a `[0,1]`-range utility value from generating
@@ -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 });
@@ -44,7 +44,7 @@ import { prepareInlineExecution, prepareInlineExecutionWithRunner } from "../../
44
44
  import { buildReflectOutputRepairPrompt, buildReflectPrompt, extractDraftConfidence, parseAgentProposalPayload, } from "../../integrations/agent/prompts.js";
45
45
  import { runnerIsLlm, runnerSupportsFileWrite } from "../../integrations/agent/runner.js";
46
46
  import { collectDispatchSensitiveValues } from "../../integrations/agent/runner-dispatch.js";
47
- import { LlmCallError } from "../../llm/client.js";
47
+ import { isJsonSchemaKnownUnsupported, LlmCallError } from "../../llm/client.js";
48
48
  import { callStructured } from "../../llm/structured-call.js";
49
49
  import { baseFailureFields, enoentHintMessage, isEnoentFailure } from "../agent/agent-support.js";
50
50
  import { isProposalSkipped, listProposalsReadOnly, proposalContent, } from "../proposal/repository.js";
@@ -578,8 +578,9 @@ export function sanitizeReflectPayload(payload, sourceContent, targetRef) {
578
578
  }
579
579
  /**
580
580
  * JSON Schema for structured reflect output. Passed to `chatCompletion` when
581
- * the connection has `supportsJsonSchema: true` so the model returns a strict
582
- * JSON object containing only the target-scoped fields AKM cannot derive.
581
+ * {@link wantsJsonSchemaOutput} selects `outputMode: "json_schema"`, so the
582
+ * model returns a strict JSON object containing only the target-scoped
583
+ * fields AKM cannot derive.
583
584
  */
584
585
  const REFLECT_FRONTMATTER_PATCH_JSON_SCHEMA = {
585
586
  type: "object",
@@ -626,6 +627,17 @@ const REFLECT_UNSCOPED_JSON_SCHEMA = {
626
627
  frontmatterPatch: REFLECT_FRONTMATTER_PATCH_JSON_SCHEMA,
627
628
  },
628
629
  };
630
+ /**
631
+ * Whether to frame the reflect prompt for structured JSON output on this
632
+ * connection. Optimistic by default — `chatCompletion` attempts
633
+ * `response_format: json_schema` fresh on every call and falls back once on
634
+ * a 4xx, so there is no persisted verdict to consult here. `false` only when
635
+ * a human/workflow explicitly disabled it, or a real call already proved
636
+ * this connection rejects it earlier in the same process.
637
+ */
638
+ function wantsJsonSchemaOutput(connection) {
639
+ return connection.supportsJsonSchema !== false && !isJsonSchemaKnownUnsupported(connection);
640
+ }
629
641
  /** Critique prompt injected between prior draft and refinement request (Self-Refine loop). */
630
642
  const REFLECT_CRITIQUE_PROMPT = "Your previous proposal is shown above. Review it critically and provide an improved version that is more specific, actionable, and avoids any issues with the previous attempt. Return only the improved response using the output contract from the original prompt.";
631
643
  /**
@@ -778,7 +790,7 @@ function parseDirectReflectOutput(raw, mode, targetRef) {
778
790
  export async function runReflectViaLlm(opts) {
779
791
  const start = Date.now();
780
792
  let repairAttempts = 0;
781
- const connection = opts.runner.connection;
793
+ const _connection = opts.runner.connection;
782
794
  const messages = [{ role: "user", content: opts.prompt ?? "" }];
783
795
  const configuredTimeout = Object.hasOwn(opts, "timeoutMs")
784
796
  ? (opts.timeoutMs ?? null)
@@ -1354,21 +1366,20 @@ async function resolveReflectSource(options, stash, emitReflectFailed) {
1354
1366
  }
1355
1367
  /**
1356
1368
  * Run the agent with the optional Self-Refine loop (R-1 / #372): up to
1357
- * MAX_REFINE_ITERS invocations, each injecting the prior draft as self-critique
1369
+ * `maxRefineIters` invocations, each injecting the prior draft as self-critique
1358
1370
  * context and exiting early on a no-op refinement. Synthesizes per-iteration
1359
1371
  * draft paths into `draftPathsToCleanup` (mutated) and returns the final agent
1360
1372
  * result + last draft path. Extracted verbatim from `akmReflect`.
1361
1373
  */
1362
1374
  async function runReflectRefineIterations(args) {
1363
1375
  const { options, parsedRef, assetContent, feedback, schemaHints, relatedLessons, rejectedProposals, standardsContext, runnerSpec, lease, agentEnv, draftPathsToCleanup, onNotices, } = args;
1364
- const MAX_REFINE_ITERS = 3;
1365
- const maxRefineIters = Math.min(Math.max(1, options.maxRefineIters ?? 1), MAX_REFINE_ITERS);
1376
+ const maxRefineIters = Math.max(1, options.maxRefineIters ?? 1);
1366
1377
  // Determine whether this dispatch can honour the file-write contract.
1367
1378
  // Agent CLI + OpenCode SDK runners both have filesystem access; the direct
1368
1379
  // LLM HTTP runner does NOT.
1369
1380
  const canRunnerWriteFile = runnerSupportsFileWrite(runnerSpec);
1370
1381
  const outputMode = runnerIsLlm(runnerSpec)
1371
- ? runnerSpec.connection.supportsJsonSchema
1382
+ ? wantsJsonSchemaOutput(runnerSpec.connection)
1372
1383
  ? "json_schema"
1373
1384
  : "framed_markdown"
1374
1385
  : undefined;
@@ -1421,10 +1432,10 @@ async function runReflectRefineIterations(args) {
1421
1432
  ...(options.signal ? { signal: options.signal } : {}),
1422
1433
  priorDraft,
1423
1434
  iteration: iter,
1424
- ...(runnerSpec.connection.supportsJsonSchema
1435
+ ...(outputMode === "json_schema"
1425
1436
  ? { responseSchema: options.ref ? REFLECT_JSON_SCHEMA : REFLECT_UNSCOPED_JSON_SCHEMA }
1426
1437
  : {}),
1427
- outputMode: runnerSpec.connection.supportsJsonSchema ? "json_schema" : "framed_markdown",
1438
+ outputMode: outputMode ?? "framed_markdown",
1428
1439
  ...(options.ref ? { targetRef: options.ref } : {}),
1429
1440
  allowRepair: repairAttempts === 0,
1430
1441
  ...(options.chat ? { chat: options.chat } : {}),