akm-cli 0.9.16-alpha.1 → 0.9.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +56 -132
- package/dist/assets/hints/cli-hints-full.md +13 -6
- package/dist/assets/tasks/core/index-refresh.yml +1 -1
- package/dist/assets/tasks/improve/akm-improve-catchup.yml +3 -6
- package/dist/cli/retired-commands.js +0 -4
- package/dist/cli/unknown-flags.js +3 -36
- package/dist/commands/env/env-binding.js +4 -4
- package/dist/commands/env/env-cli.js +3 -3
- package/dist/commands/improve/collapse-detector.js +2 -2
- package/dist/commands/improve/consolidate.js +4 -6
- package/dist/commands/improve/improve-cli.js +20 -15
- package/dist/commands/improve/reflect.js +23 -2
- package/dist/commands/lint/base-linter.js +9 -0
- package/dist/commands/lint/env-key-rules.js +2 -2
- package/dist/commands/proposal/propose.js +15 -1
- package/dist/commands/proposal/repository.js +3 -12
- package/dist/commands/proposal/validators/proposal-quality-validators.js +40 -3
- package/dist/commands/proposal/validators/proposal-validators.js +5 -4
- package/dist/commands/read/curate.js +44 -34
- package/dist/commands/read/search.js +35 -54
- package/dist/commands/read/show.js +21 -2
- package/dist/commands/registry-cli.js +5 -5
- package/dist/commands/sources/add-cli.js +59 -16
- package/dist/commands/sources/bundle-cli.js +35 -11
- package/dist/commands/sources/bundle-config-ops.js +30 -0
- package/dist/commands/sources/dangerous-env-audit.js +4 -4
- package/dist/commands/sources/info.js +8 -8
- package/dist/commands/sources/installed-stashes.js +55 -61
- package/dist/commands/sources/source-add.js +39 -38
- package/dist/commands/sources/source-manage.js +34 -12
- package/dist/commands/sources/stash-cli.js +111 -119
- package/dist/commands/sources/stash-skeleton.js +6 -3
- package/dist/commands/tasks/explain.js +4 -1
- package/dist/commands/tasks/tasks-cli.js +31 -9
- package/dist/commands/tasks/tasks.js +239 -194
- package/dist/commands/tasks/validate.js +20 -32
- package/dist/core/activation-policy.js +4 -4
- package/dist/core/adapter/adapters/akm-adapter.js +8 -35
- package/dist/core/adapter/adapters/akm-metadata.js +1 -11
- package/dist/core/adapter/execution-source.js +10 -29
- package/dist/core/asset/asset-placement.js +0 -35
- package/dist/core/config/config-schema.js +64 -8
- package/dist/core/config/config-sources.js +96 -2
- package/dist/core/config/config.js +190 -24
- package/dist/core/config/legacy-source-shape-shim.js +9 -0
- package/dist/core/config/schema/embedding.js +30 -7
- package/dist/core/config/schema/execution.js +23 -0
- package/dist/core/config/schema/experimental.js +1 -1
- package/dist/core/config/schema/scheduler.js +20 -0
- package/dist/core/config/schema/search.js +10 -12
- package/dist/core/config/schema/sources-bundles.js +32 -1
- package/dist/core/content-safety.js +52 -0
- package/dist/core/errors.js +2 -5
- package/dist/core/maintenance-barrier.js +11 -13
- package/dist/core/paths.js +11 -0
- package/dist/core/run-lock.js +2 -5
- package/dist/core/state/migrations.js +1 -26
- package/dist/core/state-db.js +27 -63
- package/dist/core/type-presentation.js +1 -1
- package/dist/core/write-source.js +13 -8
- package/dist/indexer/bundle-identity-guard.js +45 -8
- package/dist/indexer/ensure-index.js +0 -5
- package/dist/indexer/index-db-contention.js +56 -0
- package/dist/indexer/index-rebuild-lock.js +73 -0
- package/dist/indexer/index-written-assets.js +171 -133
- package/dist/indexer/indexer.js +1621 -458
- package/dist/indexer/lookup/adapter-concept-owner.js +5 -19
- package/dist/indexer/materialize-embeddings.js +785 -0
- package/dist/indexer/passes/dir-staleness.js +161 -0
- package/dist/indexer/passes/metadata.js +1 -18
- package/dist/indexer/scan/drain-dir.js +70 -27
- package/dist/indexer/search/db-search.js +89 -373
- package/dist/indexer/search/ranking-contributors.js +16 -21
- package/dist/indexer/search/ranking.js +57 -135
- package/dist/indexer/search/search-source.js +29 -11
- package/dist/integrations/agent/execution-lowering.js +3 -2
- package/dist/integrations/agent/execution-preparation.js +32 -1
- package/dist/integrations/agent/prompts.js +1 -1
- package/dist/integrations/agent/request-lowering.js +3 -2
- package/dist/llm/client.js +3 -11
- package/dist/llm/embedder.js +3 -10
- package/dist/llm/embedders/remote.js +104 -133
- package/dist/llm/feature-gate.js +2 -4
- package/dist/llm/rerank-client.js +3 -3
- package/dist/output/html-render.js +2 -1
- package/dist/output/shapes/passthrough.js +2 -1
- package/dist/output/stdout.js +24 -0
- package/dist/output/text/command-format.js +13 -19
- package/dist/output/text/helpers.js +1 -1
- package/dist/output/text/index.js +2 -5
- package/dist/output/text.js +4 -3
- package/dist/registry/resolve.js +37 -10
- package/dist/scripts/akm-migrate-node.js +15197 -11351
- package/dist/scripts/akm-migrate.js +15514 -11668
- package/dist/setup/semantic-assets.js +2 -2
- package/dist/setup/setup.js +3 -3
- package/dist/setup/steps/connection.js +2 -3
- package/dist/setup/steps/tasks.js +29 -36
- package/dist/sources/providers/git-install.js +17 -11
- package/dist/sources/providers/git-provider.js +12 -5
- package/dist/sources/providers/git-stash.js +38 -16
- package/dist/sources/snapshot-fetchers/website-ingest.js +3 -3
- package/dist/storage/repositories/embedding-salvage-repository.js +184 -0
- package/dist/storage/repositories/index-connection.js +3 -1
- package/dist/storage/repositories/index-entries-repository.js +68 -77
- package/dist/storage/repositories/index-entry-schema.js +25 -16
- package/dist/storage/repositories/index-fts-repository.js +263 -29
- package/dist/storage/repositories/index-meta-repository.js +29 -0
- package/dist/storage/repositories/index-schema.js +122 -115
- package/dist/storage/repositories/index-utility-repository.js +1 -1
- package/dist/storage/repositories/index-vec-repository.js +435 -22
- package/dist/tasks/activation-config.js +90 -0
- package/dist/tasks/backends/cron.js +9 -0
- package/dist/tasks/backends/launchd.js +1 -0
- package/dist/tasks/backends/schtasks.js +2 -0
- package/dist/tasks/embedded.js +4 -5
- package/dist/tasks/scheduler-binding.js +2 -2
- package/dist/tasks/scheduler-sync-preview.js +8 -1
- package/dist/tasks/scheduler-sync.js +19 -10
- package/dist/tasks/source/parse-task-source.js +10 -113
- package/dist/tasks/source/project-v4.js +2 -2
- package/dist/tasks/source/task-source-v4.js +4 -12
- package/dist/tasks/source/task-to-v3.js +4 -12
- package/dist/tasks/source/task-to-v4.js +40 -7
- package/docs/migration/README.md +1 -0
- package/docs/migration/release-notes/0.9.15.md +36 -34
- package/docs/migration/release-notes/0.9.16.md +60 -98
- package/docs/migration/release-notes/README.md +0 -5
- package/docs/migration/v0.9.1-to-v0.9.2.md +6 -9
- package/docs/reference/cli.md +124 -122
- package/docs/reference/configuration.md +137 -133
- package/docs/reference/data-and-telemetry.md +1 -2
- package/docs/reference/tasks.md +34 -29
- package/package.json +1 -1
- package/schemas/akm-config.json +170 -6
- package/schemas/akm-task.json +1 -2
- package/dist/commands/sources/index-status.js +0 -99
- package/dist/core/hash.js +0 -18
- package/dist/indexer/drain.js +0 -306
- package/dist/indexer/embedding-identity.js +0 -20
- package/dist/indexer/enrich.js +0 -260
- package/dist/indexer/reconcile.js +0 -890
- package/dist/indexer/scan/parse-file.js +0 -66
- package/dist/indexer/units/unit.js +0 -159
- package/dist/llm/embedders/provider-limits.js +0 -288
- package/dist/storage/repositories/files-repository.js +0 -181
- package/dist/storage/repositories/units-repository.js +0 -510
package/dist/core/errors.js
CHANGED
|
@@ -18,7 +18,6 @@ const CONFIG_HINTS = {
|
|
|
18
18
|
UNKNOWN_IMPROVE_STRATEGY: "Pass one of the listed strategy names to `--strategy`, or define it under `improve.strategies`. Names are case-sensitive.",
|
|
19
19
|
EXECUTION_NOT_AUTHORIZED: "Change the selected tools or update the machine/user execution policy, then retry.",
|
|
20
20
|
SECRET_REFERENCE_UNRESOLVED: "Check the secret exists (`akm secret list`) and the name after `secret://` matches, or run `akm secret set <name> <value>` to store it.",
|
|
21
|
-
EMBEDDING_VEC_UNAVAILABLE: "Install sqlite-vec for unit-level semantic search, or rely on lexical search until it is available.",
|
|
22
21
|
};
|
|
23
22
|
// Code-review finding: COMPOSITION_INVALID covers several unrelated causes
|
|
24
23
|
// (a rejected with:, a multi-job source, a composition cycle/depth/size
|
|
@@ -83,10 +82,8 @@ const USAGE_HINTS = {
|
|
|
83
82
|
const TRANSIENT_HINTS = {
|
|
84
83
|
RUN_LEASE_HELD: "Wait for the named engine invocation to finish or for the lease to expire, then retry. `akm workflow status <id>` shows the current lease.",
|
|
85
84
|
STATE_DB_CONTENDED: "Another akm process is writing state.db right now. Wait a few seconds and retry; commands that support --skip-if-locked can skip instead of failing.",
|
|
86
|
-
INDEX_DB_CONTENDED: "Another akm process is writing index.db
|
|
87
|
-
|
|
88
|
-
MAINTENANCE_BARRIER_BUSY: "Another akm process is registering a lock or lease right now. Retry shortly, or pass --skip-if-locked on " +
|
|
89
|
-
"scheduled improve runs to skip gracefully instead — workflow run does not treat this code as skippable.",
|
|
85
|
+
INDEX_DB_CONTENDED: "Another akm process is writing index.db; retry shortly, or pass --skip-if-locked on scheduled runs.",
|
|
86
|
+
MAINTENANCE_BARRIER_BUSY: "Another akm process is registering a lock or lease right now. Retry shortly, or pass --skip-if-locked on scheduled index/improve/workflow runs.",
|
|
90
87
|
IMPROVE_LOCK_HELD: "Another akm improve run holds the whole-run lock right now. Wait for it to finish and retry, or pass --skip-if-locked on scheduled runs.",
|
|
91
88
|
};
|
|
92
89
|
/** Default hint for each NotFoundError code. */
|
|
@@ -27,20 +27,18 @@ const heldBarrierContext = new AsyncLocalStorage();
|
|
|
27
27
|
const MAINTENANCE_BARRIER_STALE_AFTER_MS = 5 * 60 * 1000;
|
|
28
28
|
/**
|
|
29
29
|
* The barrier normally holds for one lock-file write — sub-millisecond on
|
|
30
|
-
* any real filesystem. Two akm processes racing to register a lock
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
* after the barrier releases — that thing's own held/skip/throw/wait
|
|
41
|
-
* policy belongs to its caller, not to the barrier (#872).
|
|
30
|
+
* any real filesystem. Two akm processes racing to register a lock in the
|
|
31
|
+
* very same instant (e.g. two `akm index` runs a scheduler launched back to
|
|
32
|
+
* back) can still collide on it; retrying briefly resolves that ordinary
|
|
33
|
+
* case instead of failing a legitimate concurrent invocation outright
|
|
34
|
+
* (field follow-up to #956, G1). Five seconds matches the async and
|
|
35
|
+
* synchronous-activity registration paths below and tolerates a holder being
|
|
36
|
+
* descheduled under heavy process-shard load; the barrier's normal hold time
|
|
37
|
+
* remains sub-millisecond. The bound still surfaces a genuinely wedged holder
|
|
38
|
+
* promptly and never applies to the rebuild lock itself, which stays
|
|
39
|
+
* non-blocking (#872).
|
|
42
40
|
*/
|
|
43
|
-
const MAINTENANCE_BARRIER_BUSY_RETRY_BOUND_MS =
|
|
41
|
+
const MAINTENANCE_BARRIER_BUSY_RETRY_BOUND_MS = 5_000;
|
|
44
42
|
let busyRetryBoundMsForTests;
|
|
45
43
|
/** Test-only override for {@link MAINTENANCE_BARRIER_BUSY_RETRY_BOUND_MS}, so a unit test can exercise the
|
|
46
44
|
* exhausted-retry throw without a real ~1.5s wait. Restored via tests/_helpers/seams.ts's resetAllSeams(). */
|
package/dist/core/paths.js
CHANGED
|
@@ -234,6 +234,17 @@ export function getDbPath(env = process.env) {
|
|
|
234
234
|
export function getIndexWriterLockPath() {
|
|
235
235
|
return path.join(getDataDir(), "index.db.write.lock");
|
|
236
236
|
}
|
|
237
|
+
/**
|
|
238
|
+
* Path to the opt-in, PID-liveness-only rebuild lock an explicit `akm index`
|
|
239
|
+
* run acquires (#956). Distinct from {@link getIndexWriterLockPath}'s
|
|
240
|
+
* `index.db.write.lock`, which is the asset-mutation lease and unrelated to
|
|
241
|
+
* indexing since #872 — this lock never blocks and is never required, it
|
|
242
|
+
* only lets a scheduled/opportunistic `akm index --skip-if-locked` step
|
|
243
|
+
* aside instead of contending with a run already in progress.
|
|
244
|
+
*/
|
|
245
|
+
export function getIndexRebuildLockPath() {
|
|
246
|
+
return path.join(getDataDir(), "index.rebuild.lock");
|
|
247
|
+
}
|
|
237
248
|
export function getMaintenanceBarrierPath() {
|
|
238
249
|
return path.join(getDataDir(), "maintenance.barrier.lock");
|
|
239
250
|
}
|
package/dist/core/run-lock.js
CHANGED
|
@@ -3,11 +3,8 @@
|
|
|
3
3
|
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
4
|
/**
|
|
5
5
|
* PID-liveness-only run lock — the shared mechanics behind `akm improve`'s
|
|
6
|
-
* whole-run lock (`commands/improve/locks.ts`)
|
|
7
|
-
*
|
|
8
|
-
* deleted that consumer (every index write is now a short, idempotent,
|
|
9
|
-
* content-addressed transaction, so there is no rebuild to serialize), and
|
|
10
|
-
* `improve`'s lock is this module's only caller today.
|
|
6
|
+
* whole-run lock (`commands/improve/locks.ts`) and the index rebuild lock
|
|
7
|
+
* (`indexer/index-rebuild-lock.ts`, #956).
|
|
11
8
|
*
|
|
12
9
|
* No `staleAfterMs`: only a verifiably dead holder is ever reclaimed. This is
|
|
13
10
|
* the #872 lesson encoded at the mechanics layer so every future caller gets
|
|
@@ -1200,33 +1200,12 @@ export function getStateMigrationSafety(migrationId) {
|
|
|
1200
1200
|
* Delegates to the shared SQLite migration engine; state.db has no
|
|
1201
1201
|
* pre-versioning bootstrap step, so no `bootstrap` hook is passed.
|
|
1202
1202
|
*
|
|
1203
|
-
* `freshDatabase` is the one exception to "single transaction per migration":
|
|
1204
|
-
* the entire registry is locked through the final migration and applied as
|
|
1205
|
-
* ONE transaction (mirroring the existing-unversioned prefix lock below).
|
|
1206
|
-
* Two akm processes can race to create the same brand-new state.db
|
|
1207
|
-
* (`openStateDatabase`'s file-reservation only decides who creates the file,
|
|
1208
|
-
* not who finishes initializing it first); with per-migration commits, the
|
|
1209
|
-
* loser could open its own connection between two of the winner's commits,
|
|
1210
|
-
* see a real-but-incomplete ledger, and — correctly believing this is an
|
|
1211
|
-
* ordinary existing database needing to catch up on pending migrations —
|
|
1212
|
-
* refuse historical-destructive migration 018 with the same message a
|
|
1213
|
-
* genuine legacy database gets. Applying every migration for a fresh
|
|
1214
|
-
* database atomically removes that partially-migrated state from view
|
|
1215
|
-
* entirely: a concurrent opener now only ever observes "nothing committed
|
|
1216
|
-
* yet" (treated as fresh) or "fully current" (nothing left to apply), never
|
|
1217
|
-
* the in-between. Safe to lose on a crash mid-bootstrap: a fresh database
|
|
1218
|
-
* has no prior data to preserve, so rolling back to nothing and letting the
|
|
1219
|
-
* next open retry from scratch is strictly fine.
|
|
1220
|
-
*
|
|
1221
1203
|
* Called automatically by `openStateDatabase()`.
|
|
1222
1204
|
*/
|
|
1223
1205
|
export function runMigrations(db, options) {
|
|
1224
1206
|
const initialMigration = STATE_MIGRATIONS[0];
|
|
1225
1207
|
if (!initialMigration)
|
|
1226
1208
|
throw new Error("State migration registry has no initial migration.");
|
|
1227
|
-
const finalMigration = STATE_MIGRATIONS[STATE_MIGRATIONS.length - 1];
|
|
1228
|
-
if (!finalMigration)
|
|
1229
|
-
throw new Error("State migration registry has no final migration.");
|
|
1230
1209
|
let existingUnversionedSnapshotPrepared = false;
|
|
1231
1210
|
const prepareExistingUnversionedState = (lockedDb) => {
|
|
1232
1211
|
if (existingUnversionedSnapshotPrepared)
|
|
@@ -1249,11 +1228,7 @@ export function runMigrations(db, options) {
|
|
|
1249
1228
|
existingUnversionedSnapshotPrepared = true;
|
|
1250
1229
|
};
|
|
1251
1230
|
runSqliteMigrations(db, STATE_MIGRATIONS, {
|
|
1252
|
-
lockInitialMigrationPrefixThrough: options?.existingUnversionedDatabase
|
|
1253
|
-
? "002-task-history-per-run"
|
|
1254
|
-
: options?.freshDatabase
|
|
1255
|
-
? finalMigration.id
|
|
1256
|
-
: undefined,
|
|
1231
|
+
lockInitialMigrationPrefixThrough: options?.existingUnversionedDatabase ? "002-task-history-per-run" : undefined,
|
|
1257
1232
|
beforeLedgerInitializationLocked(lockedDb) {
|
|
1258
1233
|
if (options?.freshDatabase)
|
|
1259
1234
|
return;
|
package/dist/core/state-db.js
CHANGED
|
@@ -442,24 +442,10 @@ export function openStateDatabase(dbPath, options) {
|
|
|
442
442
|
});
|
|
443
443
|
try {
|
|
444
444
|
preflight.exec("PRAGMA busy_timeout = 30000");
|
|
445
|
-
|
|
446
|
-
// statement, and without an explicit transaction SQLite auto-commits
|
|
447
|
-
// each separately, so they can see two different snapshots. A sibling
|
|
448
|
-
// process's fresh bootstrap is now one all-or-nothing transaction, so
|
|
449
|
-
// a commit landing between the two reads showed an empty ledger to the
|
|
450
|
-
// first and the sibling's freshly created tables to the second — the
|
|
451
|
-
// signature of a genuine legacy unversioned database, which this
|
|
452
|
-
// preflight then refused. Pinned to one snapshot, either nothing the
|
|
453
|
-
// sibling did is visible (empty ledger AND no tables, correctly fresh)
|
|
454
|
-
// or all of it is (a current ledger, so the refusal is never reached).
|
|
455
|
-
// A real legacy database, racing no one, reads exactly as before.
|
|
456
|
-
const { ledger, hasNoOtherTables } = preflight.transaction(() => ({
|
|
457
|
-
ledger: assertMigrationLedger(preflight, STATE_MIGRATIONS),
|
|
458
|
-
hasNoOtherTables: unversionedDatabaseHasNoTables(preflight),
|
|
459
|
-
}))();
|
|
445
|
+
const ledger = assertMigrationLedger(preflight, STATE_MIGRATIONS);
|
|
460
446
|
warnNewerStateLedger(ledger);
|
|
461
447
|
existingUnversionedDatabase = ledger.migrationIds.length === 0;
|
|
462
|
-
if (existingUnversionedDatabase &&
|
|
448
|
+
if (existingUnversionedDatabase && unversionedDatabaseHasNoTables(preflight)) {
|
|
463
449
|
existingUnversionedDatabase = false;
|
|
464
450
|
treatUnversionedAsFresh = true;
|
|
465
451
|
}
|
|
@@ -553,21 +539,7 @@ export function openStateDatabase(dbPath, options) {
|
|
|
553
539
|
if (freshReservation)
|
|
554
540
|
closeFileIdentity(freshReservation);
|
|
555
541
|
releaseActivity?.();
|
|
556
|
-
|
|
557
|
-
// `withImmediateWriteLock`, used by every migration this open can run,
|
|
558
|
-
// including a from-empty first open) throws the raw driver error after
|
|
559
|
-
// its own retry budget, not a `TransientError` — only
|
|
560
|
-
// `beginImmediateTransaction` below does that reclassification, and nothing
|
|
561
|
-
// upstream of it re-wraps a raw SQLITE_BUSY/LOCKED that surfaces from
|
|
562
|
-
// deeper in the open/migrate sequence (field follow-up: a from-empty
|
|
563
|
-
// first open racing a concurrent opener can still hit this under load).
|
|
564
|
-
// Reclassify uniformly here so the contract this function promises --
|
|
565
|
-
// contention is reported as `STATE_DB_CONTENDED` (exit 75), never a raw
|
|
566
|
-
// driver message (exit 70) -- holds regardless of which internal step
|
|
567
|
-
// the contention was observed at. Anything not contention-shaped
|
|
568
|
-
// (a genuine legacy-database refusal, corruption, a real schema error)
|
|
569
|
-
// rethrows exactly as raised.
|
|
570
|
-
throwBeginFailure(error, "state");
|
|
542
|
+
throw error;
|
|
571
543
|
}
|
|
572
544
|
}
|
|
573
545
|
/**
|
|
@@ -718,33 +690,6 @@ function sleepSyncMs(ms) {
|
|
|
718
690
|
return;
|
|
719
691
|
sleepSync(ms);
|
|
720
692
|
}
|
|
721
|
-
/**
|
|
722
|
-
* Reclassify an exhausted-retry BEGIN failure that is still contention-shaped
|
|
723
|
-
* (#948) into a `TransientError`, mirroring the RUN_LEASE_HELD precedent
|
|
724
|
-
* (`WorkflowRunsRepository.acquireEngineLease`): the driver text is accurate
|
|
725
|
-
* but unhelpful (`{"ok":false,"error":"database is locked"}`, exit
|
|
726
|
-
* 70/INTERNAL) — this instead reads as a retryable-shortly signal (exit 75,
|
|
727
|
-
* sysexits EX_TEMPFAIL — #948 addendum) with the original error preserved as
|
|
728
|
-
* `cause` for `--verbose`/debugging. A genuinely unrelated error (not
|
|
729
|
-
* contention-shaped) is rethrown exactly as raised, never reclassified.
|
|
730
|
-
*
|
|
731
|
-
* `dbKind` (field follow-up to #956) picks the reported identity: `"state"`
|
|
732
|
-
* (default, unchanged text) yields `STATE_DB_CONTENDED`; `"index"` yields
|
|
733
|
-
* `INDEX_DB_CONTENDED` with index.db's own message, mirroring
|
|
734
|
-
* `reclassifyIndexDbContention`'s text (`src/indexer/indexer.ts`) so a caller
|
|
735
|
-
* that reaches this helper directly and one that only reclassifies a raw
|
|
736
|
-
* driver error report the same thing.
|
|
737
|
-
*/
|
|
738
|
-
function throwBeginFailure(err, dbKind) {
|
|
739
|
-
if (isSqliteContentionError(err)) {
|
|
740
|
-
const contended = dbKind === "index"
|
|
741
|
-
? new TransientError("akm's index database is busy (another akm process is writing it); retry shortly.", "INDEX_DB_CONTENDED")
|
|
742
|
-
: new TransientError("akm's state database is busy (another akm process is writing it); retry shortly.", "STATE_DB_CONTENDED");
|
|
743
|
-
contended.cause = err;
|
|
744
|
-
throw contended;
|
|
745
|
-
}
|
|
746
|
-
throw err;
|
|
747
|
-
}
|
|
748
693
|
/**
|
|
749
694
|
* Open, but deliberately do not finish, an immediate transaction.
|
|
750
695
|
*
|
|
@@ -754,7 +699,26 @@ function throwBeginFailure(err, dbKind) {
|
|
|
754
699
|
* publication have all succeeded. The caller that asked for this split phase
|
|
755
700
|
* owns the matching COMMIT/ROLLBACK.
|
|
756
701
|
*/
|
|
757
|
-
|
|
702
|
+
/**
|
|
703
|
+
* Reclassify an exhausted-retry BEGIN failure that is still contention-shaped
|
|
704
|
+
* (#948) into a `TransientError("STATE_DB_CONTENDED")`, mirroring the
|
|
705
|
+
* RUN_LEASE_HELD precedent (`WorkflowRunsRepository.acquireEngineLease`): the
|
|
706
|
+
* driver text is accurate but unhelpful (`{"ok":false,"error":"database is
|
|
707
|
+
* locked"}`, exit 70/INTERNAL) — this instead reads as a retryable-shortly
|
|
708
|
+
* signal (exit 75, sysexits EX_TEMPFAIL — #948 addendum) with the original
|
|
709
|
+
* error preserved as `cause` for `--verbose`/debugging. A genuinely unrelated
|
|
710
|
+
* error (not contention-shaped) is rethrown exactly as raised, never
|
|
711
|
+
* reclassified.
|
|
712
|
+
*/
|
|
713
|
+
function throwBeginFailure(err) {
|
|
714
|
+
if (isSqliteContentionError(err)) {
|
|
715
|
+
const contended = new TransientError("akm's state database is busy (another akm process is writing it); retry shortly.", "STATE_DB_CONTENDED");
|
|
716
|
+
contended.cause = err;
|
|
717
|
+
throw contended;
|
|
718
|
+
}
|
|
719
|
+
throw err;
|
|
720
|
+
}
|
|
721
|
+
export function beginImmediateTransaction(db) {
|
|
758
722
|
if (db.inTransaction) {
|
|
759
723
|
throw new Error("beginImmediateTransaction requires a connection with no active transaction");
|
|
760
724
|
}
|
|
@@ -781,12 +745,12 @@ export function beginImmediateTransaction(db, dbKind = "state") {
|
|
|
781
745
|
sleepSyncMs(2 ** (attempt - 1));
|
|
782
746
|
continue;
|
|
783
747
|
}
|
|
784
|
-
throwBeginFailure(err
|
|
748
|
+
throwBeginFailure(err);
|
|
785
749
|
}
|
|
786
750
|
}
|
|
787
|
-
throwBeginFailure(lastBeginErr
|
|
751
|
+
throwBeginFailure(lastBeginErr);
|
|
788
752
|
}
|
|
789
|
-
export function withImmediateTransaction(db, fn
|
|
753
|
+
export function withImmediateTransaction(db, fn) {
|
|
790
754
|
// Re-entrancy guard (issue #686): if a transaction is already open on this
|
|
791
755
|
// connection (e.g. a nested withImmediateTransaction call inside an outer
|
|
792
756
|
// frame's fn), join it — run fn directly with no BEGIN/COMMIT/ROLLBACK of
|
|
@@ -797,7 +761,7 @@ export function withImmediateTransaction(db, fn, dbKind = "state") {
|
|
|
797
761
|
if (db.inTransaction) {
|
|
798
762
|
return fn();
|
|
799
763
|
}
|
|
800
|
-
beginImmediateTransaction(db
|
|
764
|
+
beginImmediateTransaction(db);
|
|
801
765
|
try {
|
|
802
766
|
const result = fn();
|
|
803
767
|
if (!db.inTransaction) {
|
|
@@ -105,7 +105,7 @@ export const TYPE_PRESENTATION = {
|
|
|
105
105
|
task: {
|
|
106
106
|
label: "Task",
|
|
107
107
|
renderer: "task-yaml",
|
|
108
|
-
action: (ref) => `akm show ${ref} -> inspect; akm task run <id> -> run now;
|
|
108
|
+
action: (ref) => `akm show ${ref} -> inspect; akm task run <id> -> run now; akm task enable|disable ${ref} -> change local scheduling`,
|
|
109
109
|
fragmentRef: false,
|
|
110
110
|
},
|
|
111
111
|
session: {
|
|
@@ -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 {
|
|
37
|
+
import { bundleKeyForContentRoot, resolveActiveConfiguredSources, 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, warnOnce } from "./warn.js";
|
|
@@ -897,12 +897,16 @@ function gitTargetNotMaterialized(target, contentRoot) {
|
|
|
897
897
|
* see plan §6 decision 3 for the rationale.
|
|
898
898
|
*/
|
|
899
899
|
export function resolveWriteTarget(akmConfig, explicitTarget, options = {}) {
|
|
900
|
-
const
|
|
900
|
+
const allConfiguredSources = resolveConfiguredSources(akmConfig);
|
|
901
|
+
const configuredSources = resolveActiveConfiguredSources(akmConfig);
|
|
901
902
|
const requireWritable = options.requireWritable !== false;
|
|
902
903
|
// 1. Explicit --target wins.
|
|
903
904
|
if (explicitTarget) {
|
|
904
905
|
const match = configuredSources.find((s) => s.name === explicitTarget);
|
|
905
906
|
if (!match) {
|
|
907
|
+
if (allConfiguredSources.some((source) => source.name === explicitTarget)) {
|
|
908
|
+
throw new UsageError(`Bundle "${explicitTarget}" is disabled.`, "INVALID_FLAG_VALUE");
|
|
909
|
+
}
|
|
906
910
|
throw new UsageError(`--target must reference a source name from your config. No source named "${explicitTarget}" is configured. Run \`akm bundle list\` to see available sources.`, "INVALID_FLAG_VALUE");
|
|
907
911
|
}
|
|
908
912
|
// Up-front writable check so an explicit --target fails fast with a
|
|
@@ -939,15 +943,13 @@ export function resolveWriteTarget(akmConfig, explicitTarget, options = {}) {
|
|
|
939
943
|
}
|
|
940
944
|
/** Resolve the implicit working stash without consulting `defaultWriteTarget`. */
|
|
941
945
|
export function resolveWorkingStashTarget(akmConfig, options = {}) {
|
|
942
|
-
const
|
|
946
|
+
const allConfiguredSources = resolveConfiguredSources(akmConfig);
|
|
947
|
+
const configuredSources = resolveActiveConfiguredSources(akmConfig);
|
|
943
948
|
const requireWritable = options.requireWritable !== false;
|
|
944
949
|
if (process.env.AKM_BUNDLE_DIR?.trim()) {
|
|
945
950
|
const stashDir = resolveStashDir();
|
|
946
|
-
const
|
|
947
|
-
const configured = configuredSources.find((source) =>
|
|
948
|
-
const sourcePath = source.source.type === "filesystem" ? source.source.path : undefined;
|
|
949
|
-
return sourcePath !== undefined && bundleContentRoot(sourcePath, source.componentRoot) === resolvedStashDir;
|
|
950
|
-
});
|
|
951
|
+
const configuredBundleId = bundleKeyForContentRoot(akmConfig, stashDir);
|
|
952
|
+
const configured = configuredSources.find((source) => source.name === configuredBundleId);
|
|
951
953
|
if (configured) {
|
|
952
954
|
const target = adaptConfiguredSource(configured);
|
|
953
955
|
if (requireWritable && !resolveWritable(target.config)) {
|
|
@@ -955,6 +957,9 @@ export function resolveWorkingStashTarget(akmConfig, options = {}) {
|
|
|
955
957
|
}
|
|
956
958
|
return { ...target, selector: undefined };
|
|
957
959
|
}
|
|
960
|
+
if (allConfiguredSources.some((source) => source.name === configuredBundleId)) {
|
|
961
|
+
throw new ConfigError("The AKM_BUNDLE_DIR source is disabled in config.", "INVALID_CONFIG_FILE");
|
|
962
|
+
}
|
|
958
963
|
const bundleId = deriveBundleId(undefined, stashDir, new Set(Object.keys(akmConfig.bundles ?? {})));
|
|
959
964
|
return {
|
|
960
965
|
source: { kind: "filesystem", name: bundleId, path: stashDir, adapterId: detectAdapterId(stashDir) },
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
* comparison over a non-empty index so the steady-state cost is a boolean check.
|
|
29
29
|
*/
|
|
30
30
|
import fs from "node:fs";
|
|
31
|
+
import path from "node:path";
|
|
31
32
|
import { loadConfig } from "../core/config/config.js";
|
|
32
33
|
import { rethrowIfTestIsolationError } from "../core/errors.js";
|
|
33
34
|
import { getDbPath } from "../core/paths.js";
|
|
@@ -39,8 +40,8 @@ let guardSettled = false;
|
|
|
39
40
|
export function resetBundleIdentityGuardForTests() {
|
|
40
41
|
guardSettled = false;
|
|
41
42
|
}
|
|
42
|
-
/**
|
|
43
|
-
function
|
|
43
|
+
/** Bundle/path ownership persisted in the index, or `undefined` when unreadable. */
|
|
44
|
+
function indexedBundlePaths(dbPath) {
|
|
44
45
|
if (!fs.existsSync(dbPath))
|
|
45
46
|
return undefined;
|
|
46
47
|
let db;
|
|
@@ -51,8 +52,9 @@ function indexBundlePrefixes(dbPath) {
|
|
|
51
52
|
if (!isCanonicalIndexGeneration(db))
|
|
52
53
|
return undefined;
|
|
53
54
|
return db
|
|
54
|
-
.prepare("SELECT DISTINCT bundle_id AS
|
|
55
|
-
|
|
55
|
+
.prepare("SELECT DISTINCT bundle_id AS bundleId, file_path AS filePath FROM entries " +
|
|
56
|
+
"WHERE bundle_id IS NOT NULL AND bundle_id != '' AND file_path IS NOT NULL AND file_path != ''")
|
|
57
|
+
.all();
|
|
56
58
|
}
|
|
57
59
|
catch (error) {
|
|
58
60
|
rethrowIfTestIsolationError(error);
|
|
@@ -80,20 +82,55 @@ export function warnOnBundleRenameDrift(config = loadConfig()) {
|
|
|
80
82
|
const configIds = new Set(Object.keys(bundles));
|
|
81
83
|
if (configIds.size === 0)
|
|
82
84
|
return;
|
|
83
|
-
const
|
|
84
|
-
if (
|
|
85
|
+
const indexedPaths = indexedBundlePaths(getDbPath());
|
|
86
|
+
if (indexedPaths === undefined || indexedPaths.length === 0)
|
|
85
87
|
return; // nothing indexed yet — re-check later
|
|
86
88
|
// A real comparison happened over a populated index: settle so the steady
|
|
87
89
|
// state is one boolean check.
|
|
88
90
|
guardSettled = true;
|
|
91
|
+
const indexIds = [...new Set(indexedPaths.map((row) => row.bundleId))];
|
|
89
92
|
const indexIdSet = new Set(indexIds);
|
|
90
93
|
const configuredMissingFromIndex = [...configIds].filter((id) => !indexIdSet.has(id));
|
|
91
94
|
const indexedNotConfigured = indexIds.filter((id) => !configIds.has(id));
|
|
92
95
|
if (configuredMissingFromIndex.length === 0 || indexedNotConfigured.length === 0)
|
|
93
96
|
return;
|
|
97
|
+
// A mismatched id set alone is also the ordinary signature of adding a new
|
|
98
|
+
// bundle while stale rows from a removed bundle still exist. Treat it as a
|
|
99
|
+
// hand rename only when the old rows physically live beneath the newly named
|
|
100
|
+
// filesystem bundle's root. This keeps the guard useful without warning on
|
|
101
|
+
// unrelated new bundles (#971).
|
|
102
|
+
const renamedConfiguredIds = configuredMissingFromIndex.filter((configuredId) => {
|
|
103
|
+
const configuredPath = config.bundles?.[configuredId]?.path;
|
|
104
|
+
if (typeof configuredPath !== "string")
|
|
105
|
+
return false;
|
|
106
|
+
const root = path.resolve(configuredPath);
|
|
107
|
+
return indexedPaths.some((row) => {
|
|
108
|
+
if (!indexedNotConfigured.includes(row.bundleId))
|
|
109
|
+
return false;
|
|
110
|
+
const relative = path.relative(root, path.resolve(row.filePath));
|
|
111
|
+
return relative !== "" && !relative.startsWith("..") && !path.isAbsolute(relative);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
if (renamedConfiguredIds.length === 0)
|
|
115
|
+
return;
|
|
116
|
+
const renamedOldIds = [
|
|
117
|
+
...new Set(indexedPaths
|
|
118
|
+
.filter((row) => {
|
|
119
|
+
if (!indexedNotConfigured.includes(row.bundleId))
|
|
120
|
+
return false;
|
|
121
|
+
return renamedConfiguredIds.some((configuredId) => {
|
|
122
|
+
const configuredPath = config.bundles?.[configuredId]?.path;
|
|
123
|
+
if (typeof configuredPath !== "string")
|
|
124
|
+
return false;
|
|
125
|
+
const relative = path.relative(path.resolve(configuredPath), path.resolve(row.filePath));
|
|
126
|
+
return relative !== "" && !relative.startsWith("..") && !path.isAbsolute(relative);
|
|
127
|
+
});
|
|
128
|
+
})
|
|
129
|
+
.map((row) => row.bundleId)),
|
|
130
|
+
];
|
|
94
131
|
warn("WARNING: bundle identity drift detected. " +
|
|
95
|
-
`Configured bundle(s) with no indexed content: ${
|
|
96
|
-
`indexed content under unconfigured bundle id(s): ${
|
|
132
|
+
`Configured bundle(s) with no indexed content: ${renamedConfiguredIds.map((id) => `"${id}"`).join(", ")}; ` +
|
|
133
|
+
`indexed content under unconfigured bundle id(s): ${renamedOldIds.map((id) => `"${id}"`).join(", ")}. ` +
|
|
97
134
|
"This is the signature of a hand-renamed bundle id (spec §11.5). AKM will NOT silently re-mint fresh state " +
|
|
98
135
|
"under the new id. There is no rekey command in 0.9.0: either restore the previous bundle id in config.json " +
|
|
99
136
|
"to reattach the existing rows, or keep the new id and run `akm index --full` to re-mint under it, accepting " +
|
|
@@ -193,11 +193,6 @@ function indexCanServeStash(stashDir) {
|
|
|
193
193
|
}
|
|
194
194
|
async function runInlineReindex(stashDir, options = {}) {
|
|
195
195
|
const { akmIndex } = await import("./indexer.js");
|
|
196
|
-
// The embedding drain on this implicit path is bounded (see
|
|
197
|
-
// `IndexOptions.implicit`, src/indexer/indexer.ts): a read command's
|
|
198
|
-
// inline bootstrap embeds one provider request's worth of units and
|
|
199
|
-
// leaves the rest of the durable queue to a later drain, so a first
|
|
200
|
-
// `search`/`show` against a fresh index never blocks on the whole corpus.
|
|
201
196
|
await akmIndex({
|
|
202
197
|
stashDir,
|
|
203
198
|
implicit: true,
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
/**
|
|
5
|
+
* Shared index.db contention reclassification (field follow-up to #956).
|
|
6
|
+
*
|
|
7
|
+
* Extracted out of `indexer.ts` so both `akmIndex`'s outer catch AND
|
|
8
|
+
* `generateEmbeddingsForDb`'s own catch (`materialize-embeddings.ts`) can
|
|
9
|
+
* reuse the ONE classifier instead of each building a raw
|
|
10
|
+
* `Semantic search verification failed: <driver message>` string. Living in
|
|
11
|
+
* its own module (rather than one importing the other) avoids the import
|
|
12
|
+
* cycle `indexer.ts` <-> `materialize-embeddings.ts` would otherwise form.
|
|
13
|
+
*/
|
|
14
|
+
import { AkmError, TransientError } from "../core/errors.js";
|
|
15
|
+
import { probeLock } from "../core/file-lock.js";
|
|
16
|
+
import { formatLockHolderPid } from "../core/run-lock.js";
|
|
17
|
+
import { isSqliteContentionError } from "../core/state-db.js";
|
|
18
|
+
import { indexRebuildLockPath } from "./index-rebuild-lock.js";
|
|
19
|
+
/**
|
|
20
|
+
* Read-only description of the rebuild lock's current holder, appended to a
|
|
21
|
+
* reclassified index.db contention message when known (field follow-up to
|
|
22
|
+
* #956). `probeLock` only inspects the sentinel — it never acquires or
|
|
23
|
+
* mutates it — so this is safe to call from inside an error path.
|
|
24
|
+
*/
|
|
25
|
+
function describeIndexRebuildLockHolder() {
|
|
26
|
+
const probe = probeLock(indexRebuildLockPath());
|
|
27
|
+
if (probe.state !== "held")
|
|
28
|
+
return "";
|
|
29
|
+
return ` The rebuild lock is currently held by pid ${formatLockHolderPid({
|
|
30
|
+
pid: probe.holderPid,
|
|
31
|
+
launcherPid: probe.launcherPid ?? null,
|
|
32
|
+
})}.`;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Reclassify a contention-shaped error escaping the walk, index, or
|
|
36
|
+
* embedding phase into a retryable-shortly `TransientError` (field
|
|
37
|
+
* follow-up to #956, dev-team field review 2026-09-10): a concurrent writer
|
|
38
|
+
* (another `akm index`, a source-update embedding pass, the per-command
|
|
39
|
+
* background reindex) can make index.db busy, and the raw SQLite driver
|
|
40
|
+
* error ("database is locked") used to escape as exit 70
|
|
41
|
+
* (internal/unclassified) instead of the "retry shortly" contract exit 75
|
|
42
|
+
* gives a scheduler to branch on — mirroring `STATE_DB_CONTENDED`'s
|
|
43
|
+
* precedent for state.db (`core/state-db.ts`). Reuses the ONE shared
|
|
44
|
+
* classifier, `isSqliteContentionError`, rather than a second one. An error
|
|
45
|
+
* that is already a classified akm error (e.g. a `STATE_DB_CONTENDED`
|
|
46
|
+
* TransientError from an inner state.db write) is never re-wrapped — only a
|
|
47
|
+
* raw, unclassified error matching the shared contention shape is
|
|
48
|
+
* reclassified. Every other error is rethrown unchanged.
|
|
49
|
+
*/
|
|
50
|
+
export function reclassifyIndexDbContention(error) {
|
|
51
|
+
if (error instanceof AkmError || !isSqliteContentionError(error))
|
|
52
|
+
return error;
|
|
53
|
+
const contended = new TransientError(`akm's index database is busy (another akm process is writing it); retry shortly.${describeIndexRebuildLockHolder()}`, "INDEX_DB_CONTENDED");
|
|
54
|
+
contended.cause = error;
|
|
55
|
+
return contended;
|
|
56
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
/**
|
|
5
|
+
* Opt-in, non-blocking rebuild lock for `akm index` (#956).
|
|
6
|
+
*
|
|
7
|
+
* #872 removed the blocking index-rebuild lease: the index is a regenerable
|
|
8
|
+
* cache, so a concurrent rebuild only wastes work rather than corrupts
|
|
9
|
+
* anything, and a live-but-wedged holder passed a PID-liveness check forever
|
|
10
|
+
* — only an age-based clock could ever free it, which is exactly the hazard
|
|
11
|
+
* #872 deleted. This module does not reinstate that lock. It adds a
|
|
12
|
+
* PID-liveness-only sentinel an explicit `akm index` run acquires and
|
|
13
|
+
* releases on exit purely so a *scheduled or opportunistic* run
|
|
14
|
+
* (`--skip-if-locked`) can step aside instead of piling up behind a rebuild
|
|
15
|
+
* already in progress. A human-typed `akm index` with no flag is never
|
|
16
|
+
* gated: it warns and proceeds exactly as it did before this lock existed.
|
|
17
|
+
*
|
|
18
|
+
* Built on the shared PID-liveness mechanics in `core/run-lock.ts` (the same
|
|
19
|
+
* ones `akm improve`'s whole-run lock uses) — see that module's doc for the
|
|
20
|
+
* no-stale-age-window rationale.
|
|
21
|
+
*/
|
|
22
|
+
import { releaseLock } from "../core/file-lock.js";
|
|
23
|
+
import { tryWithMaintenanceStartBarrier, withMaintenanceStartBarrier } from "../core/maintenance-barrier.js";
|
|
24
|
+
import { getIndexRebuildLockPath } from "../core/paths.js";
|
|
25
|
+
import { formatLockHolderPid, tryAcquireRunLock } from "../core/run-lock.js";
|
|
26
|
+
import { warn, warnVerbose } from "../core/warn.js";
|
|
27
|
+
export function indexRebuildLockPath() {
|
|
28
|
+
return getIndexRebuildLockPath();
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Acquire the rebuild lock for the duration of one `akm index` run.
|
|
32
|
+
*
|
|
33
|
+
* - Free: always returns `"acquired"`.
|
|
34
|
+
* - Held, `skipIfLocked`: warns once (naming the holder) and returns
|
|
35
|
+
* `"skipped"` — the caller must not run `akmIndex()` at all.
|
|
36
|
+
* - Held, no flag: warns once and returns `"contended"` — the caller runs
|
|
37
|
+
* `akmIndex()` unlocked, exactly as every `akm index` did before #956.
|
|
38
|
+
*
|
|
39
|
+
* A dead holder's lease is reclaimed silently (verbose-only log line, never
|
|
40
|
+
* a user-facing warning) — the operator did nothing wrong and nothing here
|
|
41
|
+
* requires their attention.
|
|
42
|
+
*/
|
|
43
|
+
export function tryAcquireIndexRebuildLock(skipIfLocked) {
|
|
44
|
+
const lockPath = indexRebuildLockPath();
|
|
45
|
+
const acquire = () => tryAcquireRunLock(lockPath, {
|
|
46
|
+
label: "index rebuild",
|
|
47
|
+
onReclaimed: (info) => {
|
|
48
|
+
warnVerbose(`[index] reclaimed a rebuild lock left by pid ${info.holderPid ?? "unknown"} ` +
|
|
49
|
+
`(${info.reason}); that process is no longer running.`);
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
if (skipIfLocked) {
|
|
53
|
+
const result = tryWithMaintenanceStartBarrier(acquire);
|
|
54
|
+
if (!result) {
|
|
55
|
+
warn("[index] maintenance barrier held; skipping (--skip-if-locked)");
|
|
56
|
+
return { state: "skipped", holder: { pid: null, startedAt: null, launcherPid: null } };
|
|
57
|
+
}
|
|
58
|
+
if (result.state === "acquired")
|
|
59
|
+
return result;
|
|
60
|
+
warn(`[index] another index run holds the lock (PID ${formatLockHolderPid(result.holder)}, started ${result.holder.startedAt}); ` +
|
|
61
|
+
"skipping (--skip-if-locked)");
|
|
62
|
+
return { state: "skipped", holder: result.holder };
|
|
63
|
+
}
|
|
64
|
+
const result = withMaintenanceStartBarrier(acquire);
|
|
65
|
+
if (result.state === "acquired")
|
|
66
|
+
return result;
|
|
67
|
+
warn(`[index] another index run is active (pid ${formatLockHolderPid(result.holder)}, started ${result.holder.startedAt}); ` +
|
|
68
|
+
"this run will contend with it — pass --skip-if-locked for scheduled runs");
|
|
69
|
+
return { state: "contended", holder: result.holder };
|
|
70
|
+
}
|
|
71
|
+
export function releaseIndexRebuildLock(ownership) {
|
|
72
|
+
releaseLock(ownership);
|
|
73
|
+
}
|