akm-cli 0.9.5 → 0.9.6
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 +105 -0
- package/dist/commands/env/env-cli.js +1 -2
- package/dist/commands/env/secret-cli.js +1 -5
- package/dist/commands/feedback-cli.js +0 -4
- package/dist/commands/health/checks.js +0 -32
- package/dist/commands/health/surfaces.js +2 -2
- package/dist/commands/health.js +6 -15
- package/dist/commands/improve/autonomy-gate.js +1 -1
- package/dist/commands/improve/consolidate.js +25 -26
- package/dist/commands/improve/distill.js +2 -2
- package/dist/commands/improve/extract.js +8 -1
- package/dist/commands/improve/improve.js +3 -5
- package/dist/commands/improve/locks.js +13 -5
- package/dist/commands/improve/loop-stages.js +8 -9
- package/dist/commands/improve/memory/memory-contradiction-detect.js +1 -19
- package/dist/commands/improve/outcome-loop.js +0 -6
- package/dist/commands/improve/reflect.js +20 -9
- package/dist/commands/lint/index.js +4 -6
- package/dist/commands/proposal/validators/proposal-quality-validators.js +0 -7
- package/dist/commands/sources/bundle-config-ops.js +9 -4
- package/dist/commands/sources/info.js +14 -10
- package/dist/commands/tasks/tasks.js +12 -16
- package/dist/commands/workflow-cli.js +2 -2
- package/dist/core/common.js +61 -24
- package/dist/core/config/config-io.js +2 -2
- package/dist/core/config/config-sources.js +32 -2
- package/dist/core/config/config.js +1 -1
- package/dist/core/config/schema/engines.js +9 -7
- package/dist/core/config/schema/primitives.js +0 -5
- package/dist/core/config/schema/search.js +2 -1
- package/dist/core/file-lock.js +2 -1
- package/dist/core/maintenance-barrier.js +2 -14
- package/dist/core/paths.js +0 -3
- package/dist/core/redaction.js +2 -2
- package/dist/core/spawn-env.js +8 -12
- package/dist/core/state/migrations.js +1 -12
- package/dist/core/state-db.js +9 -27
- package/dist/core/write-source.js +12 -19
- package/dist/execution/directory-identity.js +36 -10
- package/dist/indexer/graph/graph-boost.js +0 -4
- package/dist/indexer/index-writer-lock.js +43 -24
- package/dist/indexer/index-written-assets.js +5 -6
- package/dist/indexer/indexer.js +2 -39
- package/dist/indexer/materialize-embeddings.js +85 -41
- package/dist/indexer/search/db-search.js +15 -48
- package/dist/indexer/search/ranking-contributors.js +0 -25
- package/dist/indexer/search/ranking.js +3 -13
- package/dist/integrations/agent/builder-shared.js +0 -25
- package/dist/integrations/agent/model-map.js +2 -60
- package/dist/integrations/harnesses/aider/agent-builder.js +1 -3
- package/dist/integrations/harnesses/amazonq/agent-builder.js +2 -7
- package/dist/integrations/harnesses/claude/agent-builder.js +1 -4
- package/dist/integrations/harnesses/codex/agent-builder.js +1 -4
- package/dist/integrations/harnesses/copilot/agent-builder.js +2 -6
- package/dist/integrations/harnesses/gemini/agent-builder.js +2 -7
- package/dist/integrations/harnesses/opencode/agent-builder.js +1 -4
- package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +2 -16
- package/dist/integrations/harnesses/openhands/agent-builder.js +1 -3
- package/dist/integrations/harnesses/pi/agent-builder.js +2 -4
- package/dist/integrations/session-logs/index.js +0 -9
- package/dist/llm/client.js +75 -42
- package/dist/llm/embedder.js +7 -3
- package/dist/llm/embedders/remote.js +141 -42
- package/dist/registry/network.js +5 -37
- package/dist/runtime.js +2 -10
- package/dist/scripts/akm-migrate-node.js +60 -126
- package/dist/scripts/akm-migrate.js +60 -126
- package/dist/setup/engine-config.js +2 -5
- package/dist/setup/registry-stash-loader.js +0 -8
- package/dist/setup/setup.js +9 -46
- package/dist/setup/steps/connection-shared.js +10 -13
- package/dist/sources/providers/git-install.js +1 -1
- package/dist/storage/engines/sqlite-migrations.js +20 -1
- package/dist/storage/repositories/index-entries-repository.js +0 -15
- package/dist/tasks/backends/launchd.js +15 -20
- package/dist/tasks/backends/schtasks.js +18 -8
- package/dist/tasks/run/run-native-task.js +8 -6
- package/dist/tasks/source/bounded-document.js +2 -4
- package/dist/tasks/source/task-source-v3-frozen.js +5 -7
- package/dist/tasks/source/task-source-v4.js +5 -10
- package/dist/tasks/source/task-to-v3.js +5 -10
- package/dist/tasks/source/task-to-v4.js +1 -4
- package/dist/tasks/source-v3.js +6 -6
- package/dist/workflows/exec/native-executor.js +21 -31
- package/dist/workflows/exec/run-workflow.js +5 -6
- package/dist/workflows/exec/scheduler.js +3 -19
- package/dist/workflows/exec/step-work.js +1 -4
- package/dist/workflows/exec/unit-dispatch.js +2 -2
- package/dist/workflows/exec/worktree.js +1 -13
- package/dist/workflows/freeze/targets/child-workflow.js +2 -10
- package/dist/workflows/ir/plan-hash.js +4 -6
- package/dist/workflows/ir/schema-v4.js +0 -12
- package/dist/workflows/ir/schema.js +20 -31
- package/dist/workflows/parser.js +11 -52
- package/dist/workflows/renderer.js +2 -3
- package/dist/workflows/resource-limits.js +11 -41
- package/dist/workflows/runtime/runs.js +3 -4
- package/dist/workflows/source-ir/schema.js +14 -30
- package/dist/workflows/validator.js +1 -7
- package/docs/reference/configuration.md +1 -1
- package/package.json +1 -1
- package/schemas/akm-config.json +4 -23
- package/schemas/akm-task.json +1 -2
- package/schemas/akm-workflow.json +1 -13
- package/dist/indexer/search/semantic-status.js +0 -142
|
@@ -5,6 +5,7 @@ import { AsyncLocalStorage } from "node:async_hooks";
|
|
|
5
5
|
import { randomUUID } from "node:crypto";
|
|
6
6
|
import fs from "node:fs";
|
|
7
7
|
import path from "node:path";
|
|
8
|
+
import { sleepSync } from "../runtime.js";
|
|
8
9
|
import { ConfigError } from "./errors.js";
|
|
9
10
|
import { createLockPayload, probeLock, reclaimStaleLock, releaseLock, tryAcquireLockSync } from "./file-lock.js";
|
|
10
11
|
import { getMaintenanceBarrierPath } from "./paths.js";
|
|
@@ -93,7 +94,7 @@ function withMaintenanceStartBarrierSyncWait(run) {
|
|
|
93
94
|
const deadline = Date.now() + 5_000;
|
|
94
95
|
let release = tryAcquireMaintenanceBarrier();
|
|
95
96
|
while (!release && Date.now() < deadline) {
|
|
96
|
-
|
|
97
|
+
sleepSync(5);
|
|
97
98
|
release = tryAcquireMaintenanceBarrier();
|
|
98
99
|
}
|
|
99
100
|
if (!release)
|
|
@@ -107,19 +108,6 @@ function withMaintenanceStartBarrierSyncWait(run) {
|
|
|
107
108
|
release();
|
|
108
109
|
}
|
|
109
110
|
}
|
|
110
|
-
/** Register a long-lived state operation atomically with other start acquisitions. */
|
|
111
|
-
export async function acquireMaintenanceActivity(name) {
|
|
112
|
-
return withMaintenanceStartBarrierAsync(async () => {
|
|
113
|
-
const directory = path.join(path.dirname(getMaintenanceBarrierPath()), "maintenance-activities");
|
|
114
|
-
fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
115
|
-
const lockPath = path.join(directory, `${name}-${process.pid}-${randomUUID()}.lock`);
|
|
116
|
-
const ownership = tryAcquireLockSync(lockPath, createLockPayload({ purpose: name }));
|
|
117
|
-
if (!ownership) {
|
|
118
|
-
throw new ConfigError(`Could not register AKM maintenance activity at ${lockPath}.`, "INVALID_CONFIG_FILE");
|
|
119
|
-
}
|
|
120
|
-
return () => releaseLock(ownership);
|
|
121
|
-
});
|
|
122
|
-
}
|
|
123
111
|
/** Synchronous activity registration for synchronous database handle lifetimes. */
|
|
124
112
|
export function acquireMaintenanceActivitySync(name) {
|
|
125
113
|
return withMaintenanceStartBarrierSyncWait(() => {
|
package/dist/core/paths.js
CHANGED
|
@@ -251,9 +251,6 @@ export function getLockfilePath() {
|
|
|
251
251
|
export function getLockfileLockPath() {
|
|
252
252
|
return path.join(getDataDir(), "akm.lock.lck");
|
|
253
253
|
}
|
|
254
|
-
export function getSemanticStatusPath() {
|
|
255
|
-
return path.join(getCacheDir(), "semantic-status.json");
|
|
256
|
-
}
|
|
257
254
|
export function getRegistryCacheDir() {
|
|
258
255
|
return path.join(getCacheDir(), "registry");
|
|
259
256
|
}
|
package/dist/core/redaction.js
CHANGED
|
@@ -373,11 +373,11 @@ const ERROR_BODY_MAX_LEN = 200;
|
|
|
373
373
|
* Make an HTTP error body safe to put in an error message: pattern-redact
|
|
374
374
|
* credential shapes, then clip. Provider bodies can echo the credential that
|
|
375
375
|
* was sent and can be megabytes of HTML, and these messages travel — into
|
|
376
|
-
*
|
|
376
|
+
* `--json` output and agent transcripts.
|
|
377
377
|
*
|
|
378
378
|
* Lives here rather than beside one transport because every HTTP client in the
|
|
379
379
|
* codebase needs it; the embeddings transport originally lacked it and leaked
|
|
380
|
-
* raw 10 MB bodies into
|
|
380
|
+
* raw 10 MB bodies into error messages.
|
|
381
381
|
*/
|
|
382
382
|
export function redactErrorBody(input) {
|
|
383
383
|
if (!input)
|
package/dist/core/spawn-env.js
CHANGED
|
@@ -4,19 +4,15 @@
|
|
|
4
4
|
/**
|
|
5
5
|
* The ONE allowlist-based child-environment primitive.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
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`)
|
|
10
|
-
* `exec` unit runner (`workflows/exec/exec-unit.ts`)
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
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
|
}
|
package/dist/core/state-db.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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: !!
|
|
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
|
-
|
|
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
|
-
|
|
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 &&
|
|
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
|
-
|
|
31
|
-
|
|
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
|
-
|
|
34
|
-
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
11
|
-
const
|
|
12
|
-
const
|
|
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("
|
|
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
|
|
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
|
-
|
|
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
|
|
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 ??
|
|
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 =
|
|
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
|
|
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(
|
|
111
|
+
await delay(ASSET_MUTATION_WAIT_MS);
|
|
90
112
|
}
|
|
91
113
|
}
|
|
92
|
-
|
|
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
|
|
122
|
+
const lease = await acquireAssetMutationLease({ purpose });
|
|
100
123
|
if (!lease)
|
|
101
|
-
throw new Error(`
|
|
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 =
|
|
148
|
+
lease = tryAcquireAssetMutationLease(lockPath, purpose);
|
|
130
149
|
if (!lease) {
|
|
131
|
-
if (Date.now() - startedAt >=
|
|
132
|
-
throw new Error(`timed out waiting for
|
|
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
|
-
|
|
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
|
|
149
|
-
return probeLock(getIndexWriterLockPath()
|
|
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,
|
|
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
|
|
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
|
-
|
|
185
|
-
|
|
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,
|