@dtlabs/ai-toolkit 0.13.0 → 0.14.0

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.
@@ -0,0 +1,590 @@
1
+ 'use strict';
2
+
3
+ // Ownership/lease module (FTR-018).
4
+ //
5
+ // Repo-wide execution lease protocol (US-02-TASK-BE-01): acquireLease,
6
+ // releaseLease, readLease. owner.json is the single exclusive-coordinator
7
+ // marker for an execution root (<absolute-common-git-dir>/ai-toolkit/
8
+ // execution/owner.json, per FTR-018-Tech-Spec.md section 5). Unlike
9
+ // runs/<runId>/state.json, this file is not keyed by runId: at most one
10
+ // lease may exist for the whole repo at any time, regardless of which
11
+ // feature/run is trying to acquire it.
12
+ //
13
+ // Stale lock detection via process liveness (checkOwnerLiveness) is
14
+ // US-02-TASK-BE-02 — implemented below. It reuses the E-02 evidence-spike
15
+ // technique verified in
16
+ // internal_docs/features/FTR-018-deterministic-task-execution-checkpoints-and-resume/evidence/E-02-process-supervision/:
17
+ // worker identity is `host:pid:startTime` (never bare pid — a pid can be
18
+ // recycled by the OS for an unrelated process, so start-time is the
19
+ // anti-reuse guard), and Windows liveness is confirmed via a
20
+ // `Get-CimInstance Win32_Process` query matching pid AND exact start time.
21
+ // checkOwnerLiveness only ever reports positive evidence: 'alive: false'
22
+ // requires a confirmed-dead process query result, never age/staleness
23
+ // alone; anything inconclusive (host mismatch, non-Windows, unreadable
24
+ // start time, failed OS query) is reported as 'alive: unknown' and must
25
+ // never be treated as a reclaim signal.
26
+ //
27
+ // The atomic compare-and-swap ownership guard used by actual reclaim
28
+ // (createOwnershipGuard, US-02-TASK-BE-03) is implemented below. It guards a
29
+ // multi-step transition (read lease -> check liveness -> reclaim) with an
30
+ // exclusive `mkdir` on <executionRoot>/ownership-guard (per
31
+ // FTR-018-Tech-Spec.md section 5), the same atomicity spirit as the 'wx'
32
+ // exclusive-create pattern used for the lease file itself, but for a
33
+ // transition rather than a single file write. reclaimLease only ever
34
+ // recovers a lease that checkOwnerLiveness has positively confirmed dead
35
+ // (alive === false) — 'unknown' and true are both refused, per the Gate-1
36
+ // binding constraint carried into this Work Breakdown ("no implicit
37
+ // cross-platform guarantee", "never invent missing values"). Full stale
38
+ // recovery (cross-checking all registered workers per
39
+ // FTR-018-Tech-Spec.md section 5) is out of scope here; that belongs to the
40
+ // worker-registration tasks in US-03.
41
+
42
+ const fs = require('fs');
43
+ const os = require('os');
44
+ const path = require('path');
45
+ const crypto = require('crypto');
46
+ const { spawnSync } = require('child_process');
47
+
48
+ function _err(code, message) {
49
+ const e = new Error(message);
50
+ e.code = code;
51
+ return e;
52
+ }
53
+
54
+ function _requireNonEmptyString(value, label) {
55
+ if (typeof value !== 'string' || value.length === 0) {
56
+ throw _err('LEASE_VALIDATION_ERROR', label + ' must be a non-empty string');
57
+ }
58
+ }
59
+
60
+ function _leaseFilePath(executionRoot) {
61
+ return path.join(executionRoot, 'owner.json');
62
+ }
63
+
64
+ const GUARD_DIR_NAME = 'ownership-guard';
65
+
66
+ function _guardDirPath(executionRoot) {
67
+ return path.join(executionRoot, GUARD_DIR_NAME);
68
+ }
69
+
70
+ function _ensureDirSync(dir) {
71
+ fs.mkdirSync(dir, { recursive: true });
72
+ }
73
+
74
+ function _fsyncDirSync(dir) {
75
+ let dfd;
76
+ try {
77
+ dfd = fs.openSync(dir, 'r');
78
+ fs.fsyncSync(dfd);
79
+ } catch (_) {
80
+ // Directory-handle fsync is not supported on every platform/filesystem
81
+ // (notably Windows). Best-effort only.
82
+ } finally {
83
+ if (dfd !== undefined) {
84
+ try { fs.closeSync(dfd); } catch (_) {}
85
+ }
86
+ }
87
+ }
88
+
89
+ // Exclusive create: fails with EEXIST if the file already exists. This is
90
+ // the atomic primitive that prevents two concurrent acquireLease() calls
91
+ // from both succeeding — unlike store.js's temp-file-then-rename pattern
92
+ // (which makes *replacing* a file atomic), a lock needs the *first create*
93
+ // itself to be exclusive, which is exactly what the 'wx' flag guarantees.
94
+ function _createExclusiveSync(filePath, contents) {
95
+ const fd = fs.openSync(filePath, 'wx');
96
+ try {
97
+ fs.writeSync(fd, contents);
98
+ fs.fsyncSync(fd);
99
+ } finally {
100
+ fs.closeSync(fd);
101
+ }
102
+ }
103
+
104
+ // Same-directory exclusive temp file, complete write, flush, close, rename,
105
+ // directory flush where supported. Mirrors the atomic-write protocol used by
106
+ // store.js's _atomicWriteFileSync — used here to atomically *replace* an
107
+ // existing lease file (reclaim), as opposed to _createExclusiveSync above
108
+ // which atomically creates a lease file that must not already exist.
109
+ function _atomicReplaceFileSync(filePath, contents) {
110
+ const dir = path.dirname(filePath);
111
+ const tmpName = '.' + path.basename(filePath) + '.tmp-' + process.pid + '-' +
112
+ Date.now() + '-' + crypto.randomBytes(4).toString('hex');
113
+ const tmpPath = path.join(dir, tmpName);
114
+
115
+ let fd;
116
+ try {
117
+ fd = fs.openSync(tmpPath, 'w');
118
+ fs.writeSync(fd, contents);
119
+ fs.fsyncSync(fd);
120
+ fs.closeSync(fd);
121
+ fd = undefined;
122
+ } catch (err) {
123
+ if (fd !== undefined) {
124
+ try { fs.closeSync(fd); } catch (_) {}
125
+ }
126
+ try { fs.unlinkSync(tmpPath); } catch (_) {}
127
+ throw err;
128
+ }
129
+
130
+ try {
131
+ fs.renameSync(tmpPath, filePath);
132
+ } catch (err) {
133
+ try { fs.unlinkSync(tmpPath); } catch (_) {}
134
+ throw err;
135
+ }
136
+
137
+ _fsyncDirSync(dir);
138
+ }
139
+
140
+ function _isWindows() {
141
+ return process.platform === 'win32';
142
+ }
143
+
144
+ // Queries the OS for the current creation time of `pid`, using the same
145
+ // technique verified in the E-02 evidence spike (Windows-qualified only —
146
+ // see harness-treekill-dedup.js / harness-registration-window.js):
147
+ // `Get-CimInstance Win32_Process` filtered by ProcessId, reading
148
+ // CreationDate as a file-time-UTC integer so two captures of the same
149
+ // still-running process compare exactly equal.
150
+ //
151
+ // Returns:
152
+ // - { found: true, startTime: '<fileTimeUtc digits>' } — pid exists, start time read
153
+ // - { found: false } — OS confirms no such pid (positive dead evidence)
154
+ // - null — query is not trustworthy (non-Windows, pid not a
155
+ // safe integer, powershell unavailable/failed, or unexpected output) — callers MUST treat this as
156
+ // inconclusive, never as evidence of death.
157
+ function _queryProcessStartTimeSync(pid) {
158
+ if (!_isWindows()) return null;
159
+ if (typeof pid !== 'number' || !Number.isInteger(pid) || pid <= 0) return null;
160
+
161
+ let result;
162
+ try {
163
+ result = spawnSync(
164
+ 'powershell',
165
+ [
166
+ '-NoProfile',
167
+ '-NonInteractive',
168
+ '-Command',
169
+ '$p = Get-CimInstance Win32_Process -Filter "ProcessId=' + pid + '" -ErrorAction SilentlyContinue; ' +
170
+ 'if ($p) { $p.CreationDate.ToFileTimeUtc() } else { "NOTFOUND" }',
171
+ ],
172
+ { encoding: 'utf8', windowsHide: true }
173
+ );
174
+ } catch (_) {
175
+ return null;
176
+ }
177
+
178
+ if (!result || result.error || result.status !== 0 || typeof result.stdout !== 'string') {
179
+ return null;
180
+ }
181
+
182
+ const out = result.stdout.trim();
183
+ if (out === 'NOTFOUND') return { found: false };
184
+ if (/^-?\d+$/.test(out)) return { found: true, startTime: out };
185
+ return null; // unexpected output shape — cannot be trusted either way
186
+ }
187
+
188
+ function _readLeaseFileSync(filePath) {
189
+ let raw;
190
+ try {
191
+ raw = fs.readFileSync(filePath, 'utf8');
192
+ } catch (err) {
193
+ if (err.code === 'ENOENT') return null;
194
+ throw err;
195
+ }
196
+ try {
197
+ return JSON.parse(raw);
198
+ } catch (err) {
199
+ throw _err('LEASE_CORRUPTED', 'lease file at ' + filePath + ' is corrupted: ' + err.message);
200
+ }
201
+ }
202
+
203
+ /**
204
+ * Acquires the single repo-wide execution lease for `executionRoot`.
205
+ *
206
+ * Fails closed with LEASE_HELD if a lease already exists — this task does
207
+ * not implement stale-lease recovery (see US-02-TASK-BE-02/03); a second
208
+ * coordinator must stop with a structured diagnosis rather than duplicate
209
+ * work (AC-08).
210
+ *
211
+ * @param {string} executionRoot - Absolute path to
212
+ * <common-git-dir>/ai-toolkit/execution.
213
+ * @param {string} runId - UUID of the run requesting ownership.
214
+ * @param {object} [opts]
215
+ * @param {string} [opts.host] - Override for host identity (defaults to
216
+ * os.hostname()); exposed for tests, real callers should omit it.
217
+ * @param {number} [opts.pid] - Override for process identity (defaults to
218
+ * process.pid); exposed for tests, real callers should omit it.
219
+ * @returns {{nonce:string, host:string, pid:number, runId:string,
220
+ * generation:number, acquiredAt:string, startTime:?string}}
221
+ *
222
+ * NOTE (US-02-TASK-BE-02 schema extension): `startTime` is captured here so
223
+ * checkOwnerLiveness can later confirm this exact process (not a
224
+ * pid-recycled impostor) is still the lease holder — see the E-02 evidence
225
+ * spike technique referenced at the top of this file. It is a Windows
226
+ * file-time-UTC string on Windows, or `null` when start time could not be
227
+ * captured (non-Windows platform, or the OS query failed) — a `null`
228
+ * startTime makes checkOwnerLiveness return 'unknown' rather than 'alive',
229
+ * never 'dead'. This field is new and pre-implementation-only (no released
230
+ * lease files exist yet to be backward-compatible with).
231
+ */
232
+ function acquireLease(executionRoot, runId, opts) {
233
+ _requireNonEmptyString(executionRoot, 'acquireLease: executionRoot');
234
+ _requireNonEmptyString(runId, 'acquireLease: runId');
235
+ opts = opts || {};
236
+
237
+ _ensureDirSync(executionRoot);
238
+ const filePath = _leaseFilePath(executionRoot);
239
+
240
+ const leasePid = opts.pid != null ? opts.pid : process.pid;
241
+ const startTimeProbe = _queryProcessStartTimeSync(leasePid);
242
+
243
+ const lease = {
244
+ nonce: crypto.randomBytes(16).toString('hex'),
245
+ host: opts.host || os.hostname(),
246
+ pid: leasePid,
247
+ runId: runId,
248
+ generation: 0,
249
+ acquiredAt: new Date().toISOString(),
250
+ startTime: startTimeProbe && startTimeProbe.found ? startTimeProbe.startTime : null,
251
+ };
252
+
253
+ try {
254
+ _createExclusiveSync(filePath, JSON.stringify(lease, null, 2));
255
+ } catch (err) {
256
+ if (err.code === 'EEXIST') {
257
+ const existing = _readLeaseFileSync(filePath);
258
+ const e = _err(
259
+ 'LEASE_HELD',
260
+ 'acquireLease: a repo-wide execution lease is already held' +
261
+ (existing
262
+ ? ' by runId "' + existing.runId + '" (host ' + existing.host + ', pid ' + existing.pid + ')'
263
+ : ' but the existing lease file could not be read')
264
+ );
265
+ e.existingLease = existing;
266
+ throw e;
267
+ }
268
+ throw err;
269
+ }
270
+
271
+ _fsyncDirSync(executionRoot);
272
+
273
+ const readBack = _readLeaseFileSync(filePath);
274
+ if (!readBack || readBack.nonce !== lease.nonce) {
275
+ throw _err(
276
+ 'LEASE_READBACK_MISMATCH',
277
+ 'acquireLease: readback verification failed for runId "' + runId + '"'
278
+ );
279
+ }
280
+ return readBack;
281
+ }
282
+
283
+ /**
284
+ * Releases the repo-wide execution lease, but only if the caller's identity
285
+ * (runId + nonce, both as returned by acquireLease) matches the lease
286
+ * currently on disk. Never removes a lease belonging to a different holder
287
+ * — normal entrants must not unlink another owner's lease.
288
+ *
289
+ * @param {string} executionRoot
290
+ * @param {string} runId
291
+ * @param {string} nonce
292
+ * @returns {true}
293
+ */
294
+ function releaseLease(executionRoot, runId, nonce) {
295
+ _requireNonEmptyString(executionRoot, 'releaseLease: executionRoot');
296
+ _requireNonEmptyString(runId, 'releaseLease: runId');
297
+ _requireNonEmptyString(nonce, 'releaseLease: nonce');
298
+
299
+ const filePath = _leaseFilePath(executionRoot);
300
+ const existing = _readLeaseFileSync(filePath);
301
+
302
+ if (!existing) {
303
+ throw _err('LEASE_NOT_FOUND', 'releaseLease: no lease found at ' + filePath);
304
+ }
305
+ if (existing.runId !== runId || existing.nonce !== nonce) {
306
+ throw _err(
307
+ 'LEASE_NOT_OWNER',
308
+ 'releaseLease: caller (runId "' + runId + '") does not hold the current lease ' +
309
+ '(held by runId "' + existing.runId + '")'
310
+ );
311
+ }
312
+
313
+ fs.unlinkSync(filePath);
314
+ _fsyncDirSync(executionRoot);
315
+ return true;
316
+ }
317
+
318
+ /**
319
+ * Read-only accessor: returns the current lease contents, or null if no
320
+ * lease is held. Never mutates state.
321
+ *
322
+ * @param {string} executionRoot
323
+ * @returns {?object}
324
+ */
325
+ function readLease(executionRoot) {
326
+ _requireNonEmptyString(executionRoot, 'readLease: executionRoot');
327
+ return _readLeaseFileSync(_leaseFilePath(executionRoot));
328
+ }
329
+
330
+ /**
331
+ * Creates an ownership guard bound to `executionRoot`: a handle providing an
332
+ * exclusive, atomic "transition" primitive over the SAME executionRoot the
333
+ * lease lives in (<executionRoot>/ownership-guard, per
334
+ * FTR-018-Tech-Spec.md section 5). Any multi-step ownership transition
335
+ * (read lease -> check liveness -> reclaim) must run inside guard.transition()
336
+ * so no two transitions can interleave.
337
+ *
338
+ * The exclusivity primitive is `fs.mkdirSync` with no `{recursive: true}` on
339
+ * the guard directory: mkdir on an already-existing path throws EEXIST
340
+ * atomically (same spirit as this module's 'wx' exclusive-create pattern for
341
+ * the lease file, but guarding a transition rather than a single write).
342
+ *
343
+ * @param {string} executionRoot
344
+ * @returns {{transition: function(function(): *): *}}
345
+ */
346
+ function createOwnershipGuard(executionRoot) {
347
+ _requireNonEmptyString(executionRoot, 'createOwnershipGuard: executionRoot');
348
+ const guardDir = _guardDirPath(executionRoot);
349
+
350
+ return {
351
+ /**
352
+ * Runs `fn` under the guard's exclusive lock. Fails closed with
353
+ * GUARD_HELD (without calling `fn`) if another transition already holds
354
+ * the guard — this failure is never swallowed. The guard directory is
355
+ * always released (best-effort rmdir) in a `finally`, including when
356
+ * `fn` throws, so a crash mid-transition never leaves the lock behind
357
+ * forever; the rmdir itself is wrapped so a cleanup failure can never
358
+ * mask fn's own result/error.
359
+ *
360
+ * @param {function(): *} fn
361
+ * @returns {*} fn's return value
362
+ */
363
+ transition(fn) {
364
+ _ensureDirSync(executionRoot);
365
+ try {
366
+ fs.mkdirSync(guardDir);
367
+ } catch (err) {
368
+ if (err.code === 'EEXIST') {
369
+ throw _err(
370
+ 'GUARD_HELD',
371
+ 'createOwnershipGuard: another ownership transition is already in progress at ' + guardDir
372
+ );
373
+ }
374
+ throw err;
375
+ }
376
+
377
+ try {
378
+ return fn();
379
+ } finally {
380
+ try {
381
+ fs.rmdirSync(guardDir);
382
+ } catch (_) {
383
+ // Best-effort cleanup only, mirroring _fsyncDirSync's discipline
384
+ // elsewhere in this file: a cleanup failure here must never mask
385
+ // fn's own return value or thrown error.
386
+ }
387
+ }
388
+ },
389
+ };
390
+ }
391
+
392
+ /**
393
+ * Reclaims the repo-wide execution lease for `newRunId`, but only when the
394
+ * current holder (if any) is positively confirmed dead by
395
+ * checkOwnerLiveness. The whole read -> check -> write sequence runs inside
396
+ * a single createOwnershipGuard(executionRoot).transition() call so no other
397
+ * reclaim/acquire/release attempt can interleave with it.
398
+ *
399
+ * Behavior:
400
+ * 1. No lease currently held: this degenerates into a normal first
401
+ * acquisition — delegates to acquireLease(executionRoot, newRunId,
402
+ * opts), still performed inside the guard's exclusive lock.
403
+ * 2. A lease exists: checkOwnerLiveness(existingLease) is consulted.
404
+ * - alive === false (confirmed dead): proceed to reclaim (step 3).
405
+ * - alive === true: throw LEASE_STILL_LIVE — never reclaim a live
406
+ * owner.
407
+ * - alive === 'unknown': throw LEASE_LIVENESS_UNKNOWN — never reclaim
408
+ * on an inconclusive check (host mismatch, non-Windows, missing
409
+ * startTime, failed OS query); age alone is never sufficient
410
+ * evidence.
411
+ * 3. Confirmed dead: atomically replaces the lease file with a new lease
412
+ * for `newRunId`. generation is the old lease's generation + 1 (0 if
413
+ * the existing lease had no valid non-negative-integer generation —
414
+ * the simplest rule that still always increases monotonically from
415
+ * whatever was last recorded).
416
+ * 4. Reads the new lease back and verifies its nonce matches what was
417
+ * just written (same discipline as acquireLease), throwing
418
+ * LEASE_READBACK_MISMATCH if it does not.
419
+ *
420
+ * @param {string} executionRoot
421
+ * @param {string} newRunId - UUID of the run requesting reclaim.
422
+ * @param {object} [opts]
423
+ * @param {string} [opts.host] - Override for host identity (tests only).
424
+ * @param {number} [opts.pid] - Override for process identity (tests only).
425
+ * @returns {object} the new lease, as read back from disk.
426
+ */
427
+ function reclaimLease(executionRoot, newRunId, opts) {
428
+ _requireNonEmptyString(executionRoot, 'reclaimLease: executionRoot');
429
+ _requireNonEmptyString(newRunId, 'reclaimLease: newRunId');
430
+ opts = opts || {};
431
+
432
+ const guard = createOwnershipGuard(executionRoot);
433
+
434
+ return guard.transition(function () {
435
+ const filePath = _leaseFilePath(executionRoot);
436
+ const existing = _readLeaseFileSync(filePath);
437
+
438
+ if (!existing) {
439
+ return acquireLease(executionRoot, newRunId, opts);
440
+ }
441
+
442
+ const liveness = checkOwnerLiveness(existing, opts);
443
+ if (liveness.alive !== false) {
444
+ const code = liveness.alive === true ? 'LEASE_STILL_LIVE' : 'LEASE_LIVENESS_UNKNOWN';
445
+ throw _err(
446
+ code,
447
+ 'reclaimLease: refusing to reclaim lease held by runId "' + existing.runId + '" ' +
448
+ '(host ' + existing.host + ', pid ' + existing.pid + ') — ' + liveness.reason
449
+ );
450
+ }
451
+
452
+ const nextGeneration = Number.isInteger(existing.generation) && existing.generation >= 0
453
+ ? existing.generation + 1
454
+ : 0;
455
+
456
+ const newPid = opts.pid != null ? opts.pid : process.pid;
457
+ const startTimeProbe = _queryProcessStartTimeSync(newPid);
458
+
459
+ const newLease = {
460
+ nonce: crypto.randomBytes(16).toString('hex'),
461
+ host: opts.host || os.hostname(),
462
+ pid: newPid,
463
+ runId: newRunId,
464
+ generation: nextGeneration,
465
+ acquiredAt: new Date().toISOString(),
466
+ startTime: startTimeProbe && startTimeProbe.found ? startTimeProbe.startTime : null,
467
+ };
468
+
469
+ _atomicReplaceFileSync(filePath, JSON.stringify(newLease, null, 2));
470
+ _fsyncDirSync(executionRoot);
471
+
472
+ const readBack = _readLeaseFileSync(filePath);
473
+ if (!readBack || readBack.nonce !== newLease.nonce) {
474
+ throw _err(
475
+ 'LEASE_READBACK_MISMATCH',
476
+ 'reclaimLease: readback verification failed for runId "' + newRunId + '"'
477
+ );
478
+ }
479
+ return readBack;
480
+ });
481
+ }
482
+
483
+ /**
484
+ * Determines whether the process that recorded `lease` (as returned by
485
+ * acquireLease/readLease) is still the same live process, using the
486
+ * host:pid:startTime identity technique proven in the E-02 evidence spike
487
+ * (see the file-level comment above and
488
+ * internal_docs/features/FTR-018-deterministic-task-execution-checkpoints-and-resume/evidence/E-02-process-supervision/).
489
+ *
490
+ * This function NEVER infers death from age/staleness — only from a
491
+ * positive OS process-query result. Any condition that prevents a
492
+ * conclusive check (remote host, unsupported platform, missing startTime,
493
+ * a failed/untrusted OS query) yields `alive: 'unknown'`, which callers
494
+ * (reclaimLease, US-02-TASK-BE-03) must treat as "do not recover" — the
495
+ * same as `alive: true`. Only `alive: false` may ever justify recovery.
496
+ *
497
+ * Windows-only real check (per the Gate-1 E-02 qualification): on any
498
+ * other `process.platform`, this always returns `alive: 'unknown'` with an
499
+ * explanatory reason instead of attempting an unproven POSIX check.
500
+ *
501
+ * @param {{host:string, pid:number, startTime:?string}} lease - Lease
502
+ * object as returned by acquireLease/readLease.
503
+ * @param {object} [opts]
504
+ * @param {string} [opts.host] - Override for the current host identity
505
+ * (defaults to os.hostname()); exposed for tests.
506
+ * @returns {{alive: true|false|'unknown', reason: string}}
507
+ */
508
+ function checkOwnerLiveness(lease, opts) {
509
+ opts = opts || {};
510
+
511
+ if (!lease || typeof lease !== 'object') {
512
+ throw _err('LEASE_VALIDATION_ERROR', 'checkOwnerLiveness: lease must be a non-empty object');
513
+ }
514
+
515
+ const currentHost = opts.host || os.hostname();
516
+
517
+ if (lease.host !== currentHost) {
518
+ return {
519
+ alive: 'unknown',
520
+ reason:
521
+ 'lease host "' + lease.host + '" does not match current host "' + currentHost +
522
+ '"; a remote host\'s processes cannot be queried locally',
523
+ };
524
+ }
525
+
526
+ if (!_isWindows()) {
527
+ return {
528
+ alive: 'unknown',
529
+ reason:
530
+ 'process liveness checking is only qualified on Windows (E-02 evidence spike); ' +
531
+ 'this platform ("' + process.platform + '") has no qualified liveness mechanism yet',
532
+ };
533
+ }
534
+
535
+ if (typeof lease.pid !== 'number' || !Number.isInteger(lease.pid) || lease.pid <= 0) {
536
+ return { alive: 'unknown', reason: 'lease.pid is missing or not a valid process id; cannot query process identity' };
537
+ }
538
+
539
+ if (typeof lease.startTime !== 'string' || lease.startTime.length === 0) {
540
+ return {
541
+ alive: 'unknown',
542
+ reason:
543
+ 'lease has no recorded startTime (captured before this identity check existed, or capture ' +
544
+ 'failed at acquireLease time); a bare pid is not a safe identity check because pids can be ' +
545
+ 'recycled by the OS, so liveness cannot be conclusively determined',
546
+ };
547
+ }
548
+
549
+ const probe = _queryProcessStartTimeSync(lease.pid);
550
+
551
+ if (probe === null) {
552
+ return {
553
+ alive: 'unknown',
554
+ reason: 'the OS process query failed or returned an untrusted result; liveness cannot be conclusively determined',
555
+ };
556
+ }
557
+
558
+ if (probe.found === false) {
559
+ return {
560
+ alive: false,
561
+ reason: 'no process with pid ' + lease.pid + ' exists on host "' + currentHost + '" (confirmed via OS process query)',
562
+ };
563
+ }
564
+
565
+ if (probe.startTime === lease.startTime) {
566
+ return {
567
+ alive: true,
568
+ reason:
569
+ 'pid ' + lease.pid + ' is running on host "' + currentHost + '" with a start time matching the ' +
570
+ 'recorded lease (host:pid:startTime identity confirmed)',
571
+ };
572
+ }
573
+
574
+ return {
575
+ alive: false,
576
+ reason:
577
+ 'pid ' + lease.pid + ' exists but its current start time (' + probe.startTime + ') differs from the ' +
578
+ 'recorded lease start time (' + lease.startTime + '); this is a different process reusing a recycled ' +
579
+ 'pid, confirmed via OS process query',
580
+ };
581
+ }
582
+
583
+ module.exports = {
584
+ acquireLease,
585
+ releaseLease,
586
+ readLease,
587
+ reclaimLease,
588
+ checkOwnerLiveness,
589
+ createOwnershipGuard,
590
+ };