@skillsmith/core 0.11.4 → 0.11.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 +60 -0
- package/README.md +10 -7
- package/dist/.tsbuildinfo +1 -1
- package/dist/src/api/client.d.ts.map +1 -1
- package/dist/src/api/client.js +20 -5
- package/dist/src/api/client.js.map +1 -1
- package/dist/src/api/client.private-registry.d.ts +80 -0
- package/dist/src/api/client.private-registry.d.ts.map +1 -0
- package/dist/src/api/client.private-registry.js +97 -0
- package/dist/src/api/client.private-registry.js.map +1 -0
- package/dist/src/api/client.private-registry.test.d.ts +9 -0
- package/dist/src/api/client.private-registry.test.d.ts.map +1 -0
- package/dist/src/api/client.private-registry.test.js +127 -0
- package/dist/src/api/client.private-registry.test.js.map +1 -0
- package/dist/src/api/client.test.js +39 -0
- package/dist/src/api/client.test.js.map +1 -1
- package/dist/src/api/client.toSkill.test.d.ts +25 -0
- package/dist/src/api/client.toSkill.test.d.ts.map +1 -0
- package/dist/src/api/client.toSkill.test.js +97 -0
- package/dist/src/api/client.toSkill.test.js.map +1 -0
- package/dist/src/api/client.token-refresh.d.ts +8 -0
- package/dist/src/api/client.token-refresh.d.ts.map +1 -1
- package/dist/src/api/client.token-refresh.js +26 -0
- package/dist/src/api/client.token-refresh.js.map +1 -1
- package/dist/src/api/client.token-refresh.test.js +82 -0
- package/dist/src/api/client.token-refresh.test.js.map +1 -1
- package/dist/src/api/index.d.ts +2 -0
- package/dist/src/api/index.d.ts.map +1 -1
- package/dist/src/api/index.js +9 -0
- package/dist/src/api/index.js.map +1 -1
- package/dist/src/api/security-summary.d.ts +73 -0
- package/dist/src/api/security-summary.d.ts.map +1 -0
- package/dist/src/api/security-summary.js +88 -0
- package/dist/src/api/security-summary.js.map +1 -0
- package/dist/src/api/security-summary.test.d.ts +9 -0
- package/dist/src/api/security-summary.test.d.ts.map +1 -0
- package/dist/src/api/security-summary.test.js +94 -0
- package/dist/src/api/security-summary.test.js.map +1 -0
- package/dist/src/compatibility/slugs.d.ts +7 -3
- package/dist/src/compatibility/slugs.d.ts.map +1 -1
- package/dist/src/compatibility/slugs.js +11 -4
- package/dist/src/compatibility/slugs.js.map +1 -1
- package/dist/src/config/config-atomic-write.d.ts +30 -10
- package/dist/src/config/config-atomic-write.d.ts.map +1 -1
- package/dist/src/config/config-atomic-write.js +33 -80
- package/dist/src/config/config-atomic-write.js.map +1 -1
- package/dist/src/config/config-atomic-write.test.d.ts +8 -0
- package/dist/src/config/config-atomic-write.test.d.ts.map +1 -1
- package/dist/src/config/config-atomic-write.test.js +64 -12
- package/dist/src/config/config-atomic-write.test.js.map +1 -1
- package/dist/src/config/owned-lock.acquire.d.ts +53 -0
- package/dist/src/config/owned-lock.acquire.d.ts.map +1 -0
- package/dist/src/config/owned-lock.acquire.js +140 -0
- package/dist/src/config/owned-lock.acquire.js.map +1 -0
- package/dist/src/config/owned-lock.claim.d.ts +84 -0
- package/dist/src/config/owned-lock.claim.d.ts.map +1 -0
- package/dist/src/config/owned-lock.claim.js +310 -0
- package/dist/src/config/owned-lock.claim.js.map +1 -0
- package/dist/src/config/owned-lock.d.ts +113 -0
- package/dist/src/config/owned-lock.d.ts.map +1 -0
- package/dist/src/config/owned-lock.js +121 -0
- package/dist/src/config/owned-lock.js.map +1 -0
- package/dist/src/config/owned-lock.test.d.ts +12 -0
- package/dist/src/config/owned-lock.test.d.ts.map +1 -0
- package/dist/src/config/owned-lock.test.js +412 -0
- package/dist/src/config/owned-lock.test.js.map +1 -0
- package/dist/src/config/owned-lock.types.d.ts +78 -0
- package/dist/src/config/owned-lock.types.d.ts.map +1 -0
- package/dist/src/config/owned-lock.types.js +30 -0
- package/dist/src/config/owned-lock.types.js.map +1 -0
- package/dist/src/db/createDatabase.d.ts.map +1 -1
- package/dist/src/db/createDatabase.js +19 -7
- package/dist/src/db/createDatabase.js.map +1 -1
- package/dist/src/db/drivers/sqljsDriver.d.ts +8 -0
- package/dist/src/db/drivers/sqljsDriver.d.ts.map +1 -1
- package/dist/src/db/drivers/sqljsDriver.js +24 -2
- package/dist/src/db/drivers/sqljsDriver.js.map +1 -1
- package/dist/src/db/migration-runner.d.ts.map +1 -1
- package/dist/src/db/migration-runner.js +23 -2
- package/dist/src/db/migration-runner.js.map +1 -1
- package/dist/src/db/schema.d.ts.map +1 -1
- package/dist/src/db/schema.js +29 -3
- package/dist/src/db/schema.js.map +1 -1
- package/dist/src/embeddings/hnsw-store.exports.d.ts +11 -0
- package/dist/src/embeddings/hnsw-store.exports.d.ts.map +1 -0
- package/dist/src/embeddings/hnsw-store.exports.js +10 -0
- package/dist/src/embeddings/hnsw-store.exports.js.map +1 -0
- package/dist/src/embeddings/index.d.ts +1 -2
- package/dist/src/embeddings/index.d.ts.map +1 -1
- package/dist/src/embeddings/index.js +17 -3
- package/dist/src/embeddings/index.js.map +1 -1
- package/dist/src/embeddings/probe.d.ts.map +1 -1
- package/dist/src/embeddings/probe.js +4 -8
- package/dist/src/embeddings/probe.js.map +1 -1
- package/dist/src/exports/services.d.ts +5 -1
- package/dist/src/exports/services.d.ts.map +1 -1
- package/dist/src/exports/services.js +27 -0
- package/dist/src/exports/services.js.map +1 -1
- package/dist/src/index.d.ts +3 -3
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +9 -3
- package/dist/src/index.js.map +1 -1
- package/dist/src/install/index.d.ts +2 -2
- package/dist/src/install/index.d.ts.map +1 -1
- package/dist/src/install/index.js +1 -1
- package/dist/src/install/index.js.map +1 -1
- package/dist/src/install/paths.d.ts +165 -1
- package/dist/src/install/paths.d.ts.map +1 -1
- package/dist/src/install/paths.js +254 -1
- package/dist/src/install/paths.js.map +1 -1
- package/dist/src/install/paths.test.js +174 -2
- package/dist/src/install/paths.test.js.map +1 -1
- package/dist/src/security/scanner/multiline-category-closure.test.d.ts +80 -0
- package/dist/src/security/scanner/multiline-category-closure.test.d.ts.map +1 -0
- package/dist/src/security/scanner/multiline-category-closure.test.js +287 -0
- package/dist/src/security/scanner/multiline-category-closure.test.js.map +1 -0
- package/dist/src/services/context-words.d.ts +40 -0
- package/dist/src/services/context-words.d.ts.map +1 -0
- package/dist/src/services/context-words.js +178 -0
- package/dist/src/services/context-words.js.map +1 -0
- package/dist/src/services/context-words.test.d.ts +2 -0
- package/dist/src/services/context-words.test.d.ts.map +1 -0
- package/dist/src/services/context-words.test.js +94 -0
- package/dist/src/services/context-words.test.js.map +1 -0
- package/dist/src/services/recommend-guard.d.ts +28 -0
- package/dist/src/services/recommend-guard.d.ts.map +1 -0
- package/dist/src/services/recommend-guard.js +34 -0
- package/dist/src/services/recommend-guard.js.map +1 -0
- package/dist/src/services/recommend-guard.test.d.ts +2 -0
- package/dist/src/services/recommend-guard.test.d.ts.map +1 -0
- package/dist/src/services/recommend-guard.test.js +28 -0
- package/dist/src/services/recommend-guard.test.js.map +1 -0
- package/dist/src/services/skill-installation.content.d.ts +96 -0
- package/dist/src/services/skill-installation.content.d.ts.map +1 -0
- package/dist/src/services/skill-installation.content.js +319 -0
- package/dist/src/services/skill-installation.content.js.map +1 -0
- package/dist/src/services/skill-installation.helpers.d.ts +32 -1
- package/dist/src/services/skill-installation.helpers.d.ts.map +1 -1
- package/dist/src/services/skill-installation.helpers.js +62 -7
- package/dist/src/services/skill-installation.helpers.js.map +1 -1
- package/dist/src/services/skill-installation.helpers.test.js +108 -2
- package/dist/src/services/skill-installation.helpers.test.js.map +1 -1
- package/dist/src/services/skill-installation.io.d.ts +22 -1
- package/dist/src/services/skill-installation.io.d.ts.map +1 -1
- package/dist/src/services/skill-installation.io.js +110 -6
- package/dist/src/services/skill-installation.io.js.map +1 -1
- package/dist/src/services/skill-installation.service.d.ts +22 -1
- package/dist/src/services/skill-installation.service.d.ts.map +1 -1
- package/dist/src/services/skill-installation.service.js +30 -5
- package/dist/src/services/skill-installation.service.js.map +1 -1
- package/dist/src/services/skill-installation.types.d.ts +42 -1
- package/dist/src/services/skill-installation.types.d.ts.map +1 -1
- package/dist/src/services/skill-installation.types.js.map +1 -1
- package/dist/src/services/skill-manifest.d.ts +21 -0
- package/dist/src/services/skill-manifest.d.ts.map +1 -1
- package/dist/src/services/skill-manifest.js +51 -6
- package/dist/src/services/skill-manifest.js.map +1 -1
- package/dist/src/services/skill-manifest.test.d.ts +2 -0
- package/dist/src/services/skill-manifest.test.d.ts.map +1 -0
- package/dist/src/services/skill-manifest.test.js +202 -0
- package/dist/src/services/skill-manifest.test.js.map +1 -0
- package/dist/src/services/skill-resolution.d.ts +67 -0
- package/dist/src/services/skill-resolution.d.ts.map +1 -0
- package/dist/src/services/skill-resolution.js +67 -0
- package/dist/src/services/skill-resolution.js.map +1 -0
- package/dist/src/services/skill-resolution.test.d.ts +2 -0
- package/dist/src/services/skill-resolution.test.d.ts.map +1 -0
- package/dist/src/services/skill-resolution.test.js +143 -0
- package/dist/src/services/skill-resolution.test.js.map +1 -0
- package/dist/src/types/skill.d.ts +10 -0
- package/dist/src/types/skill.d.ts.map +1 -1
- package/dist/src/types.d.ts +9 -5
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/utils/index.d.ts +1 -0
- package/dist/src/utils/index.d.ts.map +1 -1
- package/dist/src/utils/index.js +4 -0
- package/dist/src/utils/index.js.map +1 -1
- package/dist/src/utils/quiet-mode.d.ts +21 -0
- package/dist/src/utils/quiet-mode.d.ts.map +1 -0
- package/dist/src/utils/quiet-mode.js +26 -0
- package/dist/src/utils/quiet-mode.js.map +1 -0
- package/dist/tests/EmbeddingService.test.js +58 -1
- package/dist/tests/EmbeddingService.test.js.map +1 -1
- package/dist/tests/SecurityScanner.performance.test.js +5 -2
- package/dist/tests/SecurityScanner.performance.test.js.map +1 -1
- package/dist/tests/db/database-abstraction.test.js +45 -1
- package/dist/tests/db/database-abstraction.test.js.map +1 -1
- package/dist/tests/db/migration-runner-race.test.d.ts +2 -0
- package/dist/tests/db/migration-runner-race.test.d.ts.map +1 -0
- package/dist/tests/db/migration-runner-race.test.js +75 -0
- package/dist/tests/db/migration-runner-race.test.js.map +1 -0
- package/dist/tests/db/schema-async.test.js +37 -1
- package/dist/tests/db/schema-async.test.js.map +1 -1
- package/dist/tests/db/sqljsDriver.test.js +63 -1
- package/dist/tests/db/sqljsDriver.test.js.map +1 -1
- package/dist/tests/helpers/deterministic-dead-pid.d.ts +13 -0
- package/dist/tests/helpers/deterministic-dead-pid.d.ts.map +1 -0
- package/dist/tests/helpers/deterministic-dead-pid.js +31 -0
- package/dist/tests/helpers/deterministic-dead-pid.js.map +1 -0
- package/dist/tests/integration/owned-lock-lost-update.test.d.ts +12 -0
- package/dist/tests/integration/owned-lock-lost-update.test.d.ts.map +1 -0
- package/dist/tests/integration/owned-lock-lost-update.test.js +52 -0
- package/dist/tests/integration/owned-lock-lost-update.test.js.map +1 -0
- package/dist/tests/integration/owned-lock-reclaim-race.test.d.ts +19 -0
- package/dist/tests/integration/owned-lock-reclaim-race.test.d.ts.map +1 -0
- package/dist/tests/integration/owned-lock-reclaim-race.test.js +177 -0
- package/dist/tests/integration/owned-lock-reclaim-race.test.js.map +1 -0
- package/dist/tests/schema.test.js +43 -1
- package/dist/tests/schema.test.js.map +1 -1
- package/dist/tests/unit/services/skill-installation.content.test.d.ts +10 -0
- package/dist/tests/unit/services/skill-installation.content.test.d.ts.map +1 -0
- package/dist/tests/unit/services/skill-installation.content.test.js +290 -0
- package/dist/tests/unit/services/skill-installation.content.test.js.map +1 -0
- package/dist/tests/unit/services/skill-installation.io.client-agent-path.test.d.ts +34 -0
- package/dist/tests/unit/services/skill-installation.io.client-agent-path.test.d.ts.map +1 -0
- package/dist/tests/unit/services/skill-installation.io.client-agent-path.test.js +246 -0
- package/dist/tests/unit/services/skill-installation.io.client-agent-path.test.js.map +1 -0
- package/dist/tests/unit/services/skill-installation.io.symlink.test.d.ts +8 -0
- package/dist/tests/unit/services/skill-installation.io.symlink.test.d.ts.map +1 -0
- package/dist/tests/unit/services/skill-installation.io.symlink.test.js +72 -0
- package/dist/tests/unit/services/skill-installation.io.symlink.test.js.map +1 -0
- package/dist/tests/unit/services/skill-installation.service.companion-basedir.test.d.ts +17 -0
- package/dist/tests/unit/services/skill-installation.service.companion-basedir.test.d.ts.map +1 -0
- package/dist/tests/unit/services/skill-installation.service.companion-basedir.test.js +112 -0
- package/dist/tests/unit/services/skill-installation.service.companion-basedir.test.js.map +1 -0
- package/dist/tests/unit/services/skill-installation.service.error-codes.test.d.ts +15 -0
- package/dist/tests/unit/services/skill-installation.service.error-codes.test.d.ts.map +1 -0
- package/dist/tests/unit/services/skill-installation.service.error-codes.test.js +190 -0
- package/dist/tests/unit/services/skill-installation.service.error-codes.test.js.map +1 -0
- package/dist/tests/unit/services/skill-installation.service.multi-client.test.d.ts +17 -0
- package/dist/tests/unit/services/skill-installation.service.multi-client.test.d.ts.map +1 -0
- package/dist/tests/unit/services/skill-installation.service.multi-client.test.js +126 -0
- package/dist/tests/unit/services/skill-installation.service.multi-client.test.js.map +1 -0
- package/dist/tests/unit/services/skill-installation.service.test.js +7 -112
- package/dist/tests/unit/services/skill-installation.service.test.js.map +1 -1
- package/dist/tests/utils/quiet-mode.test.d.ts +12 -0
- package/dist/tests/utils/quiet-mode.test.d.ts.map +1 -0
- package/dist/tests/utils/quiet-mode.test.js +42 -0
- package/dist/tests/utils/quiet-mode.test.js.map +1 -0
- package/package.json +17 -5
|
@@ -31,11 +31,15 @@ export const COMPATIBILITY_LABELS = {
|
|
|
31
31
|
};
|
|
32
32
|
/**
|
|
33
33
|
* Slugs that are filterable/browsable but have NO install target yet — the
|
|
34
|
-
* install-client enum (`install/paths.ts` `ClientId`) lacks them.
|
|
35
|
-
* `antigravity
|
|
36
|
-
*
|
|
34
|
+
* install-client enum (`install/paths.ts` `ClientId`) lacks them.
|
|
35
|
+
* `antigravity` was un-deferred (SMI-5982 Wave 6) — it now has a real
|
|
36
|
+
* `ClientId` + `CLIENT_NATIVE_PATHS` entry, so it moved out of this list.
|
|
37
|
+
* `gemini` has no install target (it names a model family, not a distinct
|
|
38
|
+
* install client) and remains browse-only; adding it (if ever justified) is
|
|
39
|
+
* still Phase 2c (SMI-5179). Until then the UI labels `gemini` "browse-only"
|
|
40
|
+
* rather than implying installability.
|
|
37
41
|
*/
|
|
38
|
-
export const BROWSE_ONLY_SLUGS = ['
|
|
42
|
+
export const BROWSE_ONLY_SLUGS = ['gemini'];
|
|
39
43
|
/**
|
|
40
44
|
* Map an install `ClientId` to the compatibility slug to restrict by when that
|
|
41
45
|
* client is the user's explicit tool (MCP restrictive default, SMI-5178).
|
|
@@ -49,6 +53,9 @@ export const CLIENT_TO_COMPATIBILITY_SLUG = {
|
|
|
49
53
|
copilot: 'copilot',
|
|
50
54
|
windsurf: 'windsurf',
|
|
51
55
|
agents: 'codex',
|
|
56
|
+
// SMI-5982 Wave 6: antigravity is now a real ClientId with its own 1:1
|
|
57
|
+
// compatibility slug (same pattern as claude-code/cursor/copilot/windsurf).
|
|
58
|
+
antigravity: 'antigravity',
|
|
52
59
|
};
|
|
53
60
|
/** Display label for a slug; falls back to the raw slug for unknown values. */
|
|
54
61
|
export function compatibilityLabel(slug) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"slugs.js","sourceRoot":"","sources":["../../../src/compatibility/slugs.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,6FAA6F;AAC7F,MAAM,CAAC,MAAM,mBAAmB,GAAG;IACjC,aAAa;IACb,QAAQ;IACR,SAAS;IACT,UAAU;IACV,aAAa;IACb,OAAO;IACP,QAAQ;CACA,CAAA;AAIV,8DAA8D;AAC9D,MAAM,CAAC,MAAM,oBAAoB,GAAsC;IACrE,aAAa,EAAE,aAAa;IAC5B,MAAM,EAAE,QAAQ;IAChB,OAAO,EAAE,gBAAgB;IACzB,QAAQ,EAAE,UAAU;IACpB,WAAW,EAAE,aAAa;IAC1B,KAAK,EAAE,OAAO;IACd,MAAM,EAAE,QAAQ;CACjB,CAAA;AAED
|
|
1
|
+
{"version":3,"file":"slugs.js","sourceRoot":"","sources":["../../../src/compatibility/slugs.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,6FAA6F;AAC7F,MAAM,CAAC,MAAM,mBAAmB,GAAG;IACjC,aAAa;IACb,QAAQ;IACR,SAAS;IACT,UAAU;IACV,aAAa;IACb,OAAO;IACP,QAAQ;CACA,CAAA;AAIV,8DAA8D;AAC9D,MAAM,CAAC,MAAM,oBAAoB,GAAsC;IACrE,aAAa,EAAE,aAAa;IAC5B,MAAM,EAAE,QAAQ;IAChB,OAAO,EAAE,gBAAgB;IACzB,QAAQ,EAAE,UAAU;IACpB,WAAW,EAAE,aAAa;IAC1B,KAAK,EAAE,OAAO;IACd,MAAM,EAAE,QAAQ;CACjB,CAAA;AAED;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAiC,CAAC,QAAQ,CAAC,CAAA;AAEzE;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,4BAA4B,GAAsC;IAC7E,aAAa,EAAE,aAAa;IAC5B,MAAM,EAAE,QAAQ;IAChB,OAAO,EAAE,SAAS;IAClB,QAAQ,EAAE,UAAU;IACpB,MAAM,EAAE,OAAO;IACf,uEAAuE;IACvE,4EAA4E;IAC5E,WAAW,EAAE,aAAa;CAC3B,CAAA;AAED,+EAA+E;AAC/E,MAAM,UAAU,kBAAkB,CAAC,IAAY;IAC7C,OAAQ,oBAA+C,CAAC,IAAI,CAAC,IAAI,IAAI,CAAA;AACvE,CAAC"}
|
|
@@ -17,9 +17,17 @@
|
|
|
17
17
|
* principle observe a partially-written file.
|
|
18
18
|
*
|
|
19
19
|
* This module fixes both:
|
|
20
|
-
* - {@link acquireConfigLock} serializes ALL writers (cross-process
|
|
21
|
-
*
|
|
22
|
-
*
|
|
20
|
+
* - {@link acquireConfigLock} serializes ALL writers (cross-process) so a
|
|
21
|
+
* caller's full read-modify-write sequence is one atomic critical
|
|
22
|
+
* section. SMI-5883 Wave 2: this is now a thin wrapper over the shared
|
|
23
|
+
* two-level {@link acquireOwnedLock} primitive (`owned-lock.ts`) —
|
|
24
|
+
* staleness is determined by OWNER LIVENESS (not file age), and a stale
|
|
25
|
+
* lock is reclaimed only from inside a second, strict-no-auto-reclaim
|
|
26
|
+
* lock that serializes reclaim decisions. The prior age-based
|
|
27
|
+
* `STALE_LOCK_AGE_MS` force-clear is REMOVED, not tuned — a lock held
|
|
28
|
+
* legitimately longer than any timeout now correctly times out instead
|
|
29
|
+
* of being force-cleared out from under its live holder. See
|
|
30
|
+
* `owned-lock.ts`'s module docstring for the full soundness argument.
|
|
23
31
|
* - {@link atomicWriteFile} writes to a temp file in the same directory,
|
|
24
32
|
* then `renameSync`s it into place — POSIX guarantees `rename(2)` is
|
|
25
33
|
* atomic, so a concurrent reader (which never needs the lock — only
|
|
@@ -27,14 +35,26 @@
|
|
|
27
35
|
* never a torn write.
|
|
28
36
|
*/
|
|
29
37
|
/**
|
|
30
|
-
* Acquire an exclusive, cross-process lock guarding `configPath`.
|
|
38
|
+
* Acquire an exclusive, cross-process lock guarding `configPath`. Thin
|
|
39
|
+
* wrapper over {@link acquireOwnedLock} (SMI-5883 Wave 2) — preserves this
|
|
40
|
+
* function's signature and its `${configPath}.lock` path so no caller needs
|
|
41
|
+
* to change. Behaviour deltas vs the pre-SMI-5883 implementation (each
|
|
42
|
+
* covered by a test in `config-atomic-write.test.ts`):
|
|
31
43
|
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
44
|
+
* 1. A stale lock is cleared on OWNER DEATH, not on AGE. A lock held by a
|
|
45
|
+
* live process for longer than the old 10s threshold is no longer
|
|
46
|
+
* force-cleared — it now correctly waits, then times out.
|
|
47
|
+
* 2. A legacy bare-PID lock (this module's OWN pre-SMI-5883 on-disk format)
|
|
48
|
+
* is no longer cleared at all — `StuckLockError { reason:
|
|
49
|
+
* 'unreclaimable_legacy' }` (D-5: a legacy claim carries no `host`, so
|
|
50
|
+
* "dead on this host" cannot establish "dead").
|
|
51
|
+
* 3. The on-disk lock content changes from a bare PID to a v1 JSON record.
|
|
52
|
+
* No in-repo consumer parses it directly (only this module's own
|
|
53
|
+
* `acquireConfigLock`/release touch the file).
|
|
54
|
+
* 4. The timeout message keeps the stable prefix "Timed out waiting for
|
|
55
|
+
* config lock" (via `label: 'config lock'`) so existing assertions
|
|
56
|
+
* still match, with the failure reason and the manual unstick procedure
|
|
57
|
+
* appended.
|
|
38
58
|
*
|
|
39
59
|
* @param configPath - Path to the config file being guarded (NOT the lock
|
|
40
60
|
* file itself — the lock file is `${configPath}.lock`).
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config-atomic-write.d.ts","sourceRoot":"","sources":["../../../src/config/config-atomic-write.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"config-atomic-write.d.ts","sourceRoot":"","sources":["../../../src/config/config-atomic-write.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAeH;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAgB,iBAAiB,CAC/B,UAAU,EAAE,MAAM,EAClB,SAAS,GAAE,MAAgC,GAC1C,MAAM,IAAI,CAEZ;AAED;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAarF"}
|
|
@@ -17,9 +17,17 @@
|
|
|
17
17
|
* principle observe a partially-written file.
|
|
18
18
|
*
|
|
19
19
|
* This module fixes both:
|
|
20
|
-
* - {@link acquireConfigLock} serializes ALL writers (cross-process
|
|
21
|
-
*
|
|
22
|
-
*
|
|
20
|
+
* - {@link acquireConfigLock} serializes ALL writers (cross-process) so a
|
|
21
|
+
* caller's full read-modify-write sequence is one atomic critical
|
|
22
|
+
* section. SMI-5883 Wave 2: this is now a thin wrapper over the shared
|
|
23
|
+
* two-level {@link acquireOwnedLock} primitive (`owned-lock.ts`) —
|
|
24
|
+
* staleness is determined by OWNER LIVENESS (not file age), and a stale
|
|
25
|
+
* lock is reclaimed only from inside a second, strict-no-auto-reclaim
|
|
26
|
+
* lock that serializes reclaim decisions. The prior age-based
|
|
27
|
+
* `STALE_LOCK_AGE_MS` force-clear is REMOVED, not tuned — a lock held
|
|
28
|
+
* legitimately longer than any timeout now correctly times out instead
|
|
29
|
+
* of being force-cleared out from under its live holder. See
|
|
30
|
+
* `owned-lock.ts`'s module docstring for the full soundness argument.
|
|
23
31
|
* - {@link atomicWriteFile} writes to a temp file in the same directory,
|
|
24
32
|
* then `renameSync`s it into place — POSIX guarantees `rename(2)` is
|
|
25
33
|
* atomic, so a concurrent reader (which never needs the lock — only
|
|
@@ -27,42 +35,36 @@
|
|
|
27
35
|
* never a torn write.
|
|
28
36
|
*/
|
|
29
37
|
import { randomBytes } from 'node:crypto';
|
|
30
|
-
import {
|
|
38
|
+
import { writeFileSync, renameSync, chmodSync } from 'node:fs';
|
|
31
39
|
import { dirname, join } from 'node:path';
|
|
40
|
+
import { acquireOwnedLock } from './owned-lock.js';
|
|
32
41
|
/**
|
|
33
42
|
* Max time to wait for a lock before giving up (ms). This guards a
|
|
34
43
|
* user-facing CLI/MCP-server startup path, so it must fail loudly with a
|
|
35
44
|
* clear error rather than hang indefinitely.
|
|
36
45
|
*/
|
|
37
46
|
const LOCK_ACQUIRE_TIMEOUT_MS = 5_000;
|
|
38
|
-
/** Backoff between lock-acquisition retries (ms). */
|
|
39
|
-
const LOCK_RETRY_DELAY_MS = 20;
|
|
40
47
|
/**
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
|
|
46
|
-
const STALE_LOCK_AGE_MS = 10_000;
|
|
47
|
-
/**
|
|
48
|
-
* Synchronously block the calling thread for `ms` milliseconds without
|
|
49
|
-
* spinning the CPU. `Atomics.wait` on a throwaway `SharedArrayBuffer` is the
|
|
50
|
-
* standard synchronous-sleep primitive in Node (unlike browsers, Node does
|
|
51
|
-
* not forbid calling it on the main thread).
|
|
52
|
-
*/
|
|
53
|
-
function sleepSync(ms) {
|
|
54
|
-
const view = new Int32Array(new SharedArrayBuffer(4));
|
|
55
|
-
Atomics.wait(view, 0, 0, ms);
|
|
56
|
-
}
|
|
57
|
-
/**
|
|
58
|
-
* Acquire an exclusive, cross-process lock guarding `configPath`.
|
|
48
|
+
* Acquire an exclusive, cross-process lock guarding `configPath`. Thin
|
|
49
|
+
* wrapper over {@link acquireOwnedLock} (SMI-5883 Wave 2) — preserves this
|
|
50
|
+
* function's signature and its `${configPath}.lock` path so no caller needs
|
|
51
|
+
* to change. Behaviour deltas vs the pre-SMI-5883 implementation (each
|
|
52
|
+
* covered by a test in `config-atomic-write.test.ts`):
|
|
59
53
|
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
54
|
+
* 1. A stale lock is cleared on OWNER DEATH, not on AGE. A lock held by a
|
|
55
|
+
* live process for longer than the old 10s threshold is no longer
|
|
56
|
+
* force-cleared — it now correctly waits, then times out.
|
|
57
|
+
* 2. A legacy bare-PID lock (this module's OWN pre-SMI-5883 on-disk format)
|
|
58
|
+
* is no longer cleared at all — `StuckLockError { reason:
|
|
59
|
+
* 'unreclaimable_legacy' }` (D-5: a legacy claim carries no `host`, so
|
|
60
|
+
* "dead on this host" cannot establish "dead").
|
|
61
|
+
* 3. The on-disk lock content changes from a bare PID to a v1 JSON record.
|
|
62
|
+
* No in-repo consumer parses it directly (only this module's own
|
|
63
|
+
* `acquireConfigLock`/release touch the file).
|
|
64
|
+
* 4. The timeout message keeps the stable prefix "Timed out waiting for
|
|
65
|
+
* config lock" (via `label: 'config lock'`) so existing assertions
|
|
66
|
+
* still match, with the failure reason and the manual unstick procedure
|
|
67
|
+
* appended.
|
|
66
68
|
*
|
|
67
69
|
* @param configPath - Path to the config file being guarded (NOT the lock
|
|
68
70
|
* file itself — the lock file is `${configPath}.lock`).
|
|
@@ -72,56 +74,7 @@ function sleepSync(ms) {
|
|
|
72
74
|
* `finally` block, so the lock is released even if the guarded work throws.
|
|
73
75
|
*/
|
|
74
76
|
export function acquireConfigLock(configPath, timeoutMs = LOCK_ACQUIRE_TIMEOUT_MS) {
|
|
75
|
-
|
|
76
|
-
const deadline = Date.now() + timeoutMs;
|
|
77
|
-
let staleClearAttempted = false;
|
|
78
|
-
for (;;) {
|
|
79
|
-
try {
|
|
80
|
-
const fd = openSync(lockPath, 'wx', 0o600);
|
|
81
|
-
try {
|
|
82
|
-
writeFileSync(fd, String(process.pid));
|
|
83
|
-
}
|
|
84
|
-
finally {
|
|
85
|
-
closeSync(fd);
|
|
86
|
-
}
|
|
87
|
-
return () => {
|
|
88
|
-
try {
|
|
89
|
-
unlinkSync(lockPath);
|
|
90
|
-
}
|
|
91
|
-
catch {
|
|
92
|
-
// Already gone. Releasing an already-released lock is a no-op,
|
|
93
|
-
// not an error (defensive — should not happen in practice since
|
|
94
|
-
// each acquire pairs with exactly one release).
|
|
95
|
-
}
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
catch (err) {
|
|
99
|
-
if (err.code !== 'EEXIST')
|
|
100
|
-
throw err;
|
|
101
|
-
if (!staleClearAttempted) {
|
|
102
|
-
staleClearAttempted = true;
|
|
103
|
-
try {
|
|
104
|
-
const age = Date.now() - statSync(lockPath).mtimeMs;
|
|
105
|
-
if (age > STALE_LOCK_AGE_MS) {
|
|
106
|
-
unlinkSync(lockPath);
|
|
107
|
-
continue; // retry immediately — don't burn the backoff budget on this attempt
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
catch {
|
|
111
|
-
// Lock file vanished between the failed open and this stat (the
|
|
112
|
-
// holder released it concurrently, or another waiter already
|
|
113
|
-
// cleared it) — just retry the open below.
|
|
114
|
-
continue;
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
if (Date.now() >= deadline) {
|
|
118
|
-
throw new Error(`[skillsmith] Timed out waiting for config lock at ${lockPath} after ` +
|
|
119
|
-
`${timeoutMs}ms. If this persists, a crashed process may have left a ` +
|
|
120
|
-
`stale lock — verify no other skillsmith process is running, then remove ${lockPath} manually.`);
|
|
121
|
-
}
|
|
122
|
-
sleepSync(LOCK_RETRY_DELAY_MS);
|
|
123
|
-
}
|
|
124
|
-
}
|
|
77
|
+
return acquireOwnedLock(configPath, { timeoutMs, label: 'config lock' });
|
|
125
78
|
}
|
|
126
79
|
/**
|
|
127
80
|
* Write `content` to `filePath` atomically: write to a temp file in the same
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config-atomic-write.js","sourceRoot":"","sources":["../../../src/config/config-atomic-write.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"config-atomic-write.js","sourceRoot":"","sources":["../../../src/config/config-atomic-write.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAEH,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AACzC,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,SAAS,CAAA;AAC9D,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAEzC,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAA;AAElD;;;;GAIG;AACH,MAAM,uBAAuB,GAAG,KAAK,CAAA;AAErC;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,MAAM,UAAU,iBAAiB,CAC/B,UAAkB,EAClB,YAAoB,uBAAuB;IAE3C,OAAO,gBAAgB,CAAC,UAAU,EAAE,EAAE,SAAS,EAAE,KAAK,EAAE,aAAa,EAAE,CAAC,CAAA;AAC1E,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,eAAe,CAAC,QAAgB,EAAE,OAAe,EAAE,IAAY;IAC7E,MAAM,GAAG,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAA;IAC7B,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,EAAE,IAAI,WAAW,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAC,CAAA;IACnE,aAAa,CAAC,OAAO,EAAE,OAAO,EAAE,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAA;IAC5D,UAAU,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAA;IAC7B,uEAAuE;IACvE,2EAA2E;IAC3E,mCAAmC;IACnC,IAAI,CAAC;QACH,SAAS,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAA;IAC3B,CAAC;IAAC,MAAM,CAAC;QACP,kCAAkC;IACpC,CAAC;AACH,CAAC"}
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* SMI-5531: Atomic config-write primitive tests.
|
|
3
|
+
* SMI-5883 Wave 2 (§8d/§8e): `acquireConfigLock` is now a thin wrapper over
|
|
4
|
+
* the shared two-level `acquireOwnedLock` primitive (`owned-lock.ts`). The
|
|
5
|
+
* age-based stale-lock test below is DELETED (its premise — clearing on file
|
|
6
|
+
* age — is the behavior being removed), replaced by three tests asserting
|
|
7
|
+
* the new owner-liveness-based contract: a v1 dead-PID lock reclaims
|
|
8
|
+
* quickly, a v1 live-PID lock times out regardless of a backdated mtime, and
|
|
9
|
+
* a legacy bare-PID lock (this module's OWN pre-SMI-5883 format) is NEVER
|
|
10
|
+
* auto-reclaimed even when definitely dead (D-5).
|
|
3
11
|
*
|
|
4
12
|
* Uses the same tmpdir-per-test isolation harness as device-identity.test.ts
|
|
5
13
|
* / index.test.ts (no shared HOME mutation needed here — these primitives
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config-atomic-write.test.d.ts","sourceRoot":"","sources":["../../../src/config/config-atomic-write.test.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"config-atomic-write.test.d.ts","sourceRoot":"","sources":["../../../src/config/config-atomic-write.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG"}
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* SMI-5531: Atomic config-write primitive tests.
|
|
3
|
+
* SMI-5883 Wave 2 (§8d/§8e): `acquireConfigLock` is now a thin wrapper over
|
|
4
|
+
* the shared two-level `acquireOwnedLock` primitive (`owned-lock.ts`). The
|
|
5
|
+
* age-based stale-lock test below is DELETED (its premise — clearing on file
|
|
6
|
+
* age — is the behavior being removed), replaced by three tests asserting
|
|
7
|
+
* the new owner-liveness-based contract: a v1 dead-PID lock reclaims
|
|
8
|
+
* quickly, a v1 live-PID lock times out regardless of a backdated mtime, and
|
|
9
|
+
* a legacy bare-PID lock (this module's OWN pre-SMI-5883 format) is NEVER
|
|
10
|
+
* auto-reclaimed even when definitely dead (D-5).
|
|
3
11
|
*
|
|
4
12
|
* Uses the same tmpdir-per-test isolation harness as device-identity.test.ts
|
|
5
13
|
* / index.test.ts (no shared HOME mutation needed here — these primitives
|
|
@@ -9,7 +17,9 @@ import { describe, it, expect } from 'vitest';
|
|
|
9
17
|
import { existsSync, mkdirSync, readFileSync, readdirSync, utimesSync, writeFileSync } from 'fs';
|
|
10
18
|
import * as path from 'path';
|
|
11
19
|
import * as os from 'os';
|
|
20
|
+
import { hostname } from 'node:os';
|
|
12
21
|
import { acquireConfigLock, atomicWriteFile } from './config-atomic-write.js';
|
|
22
|
+
import { mintDeadPid } from '../../tests/helpers/deterministic-dead-pid.js';
|
|
13
23
|
function makeTempDir() {
|
|
14
24
|
const dir = path.join(os.tmpdir(), `skillsmith-atomic-write-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
15
25
|
mkdirSync(dir, { recursive: true });
|
|
@@ -36,29 +46,71 @@ describe('acquireConfigLock — mutual exclusion', () => {
|
|
|
36
46
|
const secondRelease = acquireConfigLock(configPath, 200);
|
|
37
47
|
secondRelease();
|
|
38
48
|
});
|
|
39
|
-
it('
|
|
49
|
+
it('release() is idempotent — calling it twice does not throw', () => {
|
|
50
|
+
const dir = makeTempDir();
|
|
51
|
+
const configPath = path.join(dir, 'config.json');
|
|
52
|
+
const release = acquireConfigLock(configPath);
|
|
53
|
+
release();
|
|
54
|
+
expect(() => release()).not.toThrow();
|
|
55
|
+
});
|
|
56
|
+
it('reclaims a v1 dead-PID lock quickly (owner-liveness based, not age)', () => {
|
|
40
57
|
const dir = makeTempDir();
|
|
41
58
|
const configPath = path.join(dir, 'config.json');
|
|
42
59
|
const lockPath = `${configPath}.lock`;
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
60
|
+
const deadPid = mintDeadPid();
|
|
61
|
+
writeFileSync(lockPath, JSON.stringify({
|
|
62
|
+
v: 1,
|
|
63
|
+
pid: deadPid,
|
|
64
|
+
token: 'a'.repeat(16),
|
|
65
|
+
host: hostname(),
|
|
66
|
+
acquiredAt: Date.now(),
|
|
67
|
+
}) + '\n');
|
|
48
68
|
const start = Date.now();
|
|
49
|
-
|
|
50
|
-
// succeed almost immediately rather than burning the whole window.
|
|
51
|
-
const release = acquireConfigLock(configPath, 500);
|
|
69
|
+
const release = acquireConfigLock(configPath, 5_000);
|
|
52
70
|
const elapsedMs = Date.now() - start;
|
|
53
71
|
release();
|
|
54
|
-
expect(elapsedMs).toBeLessThan(
|
|
72
|
+
expect(elapsedMs).toBeLessThan(2_000);
|
|
55
73
|
});
|
|
56
|
-
it('
|
|
74
|
+
it('does NOT force-clear a v1 live-PID lock even with a backdated mtime — age is irrelevant now', () => {
|
|
75
|
+
const dir = makeTempDir();
|
|
76
|
+
const configPath = path.join(dir, 'config.json');
|
|
77
|
+
const lockPath = `${configPath}.lock`;
|
|
78
|
+
writeFileSync(lockPath, JSON.stringify({
|
|
79
|
+
v: 1,
|
|
80
|
+
pid: process.pid, // definitively alive for the duration of this test
|
|
81
|
+
token: 'b'.repeat(16),
|
|
82
|
+
host: hostname(),
|
|
83
|
+
acquiredAt: Date.now() - 60_000,
|
|
84
|
+
}) + '\n');
|
|
85
|
+
const longAgo = new Date(Date.now() - 60_000);
|
|
86
|
+
utimesSync(lockPath, longAgo, longAgo);
|
|
87
|
+
const before = readFileSync(lockPath);
|
|
88
|
+
expect(() => acquireConfigLock(configPath, 200)).toThrow(/Timed out waiting for config lock/);
|
|
89
|
+
expect(readFileSync(lockPath).equals(before)).toBe(true);
|
|
90
|
+
});
|
|
91
|
+
it('never auto-reclaims a legacy bare-PID lock even when the PID is definitely live (D-5, §8d)', () => {
|
|
57
92
|
const dir = makeTempDir();
|
|
58
93
|
const configPath = path.join(dir, 'config.json');
|
|
94
|
+
const lockPath = `${configPath}.lock`;
|
|
95
|
+
writeFileSync(lockPath, String(process.pid)); // bare integer, no JSON, definitely live
|
|
96
|
+
const before = readFileSync(lockPath);
|
|
97
|
+
expect(() => acquireConfigLock(configPath, 150)).toThrow(/Timed out waiting for config lock/);
|
|
98
|
+
expect(readFileSync(lockPath).equals(before)).toBe(true); // byte-identical
|
|
99
|
+
expect(existsSync(`${lockPath}.reclaim`)).toBe(false); // no orphan left behind
|
|
100
|
+
expect(readdirSync(dir).filter((f) => f.endsWith('.tmp'))).toEqual([]);
|
|
101
|
+
});
|
|
102
|
+
it('the on-disk lock format is a v1 JSON record, not a bare PID integer', () => {
|
|
103
|
+
const dir = makeTempDir();
|
|
104
|
+
const configPath = path.join(dir, 'config.json');
|
|
105
|
+
const lockPath = `${configPath}.lock`;
|
|
59
106
|
const release = acquireConfigLock(configPath);
|
|
107
|
+
const raw = readFileSync(lockPath, 'utf-8');
|
|
60
108
|
release();
|
|
61
|
-
|
|
109
|
+
const parsed = JSON.parse(raw);
|
|
110
|
+
expect(parsed.v).toBe(1);
|
|
111
|
+
expect(parsed.pid).toBe(process.pid);
|
|
112
|
+
expect(typeof parsed.token).toBe('string');
|
|
113
|
+
expect(parsed.host).toBe(hostname());
|
|
62
114
|
});
|
|
63
115
|
});
|
|
64
116
|
describe('atomicWriteFile', () => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config-atomic-write.test.js","sourceRoot":"","sources":["../../../src/config/config-atomic-write.test.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"config-atomic-write.test.js","sourceRoot":"","sources":["../../../src/config/config-atomic-write.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAC7C,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,YAAY,EAAE,WAAW,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,IAAI,CAAA;AAChG,OAAO,KAAK,IAAI,MAAM,MAAM,CAAA;AAC5B,OAAO,KAAK,EAAE,MAAM,IAAI,CAAA;AACxB,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAA;AAElC,OAAO,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAA;AAC7E,OAAO,EAAE,WAAW,EAAE,MAAM,+CAA+C,CAAA;AAE3E,SAAS,WAAW;IAClB,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CACnB,EAAE,CAAC,MAAM,EAAE,EACX,gCAAgC,IAAI,CAAC,GAAG,EAAE,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CACpF,CAAA;IACD,SAAS,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;IACnC,OAAO,GAAG,CAAA;AACZ,CAAC;AAED,QAAQ,CAAC,sCAAsC,EAAE,GAAG,EAAE;IACpD,EAAE,CAAC,2DAA2D,EAAE,GAAG,EAAE;QACnE,MAAM,GAAG,GAAG,WAAW,EAAE,CAAA;QACzB,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,aAAa,CAAC,CAAA;QAEhD,MAAM,OAAO,GAAG,iBAAiB,CAAC,UAAU,CAAC,CAAA;QAC7C,IAAI,CAAC;YACH,MAAM,CAAC,GAAG,EAAE,CAAC,iBAAiB,CAAC,UAAU,EAAE,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,mCAAmC,CAAC,CAAA;QAC/F,CAAC;gBAAS,CAAC;YACT,OAAO,EAAE,CAAA;QACX,CAAC;IACH,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,yDAAyD,EAAE,GAAG,EAAE;QACjE,MAAM,GAAG,GAAG,WAAW,EAAE,CAAA;QACzB,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,aAAa,CAAC,CAAA;QAEhD,MAAM,OAAO,GAAG,iBAAiB,CAAC,UAAU,CAAC,CAAA;QAC7C,OAAO,EAAE,CAAA;QAET,8DAA8D;QAC9D,MAAM,aAAa,GAAG,iBAAiB,CAAC,UAAU,EAAE,GAAG,CAAC,CAAA;QACxD,aAAa,EAAE,CAAA;IACjB,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,2DAA2D,EAAE,GAAG,EAAE;QACnE,MAAM,GAAG,GAAG,WAAW,EAAE,CAAA;QACzB,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,aAAa,CAAC,CAAA;QAEhD,MAAM,OAAO,GAAG,iBAAiB,CAAC,UAAU,CAAC,CAAA;QAC7C,OAAO,EAAE,CAAA;QACT,MAAM,CAAC,GAAG,EAAE,CAAC,OAAO,EAAE,CAAC,CAAC,GAAG,CAAC,OAAO,EAAE,CAAA;IACvC,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,qEAAqE,EAAE,GAAG,EAAE;QAC7E,MAAM,GAAG,GAAG,WAAW,EAAE,CAAA;QACzB,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,aAAa,CAAC,CAAA;QAChD,MAAM,QAAQ,GAAG,GAAG,UAAU,OAAO,CAAA;QACrC,MAAM,OAAO,GAAG,WAAW,EAAE,CAAA;QAE7B,aAAa,CACX,QAAQ,EACR,IAAI,CAAC,SAAS,CAAC;YACb,CAAC,EAAE,CAAC;YACJ,GAAG,EAAE,OAAO;YACZ,KAAK,EAAE,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC;YACrB,IAAI,EAAE,QAAQ,EAAE;YAChB,UAAU,EAAE,IAAI,CAAC,GAAG,EAAE;SACvB,CAAC,GAAG,IAAI,CACV,CAAA;QAED,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,EAAE,CAAA;QACxB,MAAM,OAAO,GAAG,iBAAiB,CAAC,UAAU,EAAE,KAAK,CAAC,CAAA;QACpD,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK,CAAA;QAEpC,OAAO,EAAE,CAAA;QACT,MAAM,CAAC,SAAS,CAAC,CAAC,YAAY,CAAC,KAAK,CAAC,CAAA;IACvC,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,6FAA6F,EAAE,GAAG,EAAE;QACrG,MAAM,GAAG,GAAG,WAAW,EAAE,CAAA;QACzB,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,aAAa,CAAC,CAAA;QAChD,MAAM,QAAQ,GAAG,GAAG,UAAU,OAAO,CAAA;QAErC,aAAa,CACX,QAAQ,EACR,IAAI,CAAC,SAAS,CAAC;YACb,CAAC,EAAE,CAAC;YACJ,GAAG,EAAE,OAAO,CAAC,GAAG,EAAE,mDAAmD;YACrE,KAAK,EAAE,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC;YACrB,IAAI,EAAE,QAAQ,EAAE;YAChB,UAAU,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,MAAM;SAChC,CAAC,GAAG,IAAI,CACV,CAAA;QACD,MAAM,OAAO,GAAG,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,MAAM,CAAC,CAAA;QAC7C,UAAU,CAAC,QAAQ,EAAE,OAAO,EAAE,OAAO,CAAC,CAAA;QACtC,MAAM,MAAM,GAAG,YAAY,CAAC,QAAQ,CAAC,CAAA;QAErC,MAAM,CAAC,GAAG,EAAE,CAAC,iBAAiB,CAAC,UAAU,EAAE,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,mCAAmC,CAAC,CAAA;QAC7F,MAAM,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;IAC1D,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,4FAA4F,EAAE,GAAG,EAAE;QACpG,MAAM,GAAG,GAAG,WAAW,EAAE,CAAA;QACzB,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,aAAa,CAAC,CAAA;QAChD,MAAM,QAAQ,GAAG,GAAG,UAAU,OAAO,CAAA;QAErC,aAAa,CAAC,QAAQ,EAAE,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAA,CAAC,yCAAyC;QACtF,MAAM,MAAM,GAAG,YAAY,CAAC,QAAQ,CAAC,CAAA;QAErC,MAAM,CAAC,GAAG,EAAE,CAAC,iBAAiB,CAAC,UAAU,EAAE,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,mCAAmC,CAAC,CAAA;QAC7F,MAAM,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA,CAAC,iBAAiB;QAC1E,MAAM,CAAC,UAAU,CAAC,GAAG,QAAQ,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA,CAAC,wBAAwB;QAC9E,MAAM,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAA;IACxE,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,qEAAqE,EAAE,GAAG,EAAE;QAC7E,MAAM,GAAG,GAAG,WAAW,EAAE,CAAA;QACzB,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,aAAa,CAAC,CAAA;QAChD,MAAM,QAAQ,GAAG,GAAG,UAAU,OAAO,CAAA;QAErC,MAAM,OAAO,GAAG,iBAAiB,CAAC,UAAU,CAAC,CAAA;QAC7C,MAAM,GAAG,GAAG,YAAY,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAA;QAC3C,OAAO,EAAE,CAAA;QAET,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAA4D,CAAA;QACzF,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;QACxB,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAA;QACpC,MAAM,CAAC,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAA;QAC1C,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC,CAAA;IACtC,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA;AAEF,QAAQ,CAAC,iBAAiB,EAAE,GAAG,EAAE;IAC/B,EAAE,CAAC,6CAA6C,EAAE,GAAG,EAAE;QACrD,MAAM,GAAG,GAAG,WAAW,EAAE,CAAA;QACzB,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,aAAa,CAAC,CAAA;QAE9C,eAAe,CAAC,QAAQ,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,KAAK,CAAC,CAAA;QAE1D,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAA;IACvE,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,2DAA2D,EAAE,GAAG,EAAE;QACnE,MAAM,GAAG,GAAG,WAAW,EAAE,CAAA;QACzB,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,aAAa,CAAC,CAAA;QAE9C,eAAe,CAAC,QAAQ,EAAE,IAAI,EAAE,KAAK,CAAC,CAAA;QAEtC,MAAM,OAAO,GAAG,WAAW,CAAC,GAAG,CAAC,CAAA;QAChC,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;IAC7D,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,sEAAsE,EAAE,GAAG,EAAE;QAC9E,MAAM,GAAG,GAAG,WAAW,EAAE,CAAA;QACzB,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,aAAa,CAAC,CAAA;QAE9C,eAAe,CAAC,QAAQ,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,KAAK,CAAC,CAAA;QAC1D,eAAe,CAAC,QAAQ,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,KAAK,CAAC,CAAA;QAE1D,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAA;IACvE,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,8BAA8B,EAAE,GAAG,EAAE;QACtC,MAAM,GAAG,GAAG,WAAW,EAAE,CAAA;QACzB,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,aAAa,CAAC,CAAA;QAE9C,eAAe,CAAC,QAAQ,EAAE,IAAI,EAAE,KAAK,CAAC,CAAA;QAEtC,MAAM,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;IACzC,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core acquire loop + `StuckLockError` for the owned-lock primitive.
|
|
3
|
+
* @module @skillsmith/core/config/owned-lock.acquire
|
|
4
|
+
* @see owned-lock.ts for the full soundness argument and the PUBLIC API.
|
|
5
|
+
* @see owned-lock.claim.ts for claim parsing, exclusive creation, and reclaim.
|
|
6
|
+
*
|
|
7
|
+
* INTERNAL module -- not part of the public surface (no `package.json`
|
|
8
|
+
* subpath export), split out of `owned-lock.claim.ts` purely to keep both
|
|
9
|
+
* files under the repo's 500-line-per-file gate. `owned-lock.ts`'s public
|
|
10
|
+
* `acquireOwnedLock` is a thin wrapper over this file's
|
|
11
|
+
* {@link acquireOwnedLockCore}; the cross-process race-test child harness
|
|
12
|
+
* imports directly from here, by relative path, specifically to reach the
|
|
13
|
+
* two destructive test-only options
|
|
14
|
+
* ({@link AcquireOwnedLockCoreOptions.unsafeSkipReclaimRevalidation} and
|
|
15
|
+
* `.linkSyncOverride`) that must NEVER be reachable via the public
|
|
16
|
+
* `@skillsmith/core/config/owned-lock` subpath.
|
|
17
|
+
*/
|
|
18
|
+
import type { Claim, ReclaimOutcome, StuckLockReason } from './owned-lock.types.js';
|
|
19
|
+
/**
|
|
20
|
+
* Thrown when {@link acquireOwnedLockCore} (and, through it, the public
|
|
21
|
+
* `acquireOwnedLock`) times out. `reason` is a stable discriminant for
|
|
22
|
+
* mechanical triage (never prose-matching); the message embeds the manual
|
|
23
|
+
* unstick procedure verbatim.
|
|
24
|
+
*/
|
|
25
|
+
export declare class StuckLockError extends Error {
|
|
26
|
+
readonly lockPath: string;
|
|
27
|
+
readonly reclaimPath: string;
|
|
28
|
+
readonly reason: StuckLockReason;
|
|
29
|
+
constructor(lockPath: string, reclaimPath: string, label: string, reason: StuckLockReason, claim: Claim);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Full internal option set, including the two options the public
|
|
33
|
+
* `AcquireOwnedLockOptions` (owned-lock.types.ts) deliberately omits.
|
|
34
|
+
*/
|
|
35
|
+
export interface AcquireOwnedLockCoreOptions {
|
|
36
|
+
timeoutMs?: number;
|
|
37
|
+
label?: string;
|
|
38
|
+
reclaimProbeAfterMs?: number;
|
|
39
|
+
onReclaimBoundary?: () => void;
|
|
40
|
+
onReclaimOutcome?: (outcome: ReclaimOutcome) => void;
|
|
41
|
+
/** @internal NEGATIVE CONTROL ONLY (owned-lock-reclaim-race.test.ts §8b). Removes the authoritative re-read that makes this mechanism sound -- reintroduces the round-3 lock-theft race on purpose. Never set outside that spec, and never reachable via the public acquireOwnedLock(). */
|
|
42
|
+
unsafeSkipReclaimRevalidation?: boolean;
|
|
43
|
+
/** @internal test seam (owned-lock.test.ts item 14). Never reachable via the public acquireOwnedLock(). */
|
|
44
|
+
linkSyncOverride?: (existingPath: string, newPath: string) => void;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The full acquire loop. `owned-lock.ts`'s public `acquireOwnedLock` is a
|
|
48
|
+
* thin wrapper over this that only ever forwards the PUBLIC-SAFE option
|
|
49
|
+
* subset -- see the module-level comment above for why the two unsafe
|
|
50
|
+
* options must never be reachable from there.
|
|
51
|
+
*/
|
|
52
|
+
export declare function acquireOwnedLockCore(target: string, opts?: AcquireOwnedLockCoreOptions): () => void;
|
|
53
|
+
//# sourceMappingURL=owned-lock.acquire.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"owned-lock.acquire.d.ts","sourceRoot":"","sources":["../../../src/config/owned-lock.acquire.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAoBH,OAAO,KAAK,EAAE,KAAK,EAAE,cAAc,EAAmB,eAAe,EAAE,MAAM,uBAAuB,CAAA;AA2BpG;;;;;GAKG;AACH,qBAAa,cAAe,SAAQ,KAAK;IACvC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;IAC5B,QAAQ,CAAC,MAAM,EAAE,eAAe,CAAA;gBAG9B,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,MAAM,EACnB,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,eAAe,EACvB,KAAK,EAAE,KAAK;CAcf;AAmBD;;;GAGG;AACH,MAAM,WAAW,2BAA2B;IAC1C,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,mBAAmB,CAAC,EAAE,MAAM,CAAA;IAC5B,iBAAiB,CAAC,EAAE,MAAM,IAAI,CAAA;IAC9B,gBAAgB,CAAC,EAAE,CAAC,OAAO,EAAE,cAAc,KAAK,IAAI,CAAA;IACpD,2RAA2R;IAC3R,6BAA6B,CAAC,EAAE,OAAO,CAAA;IACvC,2GAA2G;IAC3G,gBAAgB,CAAC,EAAE,CAAC,YAAY,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;CACnE;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,MAAM,EACd,IAAI,GAAE,2BAAgC,GACrC,MAAM,IAAI,CA4DZ"}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core acquire loop + `StuckLockError` for the owned-lock primitive.
|
|
3
|
+
* @module @skillsmith/core/config/owned-lock.acquire
|
|
4
|
+
* @see owned-lock.ts for the full soundness argument and the PUBLIC API.
|
|
5
|
+
* @see owned-lock.claim.ts for claim parsing, exclusive creation, and reclaim.
|
|
6
|
+
*
|
|
7
|
+
* INTERNAL module -- not part of the public surface (no `package.json`
|
|
8
|
+
* subpath export), split out of `owned-lock.claim.ts` purely to keep both
|
|
9
|
+
* files under the repo's 500-line-per-file gate. `owned-lock.ts`'s public
|
|
10
|
+
* `acquireOwnedLock` is a thin wrapper over this file's
|
|
11
|
+
* {@link acquireOwnedLockCore}; the cross-process race-test child harness
|
|
12
|
+
* imports directly from here, by relative path, specifically to reach the
|
|
13
|
+
* two destructive test-only options
|
|
14
|
+
* ({@link AcquireOwnedLockCoreOptions.unsafeSkipReclaimRevalidation} and
|
|
15
|
+
* `.linkSyncOverride`) that must NEVER be reachable via the public
|
|
16
|
+
* `@skillsmith/core/config/owned-lock` subpath.
|
|
17
|
+
*/
|
|
18
|
+
import { hostname } from 'node:os';
|
|
19
|
+
import { classifyRefusal, createLockExclusive, isOwnerDefinitelyDead, makeRelease, randomHex, readClaim, sleepSync, tryReclaimUnderLock, } from './owned-lock.claim.js';
|
|
20
|
+
import { LOCK_ACQUIRE_TIMEOUT_MS, LOCK_RETRY_DELAY_MS, RECLAIM_PROBE_AFTER_MS, RECLAIM_PROBE_INTERVAL_MS, } from './owned-lock.types.js';
|
|
21
|
+
function describeReason(reason, claim, reclaimPath) {
|
|
22
|
+
switch (reason) {
|
|
23
|
+
case 'held':
|
|
24
|
+
return claim.kind === 'v1'
|
|
25
|
+
? `held by pid ${claim.pid} on host '${claim.host}' (still alive)`
|
|
26
|
+
: 'held by another process';
|
|
27
|
+
case 'unreclaimable_legacy':
|
|
28
|
+
return ('held by a legacy (pre-v1) claim' +
|
|
29
|
+
(claim.kind === 'legacy' ? ` (pid ${claim.pid})` : '') +
|
|
30
|
+
' -- legacy claims carry no host attribution and are NEVER auto-reclaimed (SMI-5883 D-5)');
|
|
31
|
+
case 'unreclaimable_unparseable':
|
|
32
|
+
return 'the lock file could not be parsed as a recognized claim -- never auto-reclaimed';
|
|
33
|
+
case 'reclaim_unavailable':
|
|
34
|
+
return `the reclaim lock at ${reclaimPath} is held or was orphaned by a crash inside the reclaim critical section (residual R1)`;
|
|
35
|
+
case 'reclaim_disabled':
|
|
36
|
+
return 'auto-reclaim is disabled (SKILLSMITH_LOCK_NO_AUTO_RECLAIM=1)';
|
|
37
|
+
default: {
|
|
38
|
+
const exhaustive = reason;
|
|
39
|
+
return exhaustive;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Thrown when {@link acquireOwnedLockCore} (and, through it, the public
|
|
45
|
+
* `acquireOwnedLock`) times out. `reason` is a stable discriminant for
|
|
46
|
+
* mechanical triage (never prose-matching); the message embeds the manual
|
|
47
|
+
* unstick procedure verbatim.
|
|
48
|
+
*/
|
|
49
|
+
export class StuckLockError extends Error {
|
|
50
|
+
lockPath;
|
|
51
|
+
reclaimPath;
|
|
52
|
+
reason;
|
|
53
|
+
constructor(lockPath, reclaimPath, label, reason, claim) {
|
|
54
|
+
const namesReclaim = reason === 'reclaim_unavailable';
|
|
55
|
+
const message = `[skillsmith] Timed out waiting for ${label} at ${lockPath}: ${describeReason(reason, claim, reclaimPath)}. ` +
|
|
56
|
+
`Manual unstick -- 1) confirm no skillsmith process is running: ps -ax | grep -E '[s]killsmith|[s]klx'; ` +
|
|
57
|
+
`2) inspect (read-only): cat ${lockPath}${namesReclaim ? ` ; cat ${reclaimPath}` : ''}; ` +
|
|
58
|
+
`3) remove ONLY the file(s) named above: rm ${lockPath}${namesReclaim ? ` ; rm ${reclaimPath}` : ''}.`;
|
|
59
|
+
super(message);
|
|
60
|
+
this.name = 'StuckLockError';
|
|
61
|
+
this.lockPath = lockPath;
|
|
62
|
+
this.reclaimPath = reclaimPath;
|
|
63
|
+
this.reason = reason;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function mapRefusalToReason(refusal) {
|
|
67
|
+
switch (refusal) {
|
|
68
|
+
case 'legacy':
|
|
69
|
+
return 'unreclaimable_legacy';
|
|
70
|
+
case 'unparseable':
|
|
71
|
+
return 'unreclaimable_unparseable';
|
|
72
|
+
case 'reclaim-disabled':
|
|
73
|
+
return 'reclaim_disabled';
|
|
74
|
+
case 'unavailable':
|
|
75
|
+
return 'reclaim_unavailable';
|
|
76
|
+
default:
|
|
77
|
+
// 'held' | 'not-stale' | 'gone' | 'reclaimed' (the latter two never
|
|
78
|
+
// reach the caller as a refusal -- they trigger an immediate retry).
|
|
79
|
+
return 'held';
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* The full acquire loop. `owned-lock.ts`'s public `acquireOwnedLock` is a
|
|
84
|
+
* thin wrapper over this that only ever forwards the PUBLIC-SAFE option
|
|
85
|
+
* subset -- see the module-level comment above for why the two unsafe
|
|
86
|
+
* options must never be reachable from there.
|
|
87
|
+
*/
|
|
88
|
+
export function acquireOwnedLockCore(target, opts = {}) {
|
|
89
|
+
const lockPath = `${target}.lock`;
|
|
90
|
+
const reclaimPath = `${lockPath}.reclaim`;
|
|
91
|
+
const token = randomHex(8);
|
|
92
|
+
const label = opts.label ?? 'lock';
|
|
93
|
+
const timeoutMs = opts.timeoutMs ?? LOCK_ACQUIRE_TIMEOUT_MS;
|
|
94
|
+
const started = Date.now();
|
|
95
|
+
const deadline = started + timeoutMs;
|
|
96
|
+
let nextProbeAt = started + (opts.reclaimProbeAfterMs ?? RECLAIM_PROBE_AFTER_MS);
|
|
97
|
+
let lastRefusal = 'held'; // safe default: EEXIST already implies SOMETHING is there
|
|
98
|
+
let lastObservedClaim = { kind: 'absent' };
|
|
99
|
+
for (;;) {
|
|
100
|
+
const record = JSON.stringify({ v: 1, pid: process.pid, token, host: hostname(), acquiredAt: Date.now() }) +
|
|
101
|
+
'\n';
|
|
102
|
+
if (createLockExclusive(lockPath, record, opts.linkSyncOverride)) {
|
|
103
|
+
return makeRelease(lockPath, token);
|
|
104
|
+
}
|
|
105
|
+
// ---- contended ----
|
|
106
|
+
if (Date.now() >= nextProbeAt) {
|
|
107
|
+
const claim = readClaim(lockPath);
|
|
108
|
+
lastObservedClaim = claim;
|
|
109
|
+
if (isOwnerDefinitelyDead(claim)) {
|
|
110
|
+
opts.onReclaimBoundary?.();
|
|
111
|
+
const outcome = tryReclaimUnderLock(lockPath, reclaimPath, {
|
|
112
|
+
unsafeSkipRevalidation: opts.unsafeSkipReclaimRevalidation,
|
|
113
|
+
linkSyncOverride: opts.linkSyncOverride,
|
|
114
|
+
});
|
|
115
|
+
opts.onReclaimOutcome?.(outcome);
|
|
116
|
+
if (outcome === 'reclaimed' || outcome === 'gone') {
|
|
117
|
+
nextProbeAt = 0; // retry create at once -- don't burn the backoff budget
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
lastRefusal = outcome; // 'not-stale' | 'unavailable'
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
lastRefusal = classifyRefusal(claim);
|
|
124
|
+
}
|
|
125
|
+
nextProbeAt = Date.now() + RECLAIM_PROBE_INTERVAL_MS;
|
|
126
|
+
}
|
|
127
|
+
if (Date.now() >= deadline) {
|
|
128
|
+
// A final, read-only claim fetch purely for an accurate message -- does
|
|
129
|
+
// NOT affect the reclaim decision or `lastRefusal`. Needed because a
|
|
130
|
+
// very tight `timeoutMs` (shorter than `reclaimProbeAfterMs`) can
|
|
131
|
+
// otherwise expire before the periodic probe above ever runs once.
|
|
132
|
+
if (lastObservedClaim.kind === 'absent') {
|
|
133
|
+
lastObservedClaim = readClaim(lockPath);
|
|
134
|
+
}
|
|
135
|
+
throw new StuckLockError(lockPath, reclaimPath, label, mapRefusalToReason(lastRefusal), lastObservedClaim);
|
|
136
|
+
}
|
|
137
|
+
sleepSync(LOCK_RETRY_DELAY_MS);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
//# sourceMappingURL=owned-lock.acquire.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"owned-lock.acquire.js","sourceRoot":"","sources":["../../../src/config/owned-lock.acquire.ts"],"names":[],"mappings":"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"}
|