@youdie006/prodex 0.40.43 → 0.40.44
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/safe-file.js +37 -22
- package/dist/tui.js +7 -2
- package/docs/releasing.md +22 -0
- package/package.json +1 -1
package/dist/safe-file.js
CHANGED
|
@@ -420,6 +420,9 @@ async function tryAcquireFileLock(filePath, parentSnapshot) {
|
|
|
420
420
|
nonce: randomUUID(),
|
|
421
421
|
started_at: new Date().toISOString()
|
|
422
422
|
};
|
|
423
|
+
const pidStart = linuxProcessStartTicks(process.pid);
|
|
424
|
+
if (pidStart !== undefined)
|
|
425
|
+
owner.pid_start = pidStart;
|
|
423
426
|
const tokenPath = fileLockTokenPath(filePath, owner);
|
|
424
427
|
const handle = await openStableNoFollow(tokenPath, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL, "write", parentSnapshot, 0o600);
|
|
425
428
|
try {
|
|
@@ -488,7 +491,8 @@ function parseFileLockHolder(content) {
|
|
|
488
491
|
return {
|
|
489
492
|
pid: Number.isSafeInteger(parsed.pid) && Number(parsed.pid) > 0 ? Number(parsed.pid) : undefined,
|
|
490
493
|
nonce: typeof parsed.nonce === "string" && /^[A-Za-z0-9-]{1,128}$/.test(parsed.nonce) ? parsed.nonce : undefined,
|
|
491
|
-
started_at: typeof parsed.started_at === "string" ? parsed.started_at : undefined
|
|
494
|
+
started_at: typeof parsed.started_at === "string" ? parsed.started_at : undefined,
|
|
495
|
+
pid_start: typeof parsed.pid_start === "string" && /^\d{1,20}$/.test(parsed.pid_start) ? parsed.pid_start : undefined
|
|
492
496
|
};
|
|
493
497
|
}
|
|
494
498
|
catch {
|
|
@@ -498,7 +502,12 @@ function parseFileLockHolder(content) {
|
|
|
498
502
|
function parseFileLockOwner(holder) {
|
|
499
503
|
if (holder.pid === undefined || holder.nonce === undefined)
|
|
500
504
|
return undefined;
|
|
501
|
-
return {
|
|
505
|
+
return {
|
|
506
|
+
pid: holder.pid,
|
|
507
|
+
nonce: holder.nonce,
|
|
508
|
+
started_at: holder.started_at,
|
|
509
|
+
...(holder.pid_start !== undefined ? { pid_start: holder.pid_start } : {})
|
|
510
|
+
};
|
|
502
511
|
}
|
|
503
512
|
async function releaseFileLockLease(filePath, lease, parentSnapshot) {
|
|
504
513
|
try {
|
|
@@ -591,36 +600,42 @@ function processIsAlive(pid) {
|
|
|
591
600
|
}
|
|
592
601
|
}
|
|
593
602
|
/**
|
|
594
|
-
* Whether the holder's pid now names a process
|
|
595
|
-
*
|
|
596
|
-
* or its container leaves the lock behind, and after a restart pids
|
|
597
|
-
* out again: measured, a 2020 lock naming pid 1 held off every send
|
|
598
|
-
* whole wait budget because pid 1 was alive - as a different process.
|
|
603
|
+
* Whether the holder's pid now names a different process than the one that
|
|
604
|
+
* wrote the lock, so the writer is gone. A send that dies with the machine,
|
|
605
|
+
* the WSL VM or its container leaves the lock behind, and after a restart pids
|
|
606
|
+
* are handed out again: measured, a 2020 lock naming pid 1 held off every send
|
|
607
|
+
* for its whole wait budget because pid 1 was alive - as a different process.
|
|
608
|
+
*
|
|
609
|
+
* The lock records its writer's start time in clock ticks since boot, and that
|
|
610
|
+
* is compared with the pid's start ticks now: boot clock against boot clock.
|
|
611
|
+
* 0.40.43 converted the start to wall time through /proc/stat btime instead,
|
|
612
|
+
* and on WSL2 btime moves (measured 30 minutes over one day: the VM's boot
|
|
613
|
+
* clock stops while the host sleeps, then the wall clock is resynced), so a
|
|
614
|
+
* live holder looked newer than its own lock and its lock was reaped mid-send.
|
|
599
615
|
*/
|
|
600
616
|
function pidReusedSince(holder) {
|
|
617
|
+
if (holder.pid === undefined)
|
|
618
|
+
return false;
|
|
619
|
+
if (holder.pid_start !== undefined) {
|
|
620
|
+
const current = linuxProcessStartTicks(holder.pid);
|
|
621
|
+
return current !== undefined && current !== holder.pid_start;
|
|
622
|
+
}
|
|
623
|
+
// A lock from a prodex that did not record start ticks: the only safe call is
|
|
624
|
+
// that nothing alive now was running before the last boot.
|
|
601
625
|
const writtenAt = Date.parse(holder.started_at ?? "");
|
|
602
|
-
if (
|
|
626
|
+
if (!Number.isFinite(writtenAt))
|
|
603
627
|
return false;
|
|
604
|
-
const processStartedAt = linuxProcessStartMs(holder.pid);
|
|
605
|
-
// A holder writes the lock after it starts; a few seconds absorb clock steps.
|
|
606
|
-
if (processStartedAt !== undefined)
|
|
607
|
-
return processStartedAt > writtenAt + 5_000;
|
|
608
|
-
// No per-process start time here: nothing alive now predates the boot.
|
|
609
628
|
return writtenAt < Date.now() - os.uptime() * 1000 - 60_000;
|
|
610
629
|
}
|
|
611
|
-
function
|
|
630
|
+
function linuxProcessStartTicks(pid) {
|
|
612
631
|
if (process.platform !== "linux")
|
|
613
632
|
return undefined;
|
|
614
633
|
try {
|
|
615
|
-
const btime = /^btime (\d+)$/m.exec(readFileSync("/proc/stat", "utf8"))?.[1];
|
|
616
634
|
const stat = readFileSync(`/proc/${pid}/stat`, "utf8");
|
|
617
|
-
// Field 22 (starttime
|
|
618
|
-
//
|
|
619
|
-
const ticks =
|
|
620
|
-
|
|
621
|
-
return undefined;
|
|
622
|
-
// USER_HZ is 100 on every Linux ABI Node supports.
|
|
623
|
-
return Number(btime) * 1000 + ticks * 10;
|
|
635
|
+
// Field 22 (starttime) counted after the parenthesised command name, which
|
|
636
|
+
// may itself contain spaces.
|
|
637
|
+
const ticks = stat.slice(stat.lastIndexOf(")") + 2).split(" ")[19];
|
|
638
|
+
return ticks !== undefined && /^\d+$/.test(ticks) ? ticks : undefined;
|
|
624
639
|
}
|
|
625
640
|
catch {
|
|
626
641
|
return undefined;
|
package/dist/tui.js
CHANGED
|
@@ -63,10 +63,15 @@ export function consultArgsFromChoices(choices) {
|
|
|
63
63
|
*/
|
|
64
64
|
export function parseAttachmentLine(line) {
|
|
65
65
|
const out = [];
|
|
66
|
-
|
|
66
|
+
// An unquoted word may carry backslash escapes: dropping a file onto a macOS
|
|
67
|
+
// terminal types `My\ File.pdf`, which used to split into two paths that do
|
|
68
|
+
// not exist. Only a space or shell punctuation counts as escaped, so a
|
|
69
|
+
// Windows separator (a backslash before a letter or another backslash) stays.
|
|
70
|
+
const pattern = /"([^"]*)"|'([^']*)'|((?:\\[ ()\[\]{}'"&;!$`*?#~<>|]|\S)+)/g;
|
|
67
71
|
let match;
|
|
68
72
|
while ((match = pattern.exec(line)) !== null) {
|
|
69
|
-
const
|
|
73
|
+
const unquoted = match[3]?.replace(/\\([ ()\[\]{}'"&;!$`*?#~<>|])/g, "$1");
|
|
74
|
+
const value = (match[1] ?? match[2] ?? unquoted ?? "").trim();
|
|
70
75
|
if (value.length > 0)
|
|
71
76
|
out.push(value);
|
|
72
77
|
}
|
package/docs/releasing.md
CHANGED
|
@@ -35,6 +35,28 @@ The release adds a guarded, opt-in transition and more accurate blocker reportin
|
|
|
35
35
|
**Open**
|
|
36
36
|
- A long-running headless browser ends up behind Cloudflare's check (also noted on WSL, where the Xvfb display is used instead). On macOS there is no virtual display, and a minimized window counts as hidden, so the remaining no-headless option there is a window kept behind others. Changing the headless preference is a design decision that was not made here.
|
|
37
37
|
|
|
38
|
+
## 0.40.43 Release Record
|
|
39
|
+
|
|
40
|
+
Release commit `834e3e2` on `main` (cut after PRs #49-#58, last merge `a6bbf85`); tag `v0.40.43`. Ten fixes, including two that block or corrupt work: a send lock left by a dead send whose pid was reused blocked every later send (#54), and two processes opening a fresh repo could fail one with a false "receipt-key.local is corrupt" (#58).
|
|
41
|
+
|
|
42
|
+
**Publishing**
|
|
43
|
+
- `publish.yml` run 40451563636 first failed on macOS 15 Intel only: `tests/cli.test.ts` "uses an explicit --cwd target for local HTTP MCP start" timed out at 90 s; the same commit passed on the five other platforms and in PR CI. The failed job was rerun and passed, then npm publish with SLSA v1 provenance and the GitHub Release ran. The GitHub Release lists the ten entries from `CHANGELOG.md`.
|
|
44
|
+
- `npm run release:verify` passed on the release commit after a build.
|
|
45
|
+
|
|
46
|
+
**Installed**
|
|
47
|
+
- WSL, M3, Mac mini: `prodex --version` reports 0.40.43. Running services were not restarted: no host stdio MCP servers on WSL, 1 on M3, none on the Mac mini.
|
|
48
|
+
- The maintainer watchdog checkout was pulled to `834e3e2` and rebuilt.
|
|
49
|
+
|
|
50
|
+
**Container**
|
|
51
|
+
- `container/redesign-patches` `f72862c` ports #49-#58; full suite (164 files, 2098 tests) and `release:verify` pass on it.
|
|
52
|
+
- WSL: image rebuilt and the container swapped to `1d48d753fbf7`; the in-container send lock was idle before and after.
|
|
53
|
+
- M3: not rebuilt in this release. The build over ssh was refused by the agent's permission classifier; the M3 container still runs the 0.40.42 port.
|
|
54
|
+
|
|
55
|
+
**Verified live on installed 0.40.43 (WSL)**
|
|
56
|
+
- `pro blockers --since 7` refused: "--since expects a date or an age like 7d or 24h".
|
|
57
|
+
- `pro blockers --json` across the machine: 21 roots and 657 consults (before: 27 roots, about twice the consults, counting case-variant paths of the same repo).
|
|
58
|
+
- `ask --project " "` refused: "--project is blank".
|
|
59
|
+
|
|
38
60
|
## 0.40.42 Release Record
|
|
39
61
|
|
|
40
62
|
Release commit `9c97a14` on `main` (cut after PR #48, merge `cc129d8`); tag `v0.40.42`. Released on its own: before it, `--attach` and MCP `attach` uploaded `.env`, `.git` contents, key files and outward symlinks.
|