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
@@ -4,19 +4,15 @@
4
4
  /**
5
5
  * The ONE allowlist-based child-environment primitive.
6
6
  *
7
- * Two akm code paths spawn a child from an explicit list of environment
7
+ * Three akm code paths spawn a child from an explicit list of environment
8
8
  * variable NAMES — the agent-CLI spawn wrapper
9
- * (`integrations/agent/spawn.ts`, `profile.envPassthrough`) and the workflow
10
- * `exec` unit runner (`workflows/exec/exec-unit.ts`) and both start from an
11
- * EMPTY environment and copy through named entries with
12
- * {@link collectAllowlistedEnv}. Keeping that in one leaf module is what makes
13
- * "allowlist" a single reviewable mechanism instead of two implementations
14
- * that drift apart.
15
- *
16
- * NOT covered: the opencode-sdk server spawn
17
- * (`integrations/harnesses/opencode-sdk/sdk-runner.ts`) keeps its own
18
- * hard-coded name list and does not route through here, so it gets neither the
19
- * platform floor below nor PATH supplementation.
9
+ * (`integrations/agent/spawn.ts`, `profile.envPassthrough`), the workflow
10
+ * `exec` unit runner (`workflows/exec/exec-unit.ts`), and the opencode-sdk
11
+ * server spawn (`integrations/harnesses/opencode-sdk/sdk-runner.ts`) and
12
+ * all build their allowlist from {@link COMMON_SPAWN_ENV_PASSTHROUGH} /
13
+ * {@link spawnEnvNamesFor}. Keeping that in one leaf module is what makes
14
+ * "allowlist" a single reviewable mechanism instead of independent
15
+ * implementations that drift apart.
20
16
  *
21
17
  * A LEAF: node built-ins only, so both the integrations layer and the workflow
22
18
  * engine can import it without opening a cycle.
@@ -1132,22 +1132,11 @@ export function runMigrations(db, options) {
1132
1132
  runSqliteMigrations(db, STATE_MIGRATIONS, {
1133
1133
  lockInitialMigrationPrefixThrough: options?.existingUnversionedDatabase ? "002-task-history-per-run" : undefined,
1134
1134
  beforeLedgerInitializationLocked(lockedDb) {
1135
- if (options?.freshDatabase) {
1136
- if (!options.verifyFreshDatabaseOwnership) {
1137
- throw new Error("A fresh state.db migration requires verified file ownership.");
1138
- }
1139
- options.verifyFreshDatabaseOwnership();
1135
+ if (options?.freshDatabase)
1140
1136
  return;
1141
- }
1142
1137
  prepareExistingUnversionedState(lockedDb);
1143
1138
  },
1144
1139
  beforeMigrationLocked(migration, lockedDb) {
1145
- if (options?.freshDatabase) {
1146
- if (!options.verifyFreshDatabaseOwnership) {
1147
- throw new Error("A fresh state.db migration requires verified file ownership.");
1148
- }
1149
- options.verifyFreshDatabaseOwnership();
1150
- }
1151
1140
  if (options?.existingUnversionedDatabase && !existingUnversionedSnapshotPrepared) {
1152
1141
  prepareExistingUnversionedState(lockedDb);
1153
1142
  }
@@ -65,6 +65,7 @@
65
65
  import { randomUUID } from "node:crypto";
66
66
  import fs from "node:fs";
67
67
  import path from "node:path";
68
+ import { sleepSync } from "../runtime.js";
68
69
  import { openDatabase } from "../storage/database.js";
69
70
  import { assertMigrationLedger } from "../storage/engines/sqlite-migrations.js";
70
71
  import { openManagedDatabase, withManagedDb } from "../storage/managed-db.js";
@@ -189,20 +190,18 @@ function reserveFreshStateDatabase(dbPath) {
189
190
  throw error;
190
191
  }
191
192
  try {
192
- const reservation = {
193
+ return {
193
194
  path: dbPath,
194
195
  fd,
195
196
  identity: fs.fstatSync(fd, { bigint: true }),
196
197
  };
197
- assertOwnedFileReservation(reservation, "Fresh state.db");
198
- return reservation;
199
198
  }
200
199
  catch (error) {
201
200
  try {
202
201
  fs.closeSync(fd);
203
202
  }
204
203
  catch {
205
- // Preserve the ownership failure.
204
+ // Preserve the fstat failure.
206
205
  }
207
206
  throw error;
208
207
  }
@@ -217,20 +216,18 @@ function openExistingStateDatabaseSource(dbPath) {
217
216
  throw new Error(`Could not bind the existing state.db source inode: ${detail}`);
218
217
  }
219
218
  try {
220
- const source = {
219
+ return {
221
220
  path: dbPath,
222
221
  fd,
223
222
  identity: fs.fstatSync(fd, { bigint: true }),
224
223
  };
225
- assertStateDatabaseSource(source);
226
- return source;
227
224
  }
228
225
  catch (error) {
229
226
  try {
230
227
  fs.closeSync(fd);
231
228
  }
232
229
  catch {
233
- // Preserve the source identity failure.
230
+ // Preserve the fstat failure.
234
231
  }
235
232
  throw error;
236
233
  }
@@ -386,10 +383,7 @@ export function openStateDatabase(dbPath, options) {
386
383
  return openManagedDatabase({
387
384
  path: resolvedPath,
388
385
  pragmas: { dataDir: path.dirname(resolvedPath) },
389
- init: (db) => runMigrations(db, {
390
- freshDatabase: true,
391
- verifyFreshDatabaseOwnership: () => { },
392
- }),
386
+ init: (db) => runMigrations(db, { freshDatabase: true }),
393
387
  });
394
388
  }
395
389
  const isCanonical = path.resolve(resolvedPath) === path.resolve(canonicalPath);
@@ -404,12 +398,10 @@ export function openStateDatabase(dbPath, options) {
404
398
  freshReservation = reserveFreshStateDatabase(resolvedPath);
405
399
  if (!freshReservation) {
406
400
  existingSource = openExistingStateDatabaseSource(resolvedPath);
407
- assertStateDatabaseSource(existingSource);
408
401
  const preflight = openDatabase(sqliteBoundFilePath(existingSource), {
409
402
  readonly: true,
410
403
  });
411
404
  try {
412
- assertStateDatabaseSource(existingSource);
413
405
  preflight.exec("PRAGMA busy_timeout = 30000");
414
406
  const ledger = assertMigrationLedger(preflight, STATE_MIGRATIONS);
415
407
  existingUnversionedDatabase = ledger.migrationIds.length === 0;
@@ -422,21 +414,13 @@ export function openStateDatabase(dbPath, options) {
422
414
  preflight.close();
423
415
  }
424
416
  }
425
- const ownedFresh = freshReservation;
426
417
  const boundSource = existingSource;
427
- if (boundSource)
428
- assertStateDatabaseSource(boundSource);
429
418
  openedDb = openManagedDatabase({
430
419
  path: boundSource ? sqliteBoundFilePath(boundSource) : resolvedPath,
431
420
  pragmas: { dataDir: path.dirname(resolvedPath) },
432
421
  init: (db) => {
433
- if (boundSource)
434
- assertStateDatabaseSource(boundSource);
435
422
  runMigrations(db, {
436
- freshDatabase: !!ownedFresh,
437
- verifyFreshDatabaseOwnership: ownedFresh
438
- ? () => assertOwnedFileReservation(ownedFresh, "Fresh state.db")
439
- : undefined,
423
+ freshDatabase: !!freshReservation,
440
424
  existingUnversionedDatabase,
441
425
  allowHistoricalDestructiveStateUpgrade: options?.allowHistoricalDestructiveStateUpgrade,
442
426
  beforeExistingUnversionedStateMigration: options?.allowHistoricalDestructiveStateUpgrade
@@ -463,12 +447,10 @@ export function openStateDatabase(dbPath, options) {
463
447
  },
464
448
  });
465
449
  if (existingSource) {
466
- assertStateDatabaseSource(existingSource);
467
450
  closeFileIdentity(existingSource);
468
451
  existingSource = undefined;
469
452
  }
470
453
  if (freshReservation) {
471
- assertOwnedFileReservation(freshReservation, "Fresh state.db");
472
454
  closeFileIdentity(freshReservation);
473
455
  freshReservation = undefined;
474
456
  }
@@ -610,11 +592,11 @@ function isRetryableBeginError(err) {
610
592
  msg.includes("did not open a transaction"));
611
593
  }
612
594
  const WITH_IMMEDIATE_TX_MAX_ATTEMPTS = 5;
613
- /** Portable synchronous sleep (works under both Bun and Node). */
595
+ /** Portable synchronous sleep (works under both Bun and Node). Delegates to the runtime boundary's `sleepSync`. */
614
596
  function sleepSyncMs(ms) {
615
597
  if (ms <= 0)
616
598
  return;
617
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
599
+ sleepSync(ms);
618
600
  }
619
601
  /**
620
602
  * Open, but deliberately do not finish, an immediate transaction.
@@ -34,7 +34,7 @@ import { assetPathForName, stashDirFor } from "./asset/asset-placement.js";
34
34
  import { conceptIdFromTypeName, displayRef } from "./asset/resolve-ref.js";
35
35
  import { deriveBundleId } from "./bundle-id.js";
36
36
  import { existingFileMode, isWithin, resolveStashDir, writeFileAtomic } from "./common.js";
37
- import { resolveConfiguredSources } from "./config/config.js";
37
+ import { bundleContentRoot, resolveConfiguredSources } from "./config/config.js";
38
38
  import { ConfigError, UsageError } from "./errors.js";
39
39
  import { sanitizeCommitMessage } from "./git-message.js";
40
40
  import { warn } from "./warn.js";
@@ -111,7 +111,15 @@ export function planWriteTargetPublication(target, filePaths, options) {
111
111
  }
112
112
  const unignoredPaths = paths.filter((_, index) => !ignored.has(relativePaths[index]));
113
113
  assertWriteTargetPathsClean(target.source, unignoredPaths);
114
- assertWriteTargetPlanBase(target, expectedBaseHead);
114
+ // Not re-verified against `expectedBaseHead` here: any drift during the
115
+ // ignored/clean-path checks above is caught by the SAME expectedBaseHead
116
+ // comparison `beginWriteTargetMutation` runs immediately before the first
117
+ // mutation (its documented contract, and every real caller calls it right
118
+ // after this function returns) -- checking it again here a few lines
119
+ // earlier for the exact same value only duplicated that check, not added
120
+ // a new one. `publishWriteTargetPlan` independently re-checks a third time
121
+ // after the actual mutation, which is a genuinely different point in time
122
+ // and stays.
115
123
  return { target, paths, publish: ignored.size === 0, expectedBaseHead };
116
124
  }
117
125
  function assertNoWritePathDescendantSymlinks(source, filePaths) {
@@ -538,7 +546,6 @@ function captureGitPathState(repoPath, absolutePath, relativePath) {
538
546
  }
539
547
  /** Create or recover the one transaction-owned commit without pushing it. */
540
548
  export function ensureGitTransactionCommit(target, publication, options) {
541
- assertGitPublicationIdentity(target, publication);
542
549
  const paths = normalizePublicationPaths(options.paths);
543
550
  validateGitWorktreeSnapshots(target, paths, options.snapshots);
544
551
  if (publication.commit !== undefined) {
@@ -626,7 +633,6 @@ export function publishWriteTargetTransaction(target, publication, options) {
626
633
  }
627
634
  /** Push only the recorded transaction commit, never later local descendants. */
628
635
  export function publishGitTransactionCommit(target, publication, transactionId, paths, snapshots) {
629
- assertGitPublicationIdentity(target, publication);
630
636
  if (publication.commit === undefined) {
631
637
  throw new Error(`Git transaction ${transactionId} has no recorded publication decision.`);
632
638
  }
@@ -703,20 +709,6 @@ function readGitPublicationIdentity(target) {
703
709
  upstream: upstreamResult.stdout.trim(),
704
710
  };
705
711
  }
706
- function assertGitPublicationIdentity(target, publication) {
707
- if (target.source.kind !== "git") {
708
- throw new Error(`Git publication is bound to non-Git target "${target.source.name}".`);
709
- }
710
- const current = readGitPublicationIdentity(target);
711
- for (const key of ["repoPath", "branch", "remote", "mergeRef", "remoteUrl", "upstream"]) {
712
- if (current[key] !== publication[key]) {
713
- throw new Error(`Git publication target identity changed at ${key} for "${target.source.name}".`);
714
- }
715
- }
716
- if (JSON.stringify(current.pushUrls ?? []) !== JSON.stringify(publication.pushUrls ?? [])) {
717
- throw new Error(`Git publication target identity changed at pushUrls for "${target.source.name}".`);
718
- }
719
- }
720
712
  function normalizePublicationPaths(paths) {
721
713
  const normalized = new Set();
722
714
  for (const filePath of paths) {
@@ -951,9 +943,10 @@ export function resolveWorkingStashTarget(akmConfig, options = {}) {
951
943
  const requireWritable = options.requireWritable !== false;
952
944
  if (process.env.AKM_BUNDLE_DIR?.trim()) {
953
945
  const stashDir = resolveStashDir();
946
+ const resolvedStashDir = path.resolve(stashDir);
954
947
  const configured = configuredSources.find((source) => {
955
948
  const sourcePath = source.source.type === "filesystem" ? source.source.path : undefined;
956
- return sourcePath !== undefined && path.resolve(sourcePath) === path.resolve(stashDir);
949
+ return sourcePath !== undefined && bundleContentRoot(sourcePath, source.componentRoot) === resolvedStashDir;
957
950
  });
958
951
  if (configured) {
959
952
  const target = adaptConfiguredSource(configured);
@@ -27,19 +27,48 @@ export function captureFrozenDirectoryIdentity(rootInput, relativeCwd) {
27
27
  cwdInode: cwdStat.ino.toString(),
28
28
  });
29
29
  }
30
- export function assertFrozenDirectoryIdentity(identity) {
31
- let actual;
30
+ /**
31
+ * Re-verify, immediately before spawn, that a frozen cwd still resolves
32
+ * inside its execution root — path containment and resolved-path identity,
33
+ * NOT device/inode identity. Unlike the removed assertFrozenDirectoryIdentity,
34
+ * this does not compare device/inode numbers against the frozen snapshot: a
35
+ * remount, an rsync, or a container rebuild that leaves the same real
36
+ * content at the same real PATH must not abort a dispatch. What still
37
+ * throws is anything that changes the resolved real PATH itself — a
38
+ * symlink swap, an ancestor swap, a file replacing the directory, or the
39
+ * root itself being replaced by a symlink elsewhere (root-vs-cwd
40
+ * containment alone can't catch that last one, since both resolve together
41
+ * under the new root; comparing the root's own resolved path to its frozen
42
+ * value catches it).
43
+ */
44
+ export function assertFrozenDirectoryContained(identity) {
45
+ let realRoot;
46
+ let realCwd;
32
47
  try {
33
- const relative = path.relative(identity.requestedRoot, identity.requestedCwd);
34
- actual = captureFrozenDirectoryIdentity(identity.requestedRoot, relative);
48
+ realRoot = fs.realpathSync(identity.requestedRoot);
49
+ realCwd = fs.realpathSync(identity.requestedCwd);
35
50
  }
36
51
  catch {
37
52
  throw changed(identity.requestedCwd);
38
53
  }
39
- for (const key of ["realRoot", "rootDevice", "rootInode", "realCwd", "cwdDevice", "cwdInode"]) {
40
- if (actual[key] !== identity[key])
41
- throw changed(identity.requestedCwd);
54
+ if (realRoot !== identity.realRoot)
55
+ throw changed(identity.requestedCwd);
56
+ const relative = path.relative(realRoot, realCwd);
57
+ if (relative.startsWith("..") || path.isAbsolute(relative)) {
58
+ throw changed(identity.requestedCwd);
42
59
  }
60
+ let cwdStat;
61
+ try {
62
+ cwdStat = fs.statSync(realCwd, { bigint: true });
63
+ }
64
+ catch {
65
+ throw changed(identity.requestedCwd);
66
+ }
67
+ if (!cwdStat.isDirectory())
68
+ throw changed(identity.requestedCwd);
69
+ }
70
+ function changed(cwd) {
71
+ return new UsageError(`Frozen execution cwd ${cwd} changed and no longer resolves inside its root.`, "PATH_ESCAPE_VIOLATION");
43
72
  }
44
73
  function requireContained(root, candidate) {
45
74
  const relative = path.relative(root, candidate);
@@ -47,6 +76,3 @@ function requireContained(root, candidate) {
47
76
  throw new UsageError(`${candidate} escapes its execution root.`, "PATH_ESCAPE_VIOLATION");
48
77
  }
49
78
  }
50
- function changed(cwd) {
51
- return new UsageError(`Frozen execution cwd ${cwd} changed after publication.`, "RESOURCE_ALREADY_EXISTS");
52
- }
@@ -68,11 +68,7 @@ function combineConfidence(...parts) {
68
68
  return out;
69
69
  }
70
70
  function toConfidenceMultiplier(rawConfidence, weights) {
71
- if (weights.confidenceMode === "off")
72
- return 1;
73
71
  const confidence = normalizeConfidence(rawConfidence) ?? 1;
74
- if (weights.confidenceMode === "multiply")
75
- return confidence;
76
72
  const blendWeight = Math.max(0, Math.min(1, weights.confidenceWeight));
77
73
  return 1 - blendWeight + blendWeight * confidence;
78
74
  }
@@ -1,15 +1,36 @@
1
1
  // This Source Code Form is subject to the terms of the Mozilla Public
2
2
  // License, v. 2.0. If a copy of the MPL was not distributed with this
3
3
  // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
+ /**
5
+ * Asset-mutation lease — serializes writes to real, authored user content
6
+ * (source updates, `akm remember`, proposal apply, ...) so two concurrent
7
+ * writers cannot both pass a git exact-path preflight check before either
8
+ * commits.
9
+ *
10
+ * This module previously also gated full index REBUILDS behind the same
11
+ * lease with a 12-hour age-based stale-reclaim window (#872). That guard was
12
+ * removed: the index is a regenerable cache, concurrent rebuilds only waste
13
+ * work rather than corrupt anything, and a live-but-wedged holder passed the
14
+ * PID-liveness check forever, so only the 12h clock could ever free it —
15
+ * which cost one real install a half-day indexing outage. Index rebuilds no
16
+ * longer take any lease.
17
+ *
18
+ * What remains here guards actual data loss (a lost or conflicting git
19
+ * commit), so per AGENTS.md `## Defensive Code` it stays — but with the same
20
+ * fix applied: no age-based stale-reclaim. A holder is only ever reclaimed
21
+ * once its PID is verifiably dead; a live-but-wedged holder makes an
22
+ * acquisition attempt wait (bounded by `maxWaitMs`) or fail, never silently
23
+ * override a lease someone might still be using.
24
+ */
4
25
  import { AsyncLocalStorage } from "node:async_hooks";
5
26
  import fs from "node:fs";
6
27
  import path from "node:path";
7
28
  import { createLockPayload, probeLock, reclaimStaleLock, releaseLock, tryAcquireLockSync, } from "../core/file-lock.js";
8
29
  import { tryAcquireMaintenanceBarrier } from "../core/maintenance-barrier.js";
9
30
  import { getDbPath, getIndexWriterLockPath } from "../core/paths.js";
10
- const INDEX_WRITER_LOCK_STALE_AFTER_MS = 12 * 60 * 60 * 1000;
11
- const INDEX_WRITER_WAIT_MS = 100;
12
- const DEFAULT_INDEX_WRITER_MAX_WAIT_MS = 10 * 60 * 1000;
31
+ import { sleepSync } from "../runtime.js";
32
+ const ASSET_MUTATION_WAIT_MS = 100;
33
+ const DEFAULT_ASSET_MUTATION_MAX_WAIT_MS = 10 * 60 * 1000;
13
34
  const leaseContext = new AsyncLocalStorage();
14
35
  function buildPayload(purpose) {
15
36
  return createLockPayload({
@@ -24,7 +45,7 @@ function delay(ms) {
24
45
  function throwIfAborted(signal) {
25
46
  if (!signal?.aborted)
26
47
  return;
27
- throw signal.reason instanceof Error ? signal.reason : new Error("index writer wait aborted");
48
+ throw signal.reason instanceof Error ? signal.reason : new Error("asset mutation lease wait aborted");
28
49
  }
29
50
  function createLease(lockPath, ownership) {
30
51
  const exitHandler = () => releaseLock(ownership);
@@ -41,7 +62,7 @@ function createLease(lockPath, ownership) {
41
62
  },
42
63
  };
43
64
  }
44
- function tryAcquireIndexWriterLease(lockPath, purpose) {
65
+ function tryAcquireAssetMutationLease(lockPath, purpose) {
45
66
  while (true) {
46
67
  const releaseBarrier = tryAcquireMaintenanceBarrier();
47
68
  if (!releaseBarrier)
@@ -50,7 +71,8 @@ function tryAcquireIndexWriterLease(lockPath, purpose) {
50
71
  const ownership = tryAcquireLockSync(lockPath, buildPayload(purpose));
51
72
  if (ownership)
52
73
  return createLease(lockPath, ownership);
53
- const probe = probeLock(lockPath, { staleAfterMs: INDEX_WRITER_LOCK_STALE_AFTER_MS });
74
+ // No `staleAfterMs`: only a verifiably dead holder is ever reclaimed.
75
+ const probe = probeLock(lockPath);
54
76
  if (probe.state !== "stale" || !reclaimStaleLock(lockPath, probe))
55
77
  return undefined;
56
78
  }
@@ -59,16 +81,16 @@ function tryAcquireIndexWriterLease(lockPath, purpose) {
59
81
  }
60
82
  }
61
83
  }
62
- export async function acquireIndexWriterLease(options) {
84
+ export async function acquireAssetMutationLease(options) {
63
85
  const mode = options.mode ?? "wait";
64
86
  const lockPath = getIndexWriterLockPath();
65
87
  const startedAt = Date.now();
66
- const maxWaitMs = options.maxWaitMs ?? DEFAULT_INDEX_WRITER_MAX_WAIT_MS;
88
+ const maxWaitMs = options.maxWaitMs ?? DEFAULT_ASSET_MUTATION_MAX_WAIT_MS;
67
89
  fs.mkdirSync(path.dirname(lockPath), { recursive: true });
68
90
  let lastWaitNoticeMs = 0;
69
91
  while (true) {
70
92
  throwIfAborted(options.signal);
71
- const lease = tryAcquireIndexWriterLease(lockPath, options.purpose);
93
+ const lease = tryAcquireAssetMutationLease(lockPath, options.purpose);
72
94
  if (lease) {
73
95
  options.onAcquired?.({ waitedMs: Date.now() - startedAt });
74
96
  return lease;
@@ -79,26 +101,27 @@ export async function acquireIndexWriterLease(options) {
79
101
  // attempt, so a caller with maxWaitMs:0 still gets one chance at a free lock
80
102
  // instead of throwing before it ever tries.
81
103
  if (maxWaitMs >= 0 && Date.now() - startedAt >= maxWaitMs) {
82
- throw new Error(`timed out waiting for index writer lease for ${options.purpose}`);
104
+ throw new Error(`timed out waiting for asset mutation lease for ${options.purpose}`);
83
105
  }
84
106
  const waitedMs = Date.now() - startedAt;
85
107
  if (waitedMs - lastWaitNoticeMs >= 15000) {
86
108
  options.onWait?.({ waitedMs });
87
109
  lastWaitNoticeMs = waitedMs;
88
110
  }
89
- await delay(INDEX_WRITER_WAIT_MS);
111
+ await delay(ASSET_MUTATION_WAIT_MS);
90
112
  }
91
113
  }
92
- export async function withIndexWriterLease(options, run) {
114
+ /** Asset writes share one lease so two concurrent writers cannot both pass preflight before either commits. */
115
+ export async function withAssetMutationLease(purpose, run) {
93
116
  const lockPath = getIndexWriterLockPath();
94
117
  const inherited = leaseContext.getStore();
95
118
  if (inherited?.has(lockPath))
96
119
  return run();
97
120
  const context = inherited ?? new Set();
98
121
  const execute = async () => {
99
- const lease = await acquireIndexWriterLease(options);
122
+ const lease = await acquireAssetMutationLease({ purpose });
100
123
  if (!lease)
101
- throw new Error(`index writer lease unavailable for ${options.purpose}`);
124
+ throw new Error(`asset mutation lease unavailable for ${purpose}`);
102
125
  context.add(lockPath);
103
126
  try {
104
127
  return await run();
@@ -110,10 +133,6 @@ export async function withIndexWriterLease(options, run) {
110
133
  };
111
134
  return inherited ? execute() : leaseContext.run(context, execute);
112
135
  }
113
- /** Asset writes and index rebuilds share one lease so scans cannot publish stale snapshots. */
114
- export function withAssetMutationLease(purpose, run) {
115
- return withIndexWriterLease({ purpose }, run);
116
- }
117
136
  /** Synchronous asset-write boundary over the same interprocess lease. */
118
137
  export function withAssetMutationLeaseSync(purpose, run) {
119
138
  const lockPath = getIndexWriterLockPath();
@@ -126,12 +145,12 @@ export function withAssetMutationLeaseSync(purpose, run) {
126
145
  fs.mkdirSync(path.dirname(lockPath), { recursive: true });
127
146
  let lease;
128
147
  while (!lease) {
129
- lease = tryAcquireIndexWriterLease(lockPath, purpose);
148
+ lease = tryAcquireAssetMutationLease(lockPath, purpose);
130
149
  if (!lease) {
131
- if (Date.now() - startedAt >= DEFAULT_INDEX_WRITER_MAX_WAIT_MS) {
132
- throw new Error(`timed out waiting for index writer lease for ${purpose}`);
150
+ if (Date.now() - startedAt >= DEFAULT_ASSET_MUTATION_MAX_WAIT_MS) {
151
+ throw new Error(`timed out waiting for asset mutation lease for ${purpose}`);
133
152
  }
134
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, INDEX_WRITER_WAIT_MS);
153
+ sleepSync(ASSET_MUTATION_WAIT_MS);
135
154
  }
136
155
  }
137
156
  context.add(lockPath);
@@ -145,6 +164,6 @@ export function withAssetMutationLeaseSync(purpose, run) {
145
164
  };
146
165
  return inherited ? execute() : leaseContext.run(context, execute);
147
166
  }
148
- export function probeIndexWriterLease() {
149
- return probeLock(getIndexWriterLockPath(), { staleAfterMs: INDEX_WRITER_LOCK_STALE_AFTER_MS });
167
+ export function probeAssetMutationLease() {
168
+ return probeLock(getIndexWriterLockPath());
150
169
  }
@@ -29,9 +29,8 @@ import { getDbPath } from "../core/paths.js";
29
29
  import { warn, warnVerbose } from "../core/warn.js";
30
30
  import { closeDatabase, openExistingDatabase } from "../storage/repositories/index-connection.js";
31
31
  import { deleteEntriesByIds, getEntryCount, upsertEntry } from "../storage/repositories/index-entries-repository.js";
32
- import { withIndexWriterLease } from "./index-writer-lock.js";
33
32
  import { deriveEntryProvenance, deriveInstallations } from "./installations.js";
34
- import { generateEmbeddingsForDb, publishTargetedSemanticStatus } from "./materialize-embeddings.js";
33
+ import { generateEmbeddingsForDb, publishTargetedEmbeddingMeta } from "./materialize-embeddings.js";
35
34
  import { drainDirDocuments } from "./scan/drain-dir.js";
36
35
  import { buildSearchText } from "./search/search-fields.js";
37
36
  import { buildFileContext } from "./walk/file-context.js";
@@ -57,7 +56,7 @@ export const WRITE_PATH_INDEX_BUSY_TIMEOUT_MS = 5_000;
57
56
  */
58
57
  export async function indexWrittenAssets(stashDir, filePaths, options = {}) {
59
58
  try {
60
- return await withIndexWriterLease({ purpose: "index-written-assets" }, async () => {
59
+ return await (async () => {
61
60
  const dbPath = getDbPath();
62
61
  // `true` here means "the index is in the state the caller expects" — and
63
62
  // `acceptProposal` advances its journal to `index-finalized` on the
@@ -181,15 +180,15 @@ export async function indexWrittenAssets(stashDir, filePaths, options = {}) {
181
180
  })();
182
181
  if (mutated) {
183
182
  const config = loadConfig();
184
- const embeddingResult = await generateEmbeddingsForDb(db, config, () => { }, undefined, [...targetEntryIds]);
185
- publishTargetedSemanticStatus(db, config, embeddingResult);
183
+ await generateEmbeddingsForDb(db, config, () => { }, undefined, [...targetEntryIds]);
184
+ publishTargetedEmbeddingMeta(db, config);
186
185
  }
187
186
  }
188
187
  finally {
189
188
  closeDatabase(db);
190
189
  }
191
190
  return true;
192
- });
191
+ })();
193
192
  }
194
193
  catch (error) {
195
194
  // A permission fault is the one failure the next full index will NOT heal,
@@ -27,7 +27,6 @@ import { upsertUtilityScore } from "../storage/repositories/index-utility-reposi
27
27
  import { getEmbeddingCount, isVecAvailable, isVecFastPathReady, warnIfVecMissing, } from "../storage/repositories/index-vec-repository.js";
28
28
  import { assertIndexedWorkflowSourceIdentity, WorkflowSourceIdentityError } from "../workflows/source-files.js";
29
29
  import { deleteStoredGraph } from "./db/graph-db.js";
30
- import { withIndexWriterLease } from "./index-writer-lock.js";
31
30
  import { deriveEntryProvenance, deriveInstallations } from "./installations.js";
32
31
  import { indexedPathMatchesOwner, resolveAdapterConceptOwner, } from "./lookup/adapter-concept-owner.js";
33
32
  import { generateEmbeddingsForDb } from "./materialize-embeddings.js";
@@ -35,7 +34,6 @@ import { canUseIncrementalSkip, computeDirFingerprint, getCachedZeroRowDirState,
35
34
  import { isEnrichmentComplete, isWorkflowSkipWarning } from "./passes/metadata.js";
36
35
  import { drainDirDocuments } from "./scan/drain-dir.js";
37
36
  import { buildSearchText } from "./search/search-fields.js";
38
- import { clearSemanticStatus, deriveSemanticProviderFingerprint, writeSemanticStatus } from "./search/semantic-status.js";
39
37
  import { purgeOldUsageEvents } from "./usage/usage-events.js";
40
38
  import { walkStashFlatWithStatus } from "./walk/walker.js";
41
39
  function collectLoweringNotices(target, notices) {
@@ -265,27 +263,6 @@ async function runFinalizePhase(ctx, deferredUpdateTransaction) {
265
263
  const semanticEntryCount = getEmbeddableEntryCount(db);
266
264
  onProgress({ phase: "finalize", message: "Verifying semantic search state." });
267
265
  const verification = verifyIndexState(db, config, semanticEntryCount, embeddingResult);
268
- const persistSemanticStatus = () => {
269
- if (config.semanticSearchMode === "off") {
270
- clearSemanticStatus();
271
- return;
272
- }
273
- writeSemanticStatus({
274
- status: verification.semanticStatus === "disabled" ? "pending" : verification.semanticStatus,
275
- ...(embeddingResult.reason ? { reason: embeddingResult.reason } : {}),
276
- ...(embeddingResult.message ? { message: embeddingResult.message } : {}),
277
- providerFingerprint: deriveSemanticProviderFingerprint(config.embedding),
278
- lastCheckedAt: new Date().toISOString(),
279
- entryCount: verification.entryCount,
280
- embeddingCount: verification.embeddingCount,
281
- });
282
- };
283
- if (deferredUpdateTransaction) {
284
- deferredUpdateTransaction.afterCommit = persistSemanticStatus;
285
- }
286
- else {
287
- persistSemanticStatus();
288
- }
289
266
  onProgress({ phase: "verify", message: verification.message });
290
267
  // Store verification result and totalEntries on ctx for the caller to use
291
268
  ctx.verification = verification;
@@ -492,20 +469,7 @@ async function akmIndexReal(options) {
492
469
  "Pass `akm index --clean --dry-run`, or drop `--dry-run` to run a real index.", "INVALID_FLAG_VALUE", "Run `akm index --clean --dry-run` to preview, or `akm index --clean` to apply.");
493
470
  }
494
471
  const requestedAt = Date.now();
495
- let acquiredAt = requestedAt;
496
- return withIndexWriterLease({
497
- purpose: "akm-index",
498
- signal: options?.signal,
499
- onWait: ({ waitedMs }) => {
500
- options?.onProgress?.({
501
- phase: "preflight",
502
- message: `Waiting for index writer lease (${Math.round(waitedMs / 1000)}s elapsed).`,
503
- });
504
- },
505
- onAcquired: ({ waitedMs }) => {
506
- acquiredAt = requestedAt + waitedMs;
507
- },
508
- }, async () => {
472
+ return (async () => {
509
473
  const stashDir = options.stashDir;
510
474
  const onProgress = options?.onProgress ?? (() => { });
511
475
  const signal = options?.signal;
@@ -644,7 +608,6 @@ async function akmIndexReal(options) {
644
608
  finalizeMs: timing.tFinalizeEnd - timing.tFinalizeStart,
645
609
  cleanMs: clean ? cleanEnd - cleanStart : 0,
646
610
  preflightMs: timing.t0 - requestedAt,
647
- leaseWaitMs: acquiredAt - requestedAt,
648
611
  sourceCacheMs: sourceCacheEnd - sourceCacheStart,
649
612
  endToEndMs: Date.now() - requestedAt,
650
613
  },
@@ -658,7 +621,7 @@ async function akmIndexReal(options) {
658
621
  if (!borrowedUpdateDb)
659
622
  closeDatabase(db);
660
623
  }
661
- });
624
+ })();
662
625
  }
663
626
  function buildIndexedSourceOwners(sources) {
664
627
  const installations = deriveInstallations([...sources]);