@akagilnc/pi-workflow-roles 0.1.2503 → 0.1.2512
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/dist/public-cli/main.js +110 -36
- package/package.json +1 -1
- package/souls/quality-law.md +1 -1
- package/src/public-cli/run-lifecycle.ts +182 -46
package/dist/public-cli/main.js
CHANGED
|
@@ -16031,55 +16031,128 @@ function describeErrorIdentity(error) {
|
|
|
16031
16031
|
const message = typeof candidate?.message === "string" && candidate.message !== "" ? `: ${candidate.message}` : "";
|
|
16032
16032
|
return `${name}${code}${message}`;
|
|
16033
16033
|
}
|
|
16034
|
+
function errorCodeOf(error) {
|
|
16035
|
+
return error.code;
|
|
16036
|
+
}
|
|
16037
|
+
function isProcessAlive(pid) {
|
|
16038
|
+
try {
|
|
16039
|
+
process.kill(pid, 0);
|
|
16040
|
+
return true;
|
|
16041
|
+
} catch (error) {
|
|
16042
|
+
return errorCodeOf(error) !== "ESRCH";
|
|
16043
|
+
}
|
|
16044
|
+
}
|
|
16045
|
+
async function autopsyWriterLock(lockPath) {
|
|
16046
|
+
let content;
|
|
16047
|
+
try {
|
|
16048
|
+
content = await readFile5(lockPath, "utf8");
|
|
16049
|
+
} catch (error) {
|
|
16050
|
+
if (errorCodeOf(error) === "ENOENT") return { verdict: "absent" };
|
|
16051
|
+
return { verdict: "absent", readFailure: error };
|
|
16052
|
+
}
|
|
16053
|
+
const pid = Number.parseInt(content.trim(), 10);
|
|
16054
|
+
if (!Number.isInteger(pid) || pid <= 0) return { verdict: "absent" };
|
|
16055
|
+
return isProcessAlive(pid) ? { verdict: "alive", pid } : { verdict: "dead", pid };
|
|
16056
|
+
}
|
|
16057
|
+
function describeAutopsy(autopsy) {
|
|
16058
|
+
switch (autopsy.verdict) {
|
|
16059
|
+
case "alive":
|
|
16060
|
+
return `live pid ${autopsy.pid}`;
|
|
16061
|
+
case "dead":
|
|
16062
|
+
return `dead pid ${autopsy.pid}`;
|
|
16063
|
+
case "absent":
|
|
16064
|
+
return autopsy.readFailure !== void 0 ? "unreadable lock" : "absent or unparseable holder";
|
|
16065
|
+
}
|
|
16066
|
+
}
|
|
16067
|
+
async function reclaimStaleWriterLock(lockPath, runDirectory) {
|
|
16068
|
+
const current = await autopsyWriterLock(lockPath);
|
|
16069
|
+
if (current.verdict !== "dead") return;
|
|
16070
|
+
try {
|
|
16071
|
+
await unlink2(lockPath);
|
|
16072
|
+
} catch (error) {
|
|
16073
|
+
if (errorCodeOf(error) === "ENOENT") return;
|
|
16074
|
+
if (errorCodeOf(error) !== "EACCES") throw error;
|
|
16075
|
+
await chmod(runDirectory, 493);
|
|
16076
|
+
await unlink2(lockPath);
|
|
16077
|
+
}
|
|
16078
|
+
}
|
|
16079
|
+
async function createWriterLease(lockPath, runDirectory, reportCleanupFailure) {
|
|
16080
|
+
const handle = await open(lockPath, "wx");
|
|
16081
|
+
try {
|
|
16082
|
+
await handle.writeFile(`${process.pid}
|
|
16083
|
+
`, "utf8");
|
|
16084
|
+
} catch (error) {
|
|
16085
|
+
await handle.close().catch(() => void 0);
|
|
16086
|
+
await unlink2(lockPath).catch(() => void 0);
|
|
16087
|
+
throw error;
|
|
16088
|
+
}
|
|
16089
|
+
let released = false;
|
|
16090
|
+
return {
|
|
16091
|
+
lockPath,
|
|
16092
|
+
async release() {
|
|
16093
|
+
if (released) return;
|
|
16094
|
+
released = true;
|
|
16095
|
+
await handle.close().catch(() => void 0);
|
|
16096
|
+
try {
|
|
16097
|
+
await unlink2(lockPath);
|
|
16098
|
+
} catch (error) {
|
|
16099
|
+
if (errorCodeOf(error) === "EACCES") {
|
|
16100
|
+
try {
|
|
16101
|
+
await chmod(runDirectory, 493);
|
|
16102
|
+
await unlink2(lockPath);
|
|
16103
|
+
} catch (retryError) {
|
|
16104
|
+
reportCleanupFailure(retryError);
|
|
16105
|
+
}
|
|
16106
|
+
} else {
|
|
16107
|
+
reportCleanupFailure(error);
|
|
16108
|
+
}
|
|
16109
|
+
}
|
|
16110
|
+
}
|
|
16111
|
+
};
|
|
16112
|
+
}
|
|
16034
16113
|
async function acquireRunWriterLease(runDirectory, onCleanupFailure) {
|
|
16035
16114
|
const reportCleanupFailure = (error) => {
|
|
16036
16115
|
try {
|
|
16037
16116
|
onCleanupFailure?.(
|
|
16038
|
-
`writer lease lock cleanup failed (best-effort continue; stale lock
|
|
16117
|
+
`writer lease lock cleanup failed (best-effort continue; stale lock is reclaimed by the next acquire's holder autopsy) at ${join7(runDirectory, WRITER_LOCK_FILE)}: ${describeErrorIdentity(error)}`
|
|
16039
16118
|
);
|
|
16040
16119
|
} catch {
|
|
16041
16120
|
}
|
|
16042
16121
|
};
|
|
16043
16122
|
const lockPath = join7(runDirectory, WRITER_LOCK_FILE);
|
|
16044
|
-
|
|
16045
|
-
|
|
16123
|
+
let lastAutopsy = { verdict: "absent" };
|
|
16124
|
+
for (let reclaimsLeft = WRITER_LEASE_RECLAIM_ROUNDS; ; reclaimsLeft -= 1) {
|
|
16046
16125
|
try {
|
|
16047
|
-
await
|
|
16048
|
-
`, "utf8");
|
|
16126
|
+
return await createWriterLease(lockPath, runDirectory, reportCleanupFailure);
|
|
16049
16127
|
} catch (error) {
|
|
16050
|
-
|
|
16051
|
-
await unlink2(lockPath).catch(() => void 0);
|
|
16052
|
-
throw error;
|
|
16128
|
+
if (errorCodeOf(error) !== "EEXIST") throw error;
|
|
16053
16129
|
}
|
|
16054
|
-
|
|
16055
|
-
|
|
16056
|
-
|
|
16057
|
-
|
|
16058
|
-
|
|
16059
|
-
|
|
16060
|
-
|
|
16061
|
-
|
|
16062
|
-
|
|
16063
|
-
|
|
16064
|
-
|
|
16065
|
-
|
|
16066
|
-
|
|
16067
|
-
|
|
16068
|
-
|
|
16069
|
-
|
|
16070
|
-
|
|
16071
|
-
|
|
16072
|
-
|
|
16073
|
-
|
|
16074
|
-
|
|
16075
|
-
}
|
|
16076
|
-
};
|
|
16077
|
-
} catch (error) {
|
|
16078
|
-
if (error instanceof Error && "code" in error && error.code === "EEXIST") {
|
|
16079
|
-
throw new RunWriterLeaseHeldError();
|
|
16130
|
+
lastAutopsy = await autopsyWriterLock(lockPath);
|
|
16131
|
+
if (lastAutopsy.verdict === "absent" && lastAutopsy.readFailure !== void 0) {
|
|
16132
|
+
reportCleanupFailure(lastAutopsy.readFailure);
|
|
16133
|
+
}
|
|
16134
|
+
if (lastAutopsy.verdict === "alive") {
|
|
16135
|
+
throw new RunWriterLeaseHeldError(
|
|
16136
|
+
`role run writer lease is already held by live pid ${lastAutopsy.pid} at ${lockPath}`
|
|
16137
|
+
);
|
|
16138
|
+
}
|
|
16139
|
+
if (lastAutopsy.verdict === "absent") {
|
|
16140
|
+
throw new RunWriterLeaseHeldError(
|
|
16141
|
+
lastAutopsy.readFailure !== void 0 ? `role run writer lease lock is unreadable at ${lockPath}: ${describeErrorIdentity(lastAutopsy.readFailure)}; holder liveness unverifiable, lock left in place` : `role run writer lease lock at ${lockPath} has no verifiable holder pid (empty or unparseable); holder liveness unverifiable, lock left in place`
|
|
16142
|
+
);
|
|
16143
|
+
}
|
|
16144
|
+
if (reclaimsLeft <= 0) break;
|
|
16145
|
+
try {
|
|
16146
|
+
await reclaimStaleWriterLock(lockPath, runDirectory);
|
|
16147
|
+
} catch (reclaimError) {
|
|
16148
|
+
throw new RunWriterLeaseHeldError(
|
|
16149
|
+
`stale writer lease reclaim failed at ${lockPath} (autopsy: ${describeAutopsy(lastAutopsy)}): ${describeErrorIdentity(reclaimError)}`
|
|
16150
|
+
);
|
|
16080
16151
|
}
|
|
16081
|
-
throw error;
|
|
16082
16152
|
}
|
|
16153
|
+
throw new RunWriterLeaseHeldError(
|
|
16154
|
+
`role run writer lease stayed contested at ${lockPath} after ${WRITER_LEASE_RECLAIM_ROUNDS} reclaims (last autopsy: ${describeAutopsy(lastAutopsy)})`
|
|
16155
|
+
);
|
|
16083
16156
|
}
|
|
16084
16157
|
async function findRunDirectoryById(home, runId) {
|
|
16085
16158
|
if (runId.trim() === "") return void 0;
|
|
@@ -16471,7 +16544,7 @@ async function peekRoleRunRole(home, runId) {
|
|
|
16471
16544
|
const run = await readRoleRunState(runDirectory);
|
|
16472
16545
|
return run?.role;
|
|
16473
16546
|
}
|
|
16474
|
-
var V1_RESUMABLE_PROVIDERS, AUTO_RESUME_LIMIT, RESUME_TRANSPORT_ENVELOPE, RUN_STATE_FILE, WRITER_LOCK_FILE, RunWriterLeaseHeldError;
|
|
16547
|
+
var V1_RESUMABLE_PROVIDERS, AUTO_RESUME_LIMIT, RESUME_TRANSPORT_ENVELOPE, RUN_STATE_FILE, WRITER_LOCK_FILE, RunWriterLeaseHeldError, WRITER_LEASE_RECLAIM_ROUNDS;
|
|
16475
16548
|
var init_run_lifecycle = __esm({
|
|
16476
16549
|
"src/public-cli/run-lifecycle.ts"() {
|
|
16477
16550
|
"use strict";
|
|
@@ -16492,6 +16565,7 @@ var init_run_lifecycle = __esm({
|
|
|
16492
16565
|
this.name = "RunWriterLeaseHeldError";
|
|
16493
16566
|
}
|
|
16494
16567
|
};
|
|
16568
|
+
WRITER_LEASE_RECLAIM_ROUNDS = 3;
|
|
16495
16569
|
}
|
|
16496
16570
|
});
|
|
16497
16571
|
|
package/package.json
CHANGED
package/souls/quality-law.md
CHANGED
|
@@ -382,14 +382,151 @@ export function describeErrorIdentity(error: unknown): string {
|
|
|
382
382
|
return `${name}${code}${message}`;
|
|
383
383
|
}
|
|
384
384
|
|
|
385
|
+
function errorCodeOf(error: unknown): unknown {
|
|
386
|
+
return (error as { code?: unknown }).code;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Signal-0 liveness probe. Only ESRCH proves absence; any other refusal
|
|
391
|
+
* (e.g. EPERM) means the holder process exists.
|
|
392
|
+
*/
|
|
393
|
+
function isProcessAlive(pid: number): boolean {
|
|
394
|
+
try {
|
|
395
|
+
process.kill(pid, 0);
|
|
396
|
+
return true;
|
|
397
|
+
} catch (error) {
|
|
398
|
+
return errorCodeOf(error) !== "ESRCH";
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
type WriterLockAutopsy =
|
|
403
|
+
| { verdict: "absent"; readFailure?: unknown }
|
|
404
|
+
| { verdict: "dead"; pid: number }
|
|
405
|
+
| { verdict: "alive"; pid: number };
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Holder autopsy for an existing writer.lock (#552). "absent" covers no file,
|
|
409
|
+
* no parseable pid (a live creator mid-acquisition reads as empty, and so does
|
|
410
|
+
* the crash-window leftover), and unreadable files — absent alone never
|
|
411
|
+
* authorizes reclaim; only a "dead" verdict does. A non-ENOENT read failure
|
|
412
|
+
* still decides "absent" but rides along as readFailure so the true cause can
|
|
413
|
+
* land in the cleanup sink instead of being laundered away.
|
|
414
|
+
*/
|
|
415
|
+
async function autopsyWriterLock(lockPath: string): Promise<WriterLockAutopsy> {
|
|
416
|
+
let content: string;
|
|
417
|
+
try {
|
|
418
|
+
content = await readFile(lockPath, "utf8");
|
|
419
|
+
} catch (error) {
|
|
420
|
+
if (errorCodeOf(error) === "ENOENT") return { verdict: "absent" };
|
|
421
|
+
return { verdict: "absent", readFailure: error };
|
|
422
|
+
}
|
|
423
|
+
const pid = Number.parseInt(content.trim(), 10);
|
|
424
|
+
if (!Number.isInteger(pid) || pid <= 0) return { verdict: "absent" };
|
|
425
|
+
return isProcessAlive(pid) ? { verdict: "alive", pid } : { verdict: "dead", pid };
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function describeAutopsy(autopsy: WriterLockAutopsy): string {
|
|
429
|
+
switch (autopsy.verdict) {
|
|
430
|
+
case "alive":
|
|
431
|
+
return `live pid ${autopsy.pid}`;
|
|
432
|
+
case "dead":
|
|
433
|
+
return `dead pid ${autopsy.pid}`;
|
|
434
|
+
case "absent":
|
|
435
|
+
return autopsy.readFailure !== undefined
|
|
436
|
+
? "unreadable lock"
|
|
437
|
+
: "absent or unparseable holder";
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
385
441
|
/**
|
|
386
|
-
*
|
|
387
|
-
*
|
|
442
|
+
* Remove one lock whose re-read autopsy is still a verified-dead holder, or
|
|
443
|
+
* leave it for the next round otherwise. The pre-unlink re-read guard means a
|
|
444
|
+
* concurrent writer that re-locked between the autopsy and the unlink cannot
|
|
445
|
+
* have its live lock stolen. Residual race: a writer can still re-lock between
|
|
446
|
+
* the re-read and the unlink itself; POSIX offers no compare-and-delete, and
|
|
447
|
+
* this narrows the window to a single syscall pair.
|
|
448
|
+
*/
|
|
449
|
+
async function reclaimStaleWriterLock(lockPath: string, runDirectory: string): Promise<void> {
|
|
450
|
+
const current = await autopsyWriterLock(lockPath);
|
|
451
|
+
if (current.verdict !== "dead") return;
|
|
452
|
+
try {
|
|
453
|
+
await unlink(lockPath);
|
|
454
|
+
} catch (error) {
|
|
455
|
+
if (errorCodeOf(error) === "ENOENT") return;
|
|
456
|
+
if (errorCodeOf(error) !== "EACCES") throw error;
|
|
457
|
+
await chmod(runDirectory, 0o755);
|
|
458
|
+
await unlink(lockPath);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
async function createWriterLease(
|
|
463
|
+
lockPath: string,
|
|
464
|
+
runDirectory: string,
|
|
465
|
+
reportCleanupFailure: (error: unknown) => void,
|
|
466
|
+
): Promise<RunWriterLease> {
|
|
467
|
+
const handle = await open(lockPath, "wx");
|
|
468
|
+
try {
|
|
469
|
+
await handle.writeFile(`${process.pid}\n`, "utf8");
|
|
470
|
+
} catch (error) {
|
|
471
|
+
await handle.close().catch(() => undefined);
|
|
472
|
+
await unlink(lockPath).catch(() => undefined);
|
|
473
|
+
throw error;
|
|
474
|
+
}
|
|
475
|
+
let released = false;
|
|
476
|
+
return {
|
|
477
|
+
lockPath,
|
|
478
|
+
async release() {
|
|
479
|
+
if (released) return;
|
|
480
|
+
released = true;
|
|
481
|
+
await handle.close().catch(() => undefined);
|
|
482
|
+
try {
|
|
483
|
+
await unlink(lockPath);
|
|
484
|
+
} catch (error) {
|
|
485
|
+
if (errorCodeOf(error) === "EACCES") {
|
|
486
|
+
try {
|
|
487
|
+
await chmod(runDirectory, 0o755);
|
|
488
|
+
await unlink(lockPath);
|
|
489
|
+
} catch (retryError) {
|
|
490
|
+
// best-effort cleanup: a stale lock left here is reclaimed by the
|
|
491
|
+
// next acquire's holder autopsy, but the true chmod/unlink cause
|
|
492
|
+
// must be recorded, not swallowed.
|
|
493
|
+
reportCleanupFailure(retryError);
|
|
494
|
+
}
|
|
495
|
+
} else {
|
|
496
|
+
// non-EACCES unlink failure is best-effort settlement cleanup; record true cause.
|
|
497
|
+
reportCleanupFailure(error);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
},
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
const WRITER_LEASE_RECLAIM_ROUNDS = 3;
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* Acquire the one-writer lease for a Role run. Exclusive create — no second
|
|
508
|
+
* writer; a concurrent acquire rejects without dispatch.
|
|
509
|
+
*
|
|
510
|
+
* A contested lock gets a holder autopsy before rejection (#552): only a
|
|
511
|
+
* verified-dead holder pid — parseable pid, signal-0 ESRCH, and still dead on
|
|
512
|
+
* the pre-unlink re-read — authorizes reclaim, because no writer is left to
|
|
513
|
+
* release the lock; acquire then retries the create. An empty, unparseable, or
|
|
514
|
+
* unreadable lock proves no dead holder (a live creator is mid-acquisition
|
|
515
|
+
* between the exclusive create and its pid write), so it rejects as
|
|
516
|
+
* RunWriterLeaseHeldError naming the path and the lock stays on disk — a
|
|
517
|
+
* crash-window empty lock blocking a resume is that refusal's known residue,
|
|
518
|
+
* not safely fixable by unlink here. A live holder rejects the same typed
|
|
519
|
+
* error naming the pid and path. A pid recycled by an unrelated process reads
|
|
520
|
+
* as alive — that degrades to the same typed rejection, never worse than the
|
|
521
|
+
* pre-#552 behavior. Reclaim rounds are bounded by
|
|
522
|
+
* WRITER_LEASE_RECLAIM_ROUNDS; a lock that stays contested (e.g. a reclaim
|
|
523
|
+
* race repeatedly lost) surfaces the same typed error instead of spinning.
|
|
388
524
|
*
|
|
389
525
|
* `onCleanupFailure` receives a non-terminal diagnostic line when release-time
|
|
390
|
-
* lock cleanup fails
|
|
391
|
-
*
|
|
392
|
-
* still land somewhere observable — silent
|
|
526
|
+
* lock cleanup fails or a contested lock cannot be read. Release stays
|
|
527
|
+
* best-effort (a stale lock is reclaimed by the next acquire's autopsy), but
|
|
528
|
+
* the true error identity must still land somewhere observable — silent
|
|
529
|
+
* swallowing is forbidden.
|
|
393
530
|
*/
|
|
394
531
|
export async function acquireRunWriterLease(
|
|
395
532
|
runDirectory: string,
|
|
@@ -401,58 +538,57 @@ export async function acquireRunWriterLease(
|
|
|
401
538
|
// cause has already been handed to the sink as its argument.
|
|
402
539
|
try {
|
|
403
540
|
onCleanupFailure?.(
|
|
404
|
-
`writer lease lock cleanup failed (best-effort continue; stale lock
|
|
541
|
+
`writer lease lock cleanup failed (best-effort continue; stale lock is reclaimed by the next acquire's holder autopsy) at ${join(runDirectory, WRITER_LOCK_FILE)}: ${describeErrorIdentity(error)}`,
|
|
405
542
|
);
|
|
406
543
|
} catch {
|
|
407
544
|
// diagnostic-sink failure is itself best-effort; never break release().
|
|
408
545
|
}
|
|
409
546
|
};
|
|
410
547
|
const lockPath = join(runDirectory, WRITER_LOCK_FILE);
|
|
411
|
-
|
|
412
|
-
|
|
548
|
+
let lastAutopsy: WriterLockAutopsy = { verdict: "absent" };
|
|
549
|
+
// The reclaim budget: every successful reclaim is immediately followed by a
|
|
550
|
+
// fresh create attempt, including the budget's last one (the previous shape
|
|
551
|
+
// exited after a final-round reclaim with the path already clear).
|
|
552
|
+
for (let reclaimsLeft = WRITER_LEASE_RECLAIM_ROUNDS; ; reclaimsLeft -= 1) {
|
|
413
553
|
try {
|
|
414
|
-
await
|
|
554
|
+
return await createWriterLease(lockPath, runDirectory, reportCleanupFailure);
|
|
415
555
|
} catch (error) {
|
|
416
|
-
|
|
417
|
-
await unlink(lockPath).catch(() => undefined);
|
|
418
|
-
throw error;
|
|
556
|
+
if (errorCodeOf(error) !== "EEXIST") throw error;
|
|
419
557
|
}
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
"code" in error &&
|
|
450
|
-
(error as { code?: unknown }).code === "EEXIST"
|
|
451
|
-
) {
|
|
452
|
-
throw new RunWriterLeaseHeldError();
|
|
558
|
+
lastAutopsy = await autopsyWriterLock(lockPath);
|
|
559
|
+
if (lastAutopsy.verdict === "absent" && lastAutopsy.readFailure !== undefined) {
|
|
560
|
+
// 失败诚实: a lock we could not read must land its true read cause
|
|
561
|
+
// somewhere observable — recorded, but never treated as proof of death.
|
|
562
|
+
reportCleanupFailure(lastAutopsy.readFailure);
|
|
563
|
+
}
|
|
564
|
+
if (lastAutopsy.verdict === "alive") {
|
|
565
|
+
throw new RunWriterLeaseHeldError(
|
|
566
|
+
`role run writer lease is already held by live pid ${lastAutopsy.pid} at ${lockPath}`,
|
|
567
|
+
);
|
|
568
|
+
}
|
|
569
|
+
if (lastAutopsy.verdict === "absent") {
|
|
570
|
+
// #552 mechanical criterion: stale = holder pid verified dead. An
|
|
571
|
+
// empty lock (a live creator is mid-acquisition before its pid write)
|
|
572
|
+
// or an unreadable one proves no dead holder — unlinking here could
|
|
573
|
+
// steal a live writer's lock, so reject typed and leave the lock.
|
|
574
|
+
throw new RunWriterLeaseHeldError(
|
|
575
|
+
lastAutopsy.readFailure !== undefined
|
|
576
|
+
? `role run writer lease lock is unreadable at ${lockPath}: ${describeErrorIdentity(lastAutopsy.readFailure)}; holder liveness unverifiable, lock left in place`
|
|
577
|
+
: `role run writer lease lock at ${lockPath} has no verifiable holder pid (empty or unparseable); holder liveness unverifiable, lock left in place`,
|
|
578
|
+
);
|
|
579
|
+
}
|
|
580
|
+
if (reclaimsLeft <= 0) break;
|
|
581
|
+
try {
|
|
582
|
+
await reclaimStaleWriterLock(lockPath, runDirectory);
|
|
583
|
+
} catch (reclaimError) {
|
|
584
|
+
throw new RunWriterLeaseHeldError(
|
|
585
|
+
`stale writer lease reclaim failed at ${lockPath} (autopsy: ${describeAutopsy(lastAutopsy)}): ${describeErrorIdentity(reclaimError)}`,
|
|
586
|
+
);
|
|
453
587
|
}
|
|
454
|
-
throw error;
|
|
455
588
|
}
|
|
589
|
+
throw new RunWriterLeaseHeldError(
|
|
590
|
+
`role run writer lease stayed contested at ${lockPath} after ${WRITER_LEASE_RECLAIM_ROUNDS} reclaims (last autopsy: ${describeAutopsy(lastAutopsy)})`,
|
|
591
|
+
);
|
|
456
592
|
}
|
|
457
593
|
|
|
458
594
|
/**
|