akm-cli 0.9.0-rc.1 → 0.9.0-rc.2
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 +223 -9
- package/SECURITY.md +23 -24
- package/dist/assets/help/help-improve.md +10 -10
- package/dist/assets/hints/cli-hints-full.md +44 -10
- package/dist/assets/hints/cli-hints-short.md +6 -2
- package/dist/assets/{profiles → improve-strategies}/default.json +1 -0
- package/dist/assets/{profiles → improve-strategies}/graph-refresh.json +1 -1
- package/dist/assets/{profiles → improve-strategies}/proactive-maintenance.json +2 -3
- package/dist/assets/{profiles → improve-strategies}/reflect-distill.json +3 -4
- package/dist/assets/stash-skeleton/README.md +28 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/agent.md +6 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/command.md +6 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/fact.md +12 -1
- package/dist/assets/stash-skeleton/facts/conventions/assets/knowledge.md +11 -1
- package/dist/assets/stash-skeleton/facts/conventions/assets/lesson.md +6 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/memory.md +9 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/script.md +7 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/skill.md +7 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/workflow.md +6 -0
- package/dist/assets/stash-skeleton/facts/conventions/backlinks.md +98 -0
- package/dist/assets/stash-skeleton/facts/conventions/domains.md +63 -0
- package/dist/assets/stash-skeleton/facts/conventions/organization.md +127 -0
- package/dist/assets/tasks/core/backup.yml +1 -0
- package/dist/assets/tasks/core/extract.yml +1 -0
- package/dist/assets/tasks/core/improve.yml +1 -0
- package/dist/assets/tasks/core/index-refresh.yml +1 -0
- package/dist/assets/tasks/core/sync.yml +1 -0
- package/dist/assets/tasks/core/version-check.yml +1 -0
- package/dist/assets/tasks/graph-refresh-weekly.yml +4 -4
- package/dist/assets/templates/html/health.html +5 -1
- package/dist/cli/config-migrate.js +31 -138
- package/dist/cli/config-validate.js +10 -8
- package/dist/cli.js +48 -14
- package/dist/commands/agent/agent-dispatch.js +17 -16
- package/dist/commands/agent/agent-support.js +0 -24
- package/dist/commands/agent/contribute-cli.js +5 -15
- package/dist/commands/backup-cli.js +54 -0
- package/dist/commands/config-cli.js +45 -159
- package/dist/commands/env/secret.js +8 -5
- package/dist/commands/health/checks.js +130 -83
- package/dist/commands/health/html-report.js +4 -0
- package/dist/commands/health/improve-metrics.js +30 -32
- package/dist/commands/health/llm-usage.js +19 -19
- package/dist/commands/health/md-report.js +4 -0
- package/dist/commands/health/metrics.js +2 -1
- package/dist/commands/health/surfaces.js +5 -4
- package/dist/commands/health.js +1 -1
- package/dist/commands/improve/consolidate/chunking.js +2 -2
- package/dist/commands/improve/consolidate.js +28 -25
- package/dist/commands/improve/distill/promote-memory.js +5 -12
- package/dist/commands/improve/distill/quality-gate.js +5 -7
- package/dist/commands/improve/distill.js +16 -5
- package/dist/commands/improve/eligibility.js +22 -12
- package/dist/commands/improve/extract-cli.js +47 -19
- package/dist/commands/improve/extract.js +110 -62
- package/dist/commands/improve/improve-cli.js +38 -16
- package/dist/commands/improve/improve-result-file.js +30 -24
- package/dist/commands/improve/improve-strategies.js +137 -0
- package/dist/commands/improve/improve.js +60 -30
- package/dist/commands/improve/locks.js +66 -45
- package/dist/commands/improve/loop-stages.js +75 -33
- package/dist/commands/improve/memory/memory-belief.js +79 -7
- package/dist/commands/improve/memory/memory-contradiction-detect.js +12 -4
- package/dist/commands/improve/preparation.js +71 -73
- package/dist/commands/improve/procedural.js +3 -2
- package/dist/commands/improve/recombine.js +2 -1
- package/dist/commands/improve/reflect.js +119 -214
- package/dist/commands/improve/shared.js +11 -5
- package/dist/commands/lint/base-linter.js +152 -42
- package/dist/commands/mv-cli.js +809 -0
- package/dist/commands/proposal/proposal-cli.js +18 -8
- package/dist/commands/proposal/propose.js +64 -69
- package/dist/commands/read/knowledge.js +436 -4
- package/dist/commands/read/remember-cli.js +39 -2
- package/dist/commands/read/search-cli.js +6 -1
- package/dist/commands/registry-cli.js +29 -14
- package/dist/commands/remember.js +2 -0
- package/dist/commands/sources/init.js +13 -14
- package/dist/commands/sources/schema-repair.js +2 -4
- package/dist/commands/sources/source-add.js +62 -73
- package/dist/commands/sources/source-manage.js +50 -46
- package/dist/commands/sources/stash-cli.js +41 -4
- package/dist/commands/tasks/default-tasks.js +12 -12
- package/dist/commands/tasks/tasks-cli.js +7 -3
- package/dist/commands/tasks/tasks.js +113 -18
- package/dist/commands/wiki-cli.js +9 -10
- package/dist/core/asset/frontmatter.js +12 -2
- package/dist/core/common.js +5 -3
- package/dist/core/config/config-io.js +28 -17
- package/dist/core/config/config-schema.js +358 -66
- package/dist/core/config/config-types.js +3 -3
- package/dist/core/config/config-version.js +29 -0
- package/dist/core/config/config-walker.js +98 -27
- package/dist/core/config/config.js +132 -266
- package/dist/core/config/deep-merge.js +41 -0
- package/dist/core/config/engine-semantics.js +32 -0
- package/dist/core/errors.js +2 -2
- package/dist/core/extra-params.js +61 -0
- package/dist/core/file-lock.js +201 -56
- package/dist/core/improve-result.js +178 -0
- package/dist/core/maintenance-barrier.js +119 -0
- package/dist/core/migration-backup.js +416 -0
- package/dist/core/paths.js +3 -0
- package/dist/core/redaction.js +358 -0
- package/dist/core/state/migrations.js +17 -2
- package/dist/core/state-db.js +44 -1
- package/dist/indexer/db/db.js +116 -1
- package/dist/indexer/graph/graph-extraction.js +28 -16
- package/dist/indexer/index-writer-lock.js +31 -24
- package/dist/indexer/index-written-assets.js +15 -6
- package/dist/indexer/indexer.js +47 -2
- package/dist/indexer/passes/memory-inference.js +10 -6
- package/dist/indexer/passes/metadata.js +250 -0
- package/dist/indexer/search/db-search.js +111 -44
- package/dist/indexer/search/fts-query.js +41 -0
- package/dist/indexer/search/ranking-contributors.js +48 -0
- package/dist/indexer/search/ranking.js +36 -23
- package/dist/indexer/search/search-fields.js +11 -1
- package/dist/integrations/agent/builder-shared.js +7 -0
- package/dist/integrations/agent/builders.js +5 -52
- package/dist/integrations/agent/config.js +3 -143
- package/dist/integrations/agent/detect.js +17 -2
- package/dist/integrations/agent/engine-resolution.js +202 -0
- package/dist/integrations/agent/index.js +1 -2
- package/dist/integrations/agent/model-aliases.js +7 -2
- package/dist/integrations/agent/profiles.js +6 -99
- package/dist/integrations/agent/runner-dispatch.js +76 -13
- package/dist/integrations/agent/runner.js +76 -207
- package/dist/integrations/agent/spawn.js +4 -6
- package/dist/integrations/harnesses/aider/agent-builder.js +2 -3
- package/dist/integrations/harnesses/aider/index.js +0 -1
- package/dist/integrations/harnesses/amazonq/agent-builder.js +2 -3
- package/dist/integrations/harnesses/amazonq/index.js +0 -1
- package/dist/integrations/harnesses/claude/agent-builder.js +2 -3
- package/dist/integrations/harnesses/claude/index.js +0 -2
- package/dist/integrations/harnesses/codex/agent-builder.js +2 -3
- package/dist/integrations/harnesses/codex/index.js +0 -1
- package/dist/integrations/harnesses/copilot/agent-builder.js +2 -3
- package/dist/integrations/harnesses/copilot/index.js +0 -1
- package/dist/integrations/harnesses/gemini/agent-builder.js +2 -3
- package/dist/integrations/harnesses/gemini/index.js +0 -1
- package/dist/integrations/harnesses/index.js +1 -24
- package/dist/integrations/harnesses/opencode/agent-builder.js +2 -3
- package/dist/integrations/harnesses/opencode/index.js +0 -6
- package/dist/integrations/harnesses/opencode-sdk/harness.js +1 -6
- package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +274 -191
- package/dist/integrations/harnesses/openhands/agent-builder.js +2 -3
- package/dist/integrations/harnesses/openhands/index.js +0 -1
- package/dist/integrations/harnesses/pi/agent-builder.js +2 -3
- package/dist/integrations/harnesses/pi/index.js +0 -1
- package/dist/integrations/harnesses/types.js +1 -32
- package/dist/integrations/lockfile.js +32 -21
- package/dist/llm/client.js +48 -14
- package/dist/llm/feature-gate.js +15 -47
- package/dist/llm/graph-extract.js +1 -1
- package/dist/llm/index-passes.js +8 -42
- package/dist/llm/memory-infer-impl.js +1 -1
- package/dist/llm/usage-persist.js +4 -0
- package/dist/llm/usage-telemetry.js +35 -5
- package/dist/output/shapes/helpers.js +2 -1
- package/dist/output/shapes/passthrough.js +2 -0
- package/dist/output/text/helpers.js +3 -1
- package/dist/schemas/akm-config.json +11013 -8600
- package/dist/schemas/akm-task.json +87 -0
- package/dist/schemas/akm-workflow.json +57 -13
- package/dist/scripts/migrate-storage.js +8591 -509
- package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +8623 -484
- package/dist/setup/detected-engines.js +142 -0
- package/dist/setup/engine-config.js +89 -0
- package/dist/setup/setup.js +236 -132
- package/dist/setup/steps/connection.js +61 -32
- package/dist/setup/steps/platforms.js +4 -4
- package/dist/setup/steps.js +3 -2
- package/dist/storage/database.js +13 -1
- package/dist/storage/engines/sqlite-migrations.js +1 -0
- package/dist/storage/repositories/improve-runs-repository.js +5 -5
- package/dist/storage/repositories/task-history-repository.js +78 -0
- package/dist/storage/repositories/workflow-runs-repository.js +9 -8
- package/dist/tasks/parser.js +138 -52
- package/dist/tasks/runner.js +71 -75
- package/dist/tasks/schema.js +1 -1
- package/dist/tasks/validator.js +11 -6
- package/dist/wiki/wiki.js +9 -8
- package/dist/workflows/authoring/workflow-program-template.yaml +3 -3
- package/dist/workflows/concurrency-policy.js +15 -0
- package/dist/workflows/db.js +65 -13
- package/dist/workflows/exec/brief.js +18 -24
- package/dist/workflows/exec/frozen-judge.js +47 -0
- package/dist/workflows/exec/native-executor.js +124 -65
- package/dist/workflows/exec/report.js +93 -33
- package/dist/workflows/exec/run-workflow.js +38 -25
- package/dist/workflows/exec/scheduler.js +12 -41
- package/dist/workflows/exec/step-work.js +91 -35
- package/dist/workflows/ir/compile.js +13 -26
- package/dist/workflows/ir/freeze.js +243 -0
- package/dist/workflows/ir/plan-hash.js +40 -5
- package/dist/workflows/ir/schema.js +542 -1
- package/dist/workflows/parser.js +7 -0
- package/dist/workflows/program/parser.js +132 -23
- package/dist/workflows/program/project.js +3 -4
- package/dist/workflows/program/schema.js +2 -2
- package/dist/workflows/resource-limits.js +20 -0
- package/dist/workflows/runtime/plan-classifier.js +187 -0
- package/dist/workflows/runtime/runs.js +54 -47
- package/dist/workflows/runtime/workflow-asset-loader.js +0 -22
- package/dist/workflows/validator.js +25 -0
- package/docs/data-and-telemetry.md +2 -2
- package/docs/migration/release-notes/0.6.0.md +1 -1
- package/docs/migration/release-notes/0.7.0.md +5 -4
- package/docs/migration/v0.8-to-v0.9.md +401 -0
- package/package.json +4 -2
- package/schemas/akm-config.json +16638 -0
- package/schemas/akm-task.json +87 -0
- package/schemas/akm-workflow.json +372 -0
- package/dist/commands/improve/improve-profiles.js +0 -168
- package/dist/core/config/config-migration.js +0 -602
- package/dist/core/deep-merge.js +0 -38
- package/dist/llm/call-ai.js +0 -62
- package/dist/setup/legacy-config.js +0 -106
- package/docs/README.md +0 -104
- /package/dist/assets/{profiles → improve-strategies}/catchup.json +0 -0
- /package/dist/assets/{profiles → improve-strategies}/consolidate.json +0 -0
- /package/dist/assets/{profiles → improve-strategies}/frequent.json +0 -0
- /package/dist/assets/{profiles → improve-strategies}/memory-focus.json +0 -0
- /package/dist/assets/{profiles → improve-strategies}/quick.json +0 -0
- /package/dist/assets/{profiles → improve-strategies}/recombine-only.json +0 -0
- /package/dist/assets/{profiles → improve-strategies}/synthesize.json +0 -0
- /package/dist/assets/{profiles → improve-strategies}/thorough.json +0 -0
|
@@ -0,0 +1,61 @@
|
|
|
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
|
+
export const EXTRA_PARAMS_PROTECTED_TOP_LEVEL_KEYS = [
|
|
5
|
+
"model",
|
|
6
|
+
"messages",
|
|
7
|
+
"temperature",
|
|
8
|
+
"maxtokens",
|
|
9
|
+
"responseformat",
|
|
10
|
+
"stream",
|
|
11
|
+
"streamoptions",
|
|
12
|
+
"enablethinking",
|
|
13
|
+
];
|
|
14
|
+
export const EXTRA_PARAMS_CREDENTIAL_KEYS = [
|
|
15
|
+
"authorization",
|
|
16
|
+
"headers",
|
|
17
|
+
"apikey",
|
|
18
|
+
"token",
|
|
19
|
+
"password",
|
|
20
|
+
"secret",
|
|
21
|
+
"cookie",
|
|
22
|
+
"setcookie",
|
|
23
|
+
];
|
|
24
|
+
const PROTECTED_TOP_LEVEL_KEYS = new Set(EXTRA_PARAMS_PROTECTED_TOP_LEVEL_KEYS);
|
|
25
|
+
const CREDENTIAL_KEYS = new Set(EXTRA_PARAMS_CREDENTIAL_KEYS);
|
|
26
|
+
export function normalizeExtraParamKey(key) {
|
|
27
|
+
return key.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
28
|
+
}
|
|
29
|
+
/** Validate provider extras without allowing them to override AKM fields or carry credentials. */
|
|
30
|
+
export function validateExtraParams(value) {
|
|
31
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
32
|
+
return [{ path: [], message: "must be an object" }];
|
|
33
|
+
}
|
|
34
|
+
const issues = [];
|
|
35
|
+
const visit = (entry, path) => {
|
|
36
|
+
if (Array.isArray(entry)) {
|
|
37
|
+
entry.forEach((child, index) => {
|
|
38
|
+
visit(child, [...path, index]);
|
|
39
|
+
});
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (!entry || typeof entry !== "object")
|
|
43
|
+
return;
|
|
44
|
+
for (const [key, child] of Object.entries(entry)) {
|
|
45
|
+
const normalized = normalizeExtraParamKey(key);
|
|
46
|
+
if (path.length === 0 && PROTECTED_TOP_LEVEL_KEYS.has(normalized)) {
|
|
47
|
+
issues.push({ path: [key], message: `${key} is protected by AKM` });
|
|
48
|
+
}
|
|
49
|
+
if (CREDENTIAL_KEYS.has(normalized)) {
|
|
50
|
+
issues.push({ path: [...path, key], message: `${key} cannot carry credentials` });
|
|
51
|
+
}
|
|
52
|
+
visit(child, [...path, key]);
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
visit(value, []);
|
|
56
|
+
return issues;
|
|
57
|
+
}
|
|
58
|
+
export function formatExtraParamsIssue(label, issue) {
|
|
59
|
+
const suffix = issue.path.map((part) => (typeof part === "number" ? `[${part}]` : `.${part}`)).join("");
|
|
60
|
+
return `${label}${suffix} ${issue.message}`;
|
|
61
|
+
}
|
package/dist/core/file-lock.js
CHANGED
|
@@ -1,11 +1,114 @@
|
|
|
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
|
+
import { randomUUID } from "node:crypto";
|
|
4
5
|
import fs from "node:fs";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { openDatabase } from "../storage/database.js";
|
|
5
8
|
import { isProcessAlive } from "./common.js";
|
|
9
|
+
function readLockSnapshot(lockPath) {
|
|
10
|
+
let fd;
|
|
11
|
+
try {
|
|
12
|
+
fd = fs.openSync(lockPath, "r");
|
|
13
|
+
}
|
|
14
|
+
catch (err) {
|
|
15
|
+
if (err.code === "ENOENT")
|
|
16
|
+
return undefined;
|
|
17
|
+
throw err;
|
|
18
|
+
}
|
|
19
|
+
try {
|
|
20
|
+
const rawContent = fs.readFileSync(fd, "utf8");
|
|
21
|
+
const stat = fs.fstatSync(fd);
|
|
22
|
+
return {
|
|
23
|
+
rawContent,
|
|
24
|
+
identity: { dev: stat.dev, ino: stat.ino, size: stat.size, mtimeMs: stat.mtimeMs },
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
finally {
|
|
28
|
+
fs.closeSync(fd);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function sameIdentity(left, right) {
|
|
32
|
+
return left.dev === right.dev && left.ino === right.ino && left.size === right.size && left.mtimeMs === right.mtimeMs;
|
|
33
|
+
}
|
|
34
|
+
function operationMutexPath(lockPath) {
|
|
35
|
+
// `.sensitive` is an established non-asset suffix across stash walkers. The
|
|
36
|
+
// mutex may sit beside a secret/env lock and must never surface as an asset.
|
|
37
|
+
return path.join(path.dirname(lockPath), `.${path.basename(lockPath)}.operations.sensitive`);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Serialize every mutation of one canonical lock path. SQLite's write lock is
|
|
41
|
+
* released by the OS when a process dies, so this mutex needs no stale-owner
|
|
42
|
+
* deletion protocol (which would reproduce the same check/rename race it is
|
|
43
|
+
* meant to prevent).
|
|
44
|
+
*/
|
|
45
|
+
function withLockOperationMutex(lockPath, run) {
|
|
46
|
+
const db = openDatabase(operationMutexPath(lockPath));
|
|
47
|
+
let began = false;
|
|
48
|
+
try {
|
|
49
|
+
db.exec("PRAGMA busy_timeout = 30000");
|
|
50
|
+
for (let attempt = 0; attempt < 5 && !began; attempt += 1) {
|
|
51
|
+
db.exec("BEGIN IMMEDIATE");
|
|
52
|
+
began = db.inTransaction;
|
|
53
|
+
if (!began)
|
|
54
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 2 ** attempt);
|
|
55
|
+
}
|
|
56
|
+
if (!began)
|
|
57
|
+
throw new Error(`Could not acquire lock operation mutex for ${lockPath}.`);
|
|
58
|
+
const result = run();
|
|
59
|
+
db.exec("COMMIT");
|
|
60
|
+
began = false;
|
|
61
|
+
return result;
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
if (began && db.inTransaction) {
|
|
65
|
+
try {
|
|
66
|
+
db.exec("ROLLBACK");
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
// Preserve the operation failure.
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
finally {
|
|
75
|
+
db.close();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function tryAcquireLockRaw(lockPath, payload) {
|
|
79
|
+
try {
|
|
80
|
+
fs.writeFileSync(lockPath, payload, { flag: "wx" });
|
|
81
|
+
}
|
|
82
|
+
catch (err) {
|
|
83
|
+
if (err.code === "EEXIST")
|
|
84
|
+
return undefined;
|
|
85
|
+
throw err;
|
|
86
|
+
}
|
|
87
|
+
let snapshot;
|
|
88
|
+
try {
|
|
89
|
+
snapshot = readLockSnapshot(lockPath);
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
releaseLockRaw(lockPath);
|
|
93
|
+
throw error;
|
|
94
|
+
}
|
|
95
|
+
if (!snapshot) {
|
|
96
|
+
releaseLockRaw(lockPath);
|
|
97
|
+
throw new Error(`Could not read newly acquired lock at ${lockPath}.`);
|
|
98
|
+
}
|
|
99
|
+
return { lockPath, ...snapshot };
|
|
100
|
+
}
|
|
101
|
+
function releaseLockRaw(lockPath) {
|
|
102
|
+
try {
|
|
103
|
+
fs.unlinkSync(lockPath);
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
// Sentinel already gone — fine.
|
|
107
|
+
}
|
|
108
|
+
}
|
|
6
109
|
/**
|
|
7
110
|
* Atomically create a sentinel at `lockPath` with `payload` as the body.
|
|
8
|
-
* Returns
|
|
111
|
+
* Returns an exact ownership handle if we now own the lock, or undefined if a sentinel already
|
|
9
112
|
* exists (EEXIST). Throws any other error (permissions, missing parent
|
|
10
113
|
* dir, etc.) — callers must ensure the parent directory exists.
|
|
11
114
|
*
|
|
@@ -14,15 +117,11 @@ import { isProcessAlive } from "./common.js";
|
|
|
14
117
|
* (improve.ts records pid + startedAt so audit can correlate runs).
|
|
15
118
|
*/
|
|
16
119
|
export function tryAcquireLockSync(lockPath, payload) {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
if (err.code === "EEXIST")
|
|
23
|
-
return false;
|
|
24
|
-
throw err;
|
|
25
|
-
}
|
|
120
|
+
return withLockOperationMutex(lockPath, () => tryAcquireLockRaw(lockPath, payload));
|
|
121
|
+
}
|
|
122
|
+
/** Build a PID-bearing payload with a unique token for one acquisition attempt. */
|
|
123
|
+
export function createLockPayload(metadata = {}) {
|
|
124
|
+
return JSON.stringify({ ...metadata, pid: process.pid, lockId: randomUUID() });
|
|
26
125
|
}
|
|
27
126
|
/**
|
|
28
127
|
* Inspect an existing sentinel at `lockPath` without modifying it.
|
|
@@ -36,70 +135,116 @@ export function tryAcquireLockSync(lockPath, payload) {
|
|
|
36
135
|
* Does NOT remove the file. Callers decide recovery policy.
|
|
37
136
|
*/
|
|
38
137
|
export function probeLock(lockPath, opts) {
|
|
39
|
-
let
|
|
40
|
-
let ageMs;
|
|
41
|
-
try {
|
|
42
|
-
rawContent = fs.readFileSync(lockPath, "utf8");
|
|
43
|
-
}
|
|
44
|
-
catch (err) {
|
|
45
|
-
if (err.code === "ENOENT")
|
|
46
|
-
return { state: "absent" };
|
|
47
|
-
return { state: "stale", reason: "unreadable" };
|
|
48
|
-
}
|
|
138
|
+
let snapshot;
|
|
49
139
|
try {
|
|
50
|
-
|
|
51
|
-
ageMs = Date.now() - stat.mtimeMs;
|
|
140
|
+
snapshot = readLockSnapshot(lockPath);
|
|
52
141
|
}
|
|
53
142
|
catch {
|
|
54
|
-
|
|
55
|
-
return { state: "stale", reason: "unreadable", rawContent };
|
|
143
|
+
return { state: "stale", reason: "unreadable" };
|
|
56
144
|
}
|
|
145
|
+
if (!snapshot)
|
|
146
|
+
return { state: "absent" };
|
|
147
|
+
const { rawContent, identity } = snapshot;
|
|
148
|
+
const ageMs = Date.now() - identity.mtimeMs;
|
|
57
149
|
const holderPid = extractHolderPid(rawContent);
|
|
58
150
|
if (holderPid === undefined) {
|
|
59
|
-
return { state: "stale", reason: "invalid_pid", ageMs, rawContent };
|
|
151
|
+
return { state: "stale", reason: "invalid_pid", ageMs, rawContent, identity };
|
|
60
152
|
}
|
|
61
153
|
if (!isProcessAlive(holderPid)) {
|
|
62
|
-
return { state: "stale", reason: "pid_dead", holderPid, ageMs, rawContent };
|
|
154
|
+
return { state: "stale", reason: "pid_dead", holderPid, ageMs, rawContent, identity };
|
|
63
155
|
}
|
|
64
156
|
if (opts?.staleAfterMs !== undefined && ageMs > opts.staleAfterMs) {
|
|
65
|
-
return { state: "stale", reason: "age_exceeded", holderPid, ageMs, rawContent };
|
|
157
|
+
return { state: "stale", reason: "age_exceeded", holderPid, ageMs, rawContent, identity };
|
|
66
158
|
}
|
|
67
|
-
return { state: "held", holderPid, ageMs, rawContent };
|
|
159
|
+
return { state: "held", holderPid, ageMs, rawContent, identity };
|
|
68
160
|
}
|
|
69
161
|
/**
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
162
|
+
* Revalidate and quarantine the probed sentinel while holding the same operation
|
|
163
|
+
* mutex used by acquisitions. A newer owner therefore cannot be renamed in the
|
|
164
|
+
* check/quarantine window, and a third contender cannot acquire until cleanup
|
|
165
|
+
* has completed.
|
|
73
166
|
*/
|
|
74
|
-
export function
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
167
|
+
export function reclaimStaleLock(lockPath, probe, options) {
|
|
168
|
+
if (probe.rawContent === undefined || probe.identity === undefined)
|
|
169
|
+
return false;
|
|
170
|
+
const expectedContent = probe.rawContent;
|
|
171
|
+
const expectedIdentity = probe.identity;
|
|
172
|
+
return withLockOperationMutex(lockPath, () => {
|
|
173
|
+
let current;
|
|
174
|
+
try {
|
|
175
|
+
current = readLockSnapshot(lockPath);
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
if (!current || current.rawContent !== expectedContent || !sameIdentity(current.identity, expectedIdentity)) {
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
const quarantinePath = `${lockPath}.stale-${process.pid}-${randomUUID()}`;
|
|
184
|
+
try {
|
|
185
|
+
fs.renameSync(lockPath, quarantinePath);
|
|
186
|
+
}
|
|
187
|
+
catch (err) {
|
|
188
|
+
if (err.code === "ENOENT")
|
|
189
|
+
return false;
|
|
190
|
+
throw err;
|
|
191
|
+
}
|
|
192
|
+
let quarantined;
|
|
193
|
+
try {
|
|
194
|
+
quarantined = readLockSnapshot(quarantinePath);
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
quarantined = undefined;
|
|
198
|
+
}
|
|
199
|
+
if (!quarantined ||
|
|
200
|
+
quarantined.rawContent !== expectedContent ||
|
|
201
|
+
!sameIdentity(quarantined.identity, expectedIdentity)) {
|
|
202
|
+
try {
|
|
203
|
+
// Restore without replacing a non-cooperating lock installed after quarantine.
|
|
204
|
+
fs.linkSync(quarantinePath, lockPath);
|
|
205
|
+
}
|
|
206
|
+
catch (err) {
|
|
207
|
+
if (err.code !== "EEXIST")
|
|
208
|
+
throw err;
|
|
209
|
+
}
|
|
210
|
+
releaseLockRaw(quarantinePath);
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
options?.afterQuarantineVerified?.();
|
|
214
|
+
try {
|
|
215
|
+
fs.unlinkSync(quarantinePath);
|
|
216
|
+
return true;
|
|
217
|
+
}
|
|
218
|
+
catch (err) {
|
|
219
|
+
if (err.code === "ENOENT")
|
|
220
|
+
return false;
|
|
221
|
+
throw err;
|
|
222
|
+
}
|
|
223
|
+
});
|
|
81
224
|
}
|
|
82
225
|
/**
|
|
83
|
-
* Release
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
* ownership first means that if the lock was already released and re-acquired by
|
|
88
|
-
* a different process, this leaves that process's lock intact (no cross-run
|
|
89
|
-
* deletion / PID-reuse footgun). Synchronous so it is valid inside an exit handler.
|
|
226
|
+
* Release only the exact sentinel returned by `tryAcquireLockSync`. Content and
|
|
227
|
+
* file identity are revalidated while holding the acquisition operation mutex,
|
|
228
|
+
* so a stale holder cannot remove a successor. Synchronous and idempotent so it
|
|
229
|
+
* is safe in both `finally` blocks and process exit handlers.
|
|
90
230
|
*/
|
|
91
|
-
export function
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
rawContent = fs.readFileSync(lockPath, "utf8");
|
|
95
|
-
}
|
|
96
|
-
catch {
|
|
97
|
-
// Absent or unreadable — nothing of ours to release.
|
|
231
|
+
export function releaseLock(ownership) {
|
|
232
|
+
const { lockPath } = ownership;
|
|
233
|
+
if (!fs.existsSync(lockPath) && !fs.existsSync(operationMutexPath(lockPath)))
|
|
98
234
|
return;
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
235
|
+
withLockOperationMutex(lockPath, () => {
|
|
236
|
+
let current;
|
|
237
|
+
try {
|
|
238
|
+
current = readLockSnapshot(lockPath);
|
|
239
|
+
}
|
|
240
|
+
catch {
|
|
241
|
+
// Absent or unreadable — nothing of ours to release.
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
if (current && current.rawContent === ownership.rawContent && sameIdentity(current.identity, ownership.identity)) {
|
|
245
|
+
releaseLockRaw(lockPath);
|
|
246
|
+
}
|
|
247
|
+
});
|
|
103
248
|
}
|
|
104
249
|
/**
|
|
105
250
|
* Extract a PID from a sentinel body. Accepts the two shapes used across
|
|
@@ -0,0 +1,178 @@
|
|
|
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
|
+
const COMMON_FIELDS = [
|
|
5
|
+
"schemaVersion",
|
|
6
|
+
"ok",
|
|
7
|
+
"scope",
|
|
8
|
+
"dryRun",
|
|
9
|
+
"skipped",
|
|
10
|
+
"guidance",
|
|
11
|
+
"memorySummary",
|
|
12
|
+
"memoryCleanup",
|
|
13
|
+
"cyclesRun",
|
|
14
|
+
"plannedRefs",
|
|
15
|
+
"actions",
|
|
16
|
+
"distillSkipped",
|
|
17
|
+
"validationFailures",
|
|
18
|
+
"schemaRepairs",
|
|
19
|
+
"consolidation",
|
|
20
|
+
"extract",
|
|
21
|
+
"lintSummary",
|
|
22
|
+
"memoryIndexHealth",
|
|
23
|
+
"coverageGaps",
|
|
24
|
+
"evalCasesWritten",
|
|
25
|
+
"deadUrls",
|
|
26
|
+
"reflectsWithErrorContext",
|
|
27
|
+
"memoryInference",
|
|
28
|
+
"graphExtraction",
|
|
29
|
+
"memoryInferenceDurationMs",
|
|
30
|
+
"graphExtractionDurationMs",
|
|
31
|
+
"orphansPurged",
|
|
32
|
+
"proposalsExpired",
|
|
33
|
+
"reflectCooldownActions",
|
|
34
|
+
"reflectSkippedActions",
|
|
35
|
+
"reflectGuardRejectedActions",
|
|
36
|
+
"gateAutoAcceptedCount",
|
|
37
|
+
"gateAutoAcceptFailedCount",
|
|
38
|
+
"triage",
|
|
39
|
+
"proactiveMaintenance",
|
|
40
|
+
"recombination",
|
|
41
|
+
"proceduralCompilation",
|
|
42
|
+
"cycleMetrics",
|
|
43
|
+
"runId",
|
|
44
|
+
"sync",
|
|
45
|
+
"terminated",
|
|
46
|
+
];
|
|
47
|
+
const V1_FIELDS = new Set([...COMMON_FIELDS, "profile", "profileFilteredRefs"]);
|
|
48
|
+
const V2_FIELDS = new Set([...COMMON_FIELDS, "strategy", "strategyFilteredRefs"]);
|
|
49
|
+
function fail(message) {
|
|
50
|
+
throw new Error(`invalid improve-result envelope: ${message}`);
|
|
51
|
+
}
|
|
52
|
+
function isRecord(value) {
|
|
53
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
54
|
+
}
|
|
55
|
+
function requireExactFields(value, allowed) {
|
|
56
|
+
const unknown = Object.keys(value).filter((key) => !allowed.has(key));
|
|
57
|
+
if (unknown.length > 0)
|
|
58
|
+
fail(`unknown field${unknown.length === 1 ? "" : "s"}: ${unknown.sort().join(", ")}`);
|
|
59
|
+
}
|
|
60
|
+
function validateCommon(value) {
|
|
61
|
+
if (typeof value.ok !== "boolean")
|
|
62
|
+
fail("ok must be a boolean");
|
|
63
|
+
if (typeof value.dryRun !== "boolean")
|
|
64
|
+
fail("dryRun must be a boolean");
|
|
65
|
+
if (!Array.isArray(value.plannedRefs))
|
|
66
|
+
fail("plannedRefs must be an array");
|
|
67
|
+
if (!isRecord(value.scope))
|
|
68
|
+
fail("scope must be an object");
|
|
69
|
+
requireExactFields(value.scope, new Set(["mode", "value"]));
|
|
70
|
+
if (value.scope.mode !== "all" && value.scope.mode !== "type" && value.scope.mode !== "ref") {
|
|
71
|
+
fail('scope.mode must be "all", "type", or "ref"');
|
|
72
|
+
}
|
|
73
|
+
if (value.scope.value !== undefined && typeof value.scope.value !== "string") {
|
|
74
|
+
fail("scope.value must be a string when present");
|
|
75
|
+
}
|
|
76
|
+
if (!isRecord(value.memorySummary))
|
|
77
|
+
fail("memorySummary must be an object");
|
|
78
|
+
requireExactFields(value.memorySummary, new Set(["eligible", "derived"]));
|
|
79
|
+
if (typeof value.memorySummary.eligible !== "number" || typeof value.memorySummary.derived !== "number") {
|
|
80
|
+
fail("memorySummary.eligible and memorySummary.derived must be numbers");
|
|
81
|
+
}
|
|
82
|
+
for (const field of [
|
|
83
|
+
"actions",
|
|
84
|
+
"validationFailures",
|
|
85
|
+
"schemaRepairs",
|
|
86
|
+
"extract",
|
|
87
|
+
"coverageGaps",
|
|
88
|
+
"deadUrls",
|
|
89
|
+
]) {
|
|
90
|
+
if (value[field] !== undefined && !Array.isArray(value[field]))
|
|
91
|
+
fail(`${field} must be an array`);
|
|
92
|
+
}
|
|
93
|
+
for (const field of [
|
|
94
|
+
"cyclesRun",
|
|
95
|
+
"evalCasesWritten",
|
|
96
|
+
"reflectsWithErrorContext",
|
|
97
|
+
"memoryInferenceDurationMs",
|
|
98
|
+
"graphExtractionDurationMs",
|
|
99
|
+
"orphansPurged",
|
|
100
|
+
"proposalsExpired",
|
|
101
|
+
"reflectCooldownActions",
|
|
102
|
+
"reflectSkippedActions",
|
|
103
|
+
"reflectGuardRejectedActions",
|
|
104
|
+
"gateAutoAcceptedCount",
|
|
105
|
+
"gateAutoAcceptFailedCount",
|
|
106
|
+
]) {
|
|
107
|
+
if (value[field] !== undefined && typeof value[field] !== "number")
|
|
108
|
+
fail(`${field} must be a number`);
|
|
109
|
+
}
|
|
110
|
+
for (const field of ["guidance", "runId"]) {
|
|
111
|
+
if (value[field] !== undefined && typeof value[field] !== "string")
|
|
112
|
+
fail(`${field} must be a string`);
|
|
113
|
+
}
|
|
114
|
+
for (const field of [
|
|
115
|
+
"skipped",
|
|
116
|
+
"memoryCleanup",
|
|
117
|
+
"distillSkipped",
|
|
118
|
+
"consolidation",
|
|
119
|
+
"lintSummary",
|
|
120
|
+
"memoryIndexHealth",
|
|
121
|
+
"memoryInference",
|
|
122
|
+
"graphExtraction",
|
|
123
|
+
"triage",
|
|
124
|
+
"proactiveMaintenance",
|
|
125
|
+
"recombination",
|
|
126
|
+
"proceduralCompilation",
|
|
127
|
+
"cycleMetrics",
|
|
128
|
+
"sync",
|
|
129
|
+
"terminated",
|
|
130
|
+
]) {
|
|
131
|
+
if (value[field] !== undefined && !isRecord(value[field]))
|
|
132
|
+
fail(`${field} must be an object`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
/** Decode the persisted public v1/v2 contract without guessing across versions. */
|
|
136
|
+
export function decodeImproveResult(input) {
|
|
137
|
+
let parsed = input;
|
|
138
|
+
if (typeof input === "string") {
|
|
139
|
+
try {
|
|
140
|
+
parsed = JSON.parse(input);
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
fail("not valid JSON");
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (!isRecord(parsed))
|
|
147
|
+
fail("root must be an object");
|
|
148
|
+
if (parsed.schemaVersion === 1) {
|
|
149
|
+
requireExactFields(parsed, V1_FIELDS);
|
|
150
|
+
validateCommon(parsed);
|
|
151
|
+
if (parsed.profile !== undefined && typeof parsed.profile !== "string")
|
|
152
|
+
fail("profile must be a string");
|
|
153
|
+
if (parsed.profileFilteredRefs !== undefined && !Array.isArray(parsed.profileFilteredRefs)) {
|
|
154
|
+
fail("profileFilteredRefs must be an array");
|
|
155
|
+
}
|
|
156
|
+
return {
|
|
157
|
+
envelope: parsed,
|
|
158
|
+
strategy: null,
|
|
159
|
+
legacyProfile: typeof parsed.profile === "string" ? parsed.profile : null,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
if (parsed.schemaVersion === 2) {
|
|
163
|
+
requireExactFields(parsed, V2_FIELDS);
|
|
164
|
+
validateCommon(parsed);
|
|
165
|
+
if (typeof parsed.strategy !== "string" || parsed.strategy.length === 0) {
|
|
166
|
+
fail("strategy must be a non-empty string");
|
|
167
|
+
}
|
|
168
|
+
if (parsed.strategyFilteredRefs !== undefined && !Array.isArray(parsed.strategyFilteredRefs)) {
|
|
169
|
+
fail("strategyFilteredRefs must be an array");
|
|
170
|
+
}
|
|
171
|
+
return {
|
|
172
|
+
envelope: parsed,
|
|
173
|
+
strategy: parsed.strategy,
|
|
174
|
+
legacyProfile: null,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
fail(`unsupported schemaVersion: ${String(parsed.schemaVersion)}`);
|
|
178
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
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
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
5
|
+
import { randomUUID } from "node:crypto";
|
|
6
|
+
import fs from "node:fs";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { ConfigError } from "./errors.js";
|
|
9
|
+
import { createLockPayload, probeLock, reclaimStaleLock, releaseLock, tryAcquireLockSync } from "./file-lock.js";
|
|
10
|
+
import { getMaintenanceBarrierPath } from "./paths.js";
|
|
11
|
+
const heldBarrierContext = new AsyncLocalStorage();
|
|
12
|
+
/**
|
|
13
|
+
* Serialize restore with the short critical section that creates every
|
|
14
|
+
* long-lived AKM lock or workflow lease. The long operation keeps its own
|
|
15
|
+
* lock/lease; this barrier is released immediately after acquisition.
|
|
16
|
+
*/
|
|
17
|
+
export function tryAcquireMaintenanceBarrier() {
|
|
18
|
+
const lockPath = getMaintenanceBarrierPath();
|
|
19
|
+
fs.mkdirSync(path.dirname(lockPath), { recursive: true });
|
|
20
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
21
|
+
const ownership = tryAcquireLockSync(lockPath, createLockPayload({ purpose: "maintenance-start" }));
|
|
22
|
+
if (ownership) {
|
|
23
|
+
return () => releaseLock(ownership);
|
|
24
|
+
}
|
|
25
|
+
const probe = probeLock(lockPath);
|
|
26
|
+
if (probe.state !== "stale" || !reclaimStaleLock(lockPath, probe))
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
export function acquireMaintenanceBarrier() {
|
|
32
|
+
const release = tryAcquireMaintenanceBarrier();
|
|
33
|
+
if (release)
|
|
34
|
+
return release;
|
|
35
|
+
throw new ConfigError(`AKM maintenance is in progress (barrier ${getMaintenanceBarrierPath()}); retry after it completes.`, "INVALID_CONFIG_FILE");
|
|
36
|
+
}
|
|
37
|
+
export function withMaintenanceStartBarrier(run) {
|
|
38
|
+
if (heldBarrierContext.getStore()?.active)
|
|
39
|
+
return run();
|
|
40
|
+
const release = acquireMaintenanceBarrier();
|
|
41
|
+
const ownership = { active: true };
|
|
42
|
+
try {
|
|
43
|
+
return heldBarrierContext.run(ownership, run);
|
|
44
|
+
}
|
|
45
|
+
finally {
|
|
46
|
+
ownership.active = false;
|
|
47
|
+
release();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
async function acquireMaintenanceBarrierAsync() {
|
|
51
|
+
const deadline = Date.now() + 5_000;
|
|
52
|
+
while (true) {
|
|
53
|
+
const release = tryAcquireMaintenanceBarrier();
|
|
54
|
+
if (release)
|
|
55
|
+
return release;
|
|
56
|
+
if (Date.now() >= deadline)
|
|
57
|
+
return acquireMaintenanceBarrier();
|
|
58
|
+
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
export async function withMaintenanceStartBarrierAsync(run) {
|
|
62
|
+
if (heldBarrierContext.getStore()?.active)
|
|
63
|
+
return run();
|
|
64
|
+
const release = await acquireMaintenanceBarrierAsync();
|
|
65
|
+
const ownership = { active: true };
|
|
66
|
+
try {
|
|
67
|
+
return await heldBarrierContext.run(ownership, run);
|
|
68
|
+
}
|
|
69
|
+
finally {
|
|
70
|
+
ownership.active = false;
|
|
71
|
+
release();
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function withMaintenanceStartBarrierSyncWait(run) {
|
|
75
|
+
if (heldBarrierContext.getStore()?.active)
|
|
76
|
+
return run();
|
|
77
|
+
const deadline = Date.now() + 5_000;
|
|
78
|
+
let release = tryAcquireMaintenanceBarrier();
|
|
79
|
+
while (!release && Date.now() < deadline) {
|
|
80
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 5);
|
|
81
|
+
release = tryAcquireMaintenanceBarrier();
|
|
82
|
+
}
|
|
83
|
+
if (!release)
|
|
84
|
+
release = acquireMaintenanceBarrier();
|
|
85
|
+
const ownership = { active: true };
|
|
86
|
+
try {
|
|
87
|
+
return heldBarrierContext.run(ownership, run);
|
|
88
|
+
}
|
|
89
|
+
finally {
|
|
90
|
+
ownership.active = false;
|
|
91
|
+
release();
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/** Register a long-lived operation atomically with restore's blocker scan. */
|
|
95
|
+
export async function acquireMaintenanceActivity(name) {
|
|
96
|
+
return withMaintenanceStartBarrierAsync(async () => {
|
|
97
|
+
const directory = path.join(path.dirname(getMaintenanceBarrierPath()), "maintenance-activities");
|
|
98
|
+
fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
99
|
+
const lockPath = path.join(directory, `${name}-${process.pid}-${randomUUID()}.lock`);
|
|
100
|
+
const ownership = tryAcquireLockSync(lockPath, createLockPayload({ purpose: name }));
|
|
101
|
+
if (!ownership) {
|
|
102
|
+
throw new ConfigError(`Could not register AKM maintenance activity at ${lockPath}.`, "INVALID_CONFIG_FILE");
|
|
103
|
+
}
|
|
104
|
+
return () => releaseLock(ownership);
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
/** Synchronous activity registration for synchronous database handle lifetimes. */
|
|
108
|
+
export function acquireMaintenanceActivitySync(name) {
|
|
109
|
+
return withMaintenanceStartBarrierSyncWait(() => {
|
|
110
|
+
const directory = path.join(path.dirname(getMaintenanceBarrierPath()), "maintenance-activities");
|
|
111
|
+
fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
112
|
+
const lockPath = path.join(directory, `${name}-${process.pid}-${randomUUID()}.lock`);
|
|
113
|
+
const ownership = tryAcquireLockSync(lockPath, createLockPayload({ purpose: name }));
|
|
114
|
+
if (!ownership) {
|
|
115
|
+
throw new ConfigError(`Could not register AKM maintenance activity at ${lockPath}.`, "INVALID_CONFIG_FILE");
|
|
116
|
+
}
|
|
117
|
+
return () => releaseLock(ownership);
|
|
118
|
+
});
|
|
119
|
+
}
|