@youdie006/prodex 0.40.6 → 0.40.9
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/README.md +31 -13
- package/dist/browser-send-lock.js +10 -131
- package/dist/chatgpt-browser.js +428 -746
- package/dist/cli-args.js +2 -0
- package/dist/cli-help.js +30 -24
- package/dist/cli-ledger.js +2 -0
- package/dist/cli-pro.js +328 -121
- package/dist/cli-server.js +2 -2
- package/dist/cli.js +1 -4
- package/dist/config.js +3 -3
- package/dist/continue-thread.js +9 -2
- package/dist/http-mcp.js +1 -1
- package/dist/issue-report.js +9 -4
- package/dist/mcp-tools.js +42 -10
- package/dist/mcp.js +11 -7
- package/dist/registry.js +54 -6
- package/dist/repo-write.js +22 -2
- package/dist/safe-file.js +249 -1
- package/dist/schema.js +8 -0
- package/dist/store.js +10 -1
- package/dist/tui-flow.js +0 -1
- package/dist/tui-run.js +62 -25
- package/dist/tui.js +68 -54
- package/docs/claude.md +6 -2
- package/docs/cli-reference.md +18 -7
- package/docs/clients.md +18 -6
- package/docs/http-mcp.md +5 -1
- package/docs/releasing.md +3 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -14,7 +14,9 @@
|
|
|
14
14
|
|
|
15
15
|
</div>
|
|
16
16
|
|
|
17
|
-
You pay for ChatGPT Pro. The reasoning that makes it worth paying for lives behind a web page, and the coding agent you actually spend the day with cannot reach it. prodex closes that gap without an API key, a proxy, or a stealth bot: it drives a real
|
|
17
|
+
You pay for ChatGPT Pro. The reasoning that makes it worth paying for lives behind a web page, and the coding agent you actually spend the day with cannot reach it. prodex closes that gap without an API key, a proxy, or a stealth bot: it drives a real Chrome that you logged into once, types into the same composer you would, and reads the rendered answer from the page.
|
|
18
|
+
|
|
19
|
+
The recording below is from 0.40.6, before internal transcript access was removed. Current builds use rendered page content only; they do not fetch hidden ChatGPT APIs or extract session tokens.
|
|
18
20
|
|
|
19
21
|
```console
|
|
20
22
|
$ prodex ask --new-chat --effort Pro "A CLI drives a logged-in browser over the Chrome DevTools Protocol and holds a cross-process file lock while a send is in flight. What failure modes must the lock's expiry rule handle, and which single rule would you ship? Under 150 words."
|
|
@@ -38,11 +40,11 @@ That is a real run, timings included. Every consult lands as a task, a result an
|
|
|
38
40
|
|
|
39
41
|
## What you can do with it
|
|
40
42
|
|
|
41
|
-
- **Ask from the terminal.** `prodex ask` with a question, a file's contents (`--file`), an uploaded pdf, deck, sheet or image (`--attach`), or anything piped in (`--stdin`).
|
|
43
|
+
- **Ask from the terminal.** `prodex ask` with a question, a file's contents (`--file`), an uploaded pdf, deck, sheet or image (`--attach`), or anything piped in (`--stdin`). Available rendered composer tools can be selected with `--tool`; unsupported result formats stop with a blocker.
|
|
42
44
|
- **Let your agent ask.** `prodex mcp` is a stdio MCP server with a `pro_consult` tool; Claude Code, Codex, Cursor and Gemini CLI call it like any other tool. ChatGPT Projects can hand work back the other way over a loopback HTTP MCP bridge.
|
|
43
45
|
- **Pick the model and effort per ask.** The picker ChatGPT shows is the picker prodex drives: `--effort Pro` reaches the top rung, `--project` sends inside a sidebar project, and `prodex setup` pins defaults per repo.
|
|
44
46
|
- **Keep every answer.** Tasks, results, sessions and receipts are versioned JSON on disk, signed with a local key. Nothing is stored anywhere else.
|
|
45
|
-
- **Run it with no window
|
|
47
|
+
- **Run it with no desktop window.** One headed login, then a virtual display: a dedicated browser running off your desktop. Authentication and protective checks still require your attention.
|
|
46
48
|
- **Stop where a person should.** Login, captcha, Cloudflare, usage limits and permission prompts halt the send with a named blocker and a next step. prodex solves none of them for you.
|
|
47
49
|
|
|
48
50
|
## Install
|
|
@@ -67,16 +69,19 @@ prodex pro latest # re-print the last answer
|
|
|
67
69
|
|
|
68
70
|
`prodex ask` is the short form of `prodex pro browser ask`; every flag works on both. The login opens its own Chrome profile (`~/.local/share/prodex/chrome-chatgpt-pro`), never your daily browser, and in a terminal it keeps watching the window and names the manual step still missing (sign in, clear a check, open a chat) until it reports READY.
|
|
69
71
|
|
|
70
|
-
While Pro thinks, progress goes to stderr: connecting, prompt sent, elapsed time while generating. A Pro selection raises the send budget to twenty minutes on its own; `--timeout-ms` overrides it.
|
|
72
|
+
While Pro thinks, progress goes to stderr: connecting, prompt sent, elapsed time while generating. A Pro selection raises the send budget to twenty minutes on its own; `--timeout-ms` overrides it. Answers are read from the rendered page, so formatting can differ from the original message. If the dedicated browser is not running, an interactive `ask` starts it, waits for your saved session, and retries once (`--no-auto-login` turns that off; scripts opt in with `--auto-login`).
|
|
73
|
+
|
|
74
|
+
If the browser stops responding after your question was sent, prodex stops without sending it again. Use the `thread` and `request_id` from the error with `prodex pro browser recover --target-url <thread-url> --request-id <32hex>` (MCP: `pro_recover`). The request ID verifies that the recovered assistant answer follows that exact marked user turn. Legacy recovery without it remains available but returns `request_verified: false` and a warning.
|
|
71
75
|
|
|
72
76
|
Useful flags on every send:
|
|
73
77
|
|
|
74
78
|
| Flag | What it does |
|
|
75
79
|
|---|---|
|
|
76
|
-
| `--new-chat` |
|
|
80
|
+
| `--new-chat` | Explicitly request the default: ordinary consults start in a fresh chat. The shared current tab is never an implicit destination. |
|
|
81
|
+
| `--session-key id` | Identify one caller for scoped `--continue`. Falls back to `PRODEX_SESSION_KEY`, then `CODEX_THREAD_ID`. |
|
|
77
82
|
| `--file path` | Inline a text file's contents into the prompt. Repeatable. |
|
|
78
83
|
| `--attach path` | Upload the file itself: the only way to hand ChatGPT a pdf, pptx, xlsx or image. Paths must live inside the repo. |
|
|
79
|
-
| `--tool
|
|
84
|
+
| `--tool web-search` | Select a rendered composer tool. Automatic deep-research report retrieval is currently unsupported and is blocked before sending. |
|
|
80
85
|
| `--project "name"` | Send inside an existing sidebar project. `--project-new` creates one first. `prodex pro browser projects` lists exact names. |
|
|
81
86
|
| `--temporary` | A ChatGPT Temporary Chat: nothing in your chat list, but the answer is read off the page and cannot be recovered later. |
|
|
82
87
|
| `--json` | Structured output on stdout, progress on stderr. |
|
|
@@ -84,6 +89,8 @@ Useful flags on every send:
|
|
|
84
89
|
|
|
85
90
|
Prefer prompts to flags? `prodex ui` (or a bare `prodex` in a terminal) asks what to send and where, shows a progress bar, and prints the equivalent command so the flags are learnable.
|
|
86
91
|
|
|
92
|
+
Conversation/project lists contain only entries exposed by the rendered UI, not the full account history. Automatic chat/project deletion and hidden transcript/report retrieval are unavailable; perform those operations yourself in ChatGPT. A successful recovery requires the requested conversation and a stable, finished answer.
|
|
93
|
+
|
|
87
94
|
## Agents over MCP
|
|
88
95
|
|
|
89
96
|
**Claude Code, Codex, Cursor, Gemini CLI** talk to prodex over stdio. For Claude:
|
|
@@ -98,7 +105,9 @@ prints a token-free config that points Claude at `prodex mcp --cwd /absolute/pat
|
|
|
98
105
|
{ "mcpServers": { "prodex": { "command": "prodex", "args": ["mcp", "--cwd", "/absolute/path/to/your/repo"] } } }
|
|
99
106
|
```
|
|
100
107
|
|
|
101
|
-
The server exposes `pro_consult` (a visible-browser send, with the same model, effort, project and tool choices as the CLI), `pro_recover` (fetch an answer that finished after a timeout), the bridge ledger tools (`bridge_create_task`, `bridge_list_tasks`, `bridge_fetch_result`, receipts, sessions), bounded `repo_read_file` and `repo_search`, and a receipt-gated write path: `repo_write_file_dry_run` first, `repo_write_file_apply` only while git HEAD and the file's preimage hash still match, `repo_stage_reviewed_paths` for applied receipts only. No shell tool, no ungated write. `prodex claude prompt` prints a paste-ready prompt that verifies the wiring. [docs/claude.md](docs/claude.md) covers Claude Desktop and Claude Code; [docs/clients.md](docs/clients.md) covers the others, including the per-call approval and `tool_timeout_sec` Codex needs.
|
|
108
|
+
The server exposes `pro_consult` (a visible-browser send, with the same model, effort, project and tool choices as the CLI), `pro_recover` (fetch an answer that finished after a timeout), the bridge ledger tools (`bridge_create_task`, `bridge_list_tasks`, `bridge_fetch_result`, receipts, sessions), bounded `repo_read_file` and `repo_search`, and a receipt-gated write path: `repo_write_file_dry_run` first, `repo_write_file_apply` only while git HEAD and the file's preimage hash still match, `repo_stage_reviewed_paths` for applied receipts only. Each stdio MCP connection receives one default session key, ordinary consults start fresh, and `continue_thread` only searches that key and project. Logical agents sharing one MCP connection should pass distinct explicit `session_key` values and preserve them for follow-ups; an explicit key also preserves continuity across an MCP restart. No shell tool, no ungated write. `prodex claude prompt` prints a paste-ready prompt that verifies the wiring. [docs/claude.md](docs/claude.md) covers Claude Desktop and Claude Code; [docs/clients.md](docs/clients.md) covers the others, including the per-call approval and `tool_timeout_sec` Codex needs.
|
|
109
|
+
|
|
110
|
+
Updating the installed npm package does not reload an MCP process that is already running. Reconnect the MCP server or restart the Codex/Claude client to load the new build. The dedicated browser profile is separate and remains signed in, so this does not require ChatGPT authentication again.
|
|
102
111
|
|
|
103
112
|
An MCP server usually starts without `--cwd`, so a per-repo default can be missed. For defaults that apply from any directory, set `PRODEX_DEFAULT_PROJECT`, `PRODEX_DEFAULT_MODEL`, `PRODEX_DEFAULT_EFFORT` or `PRODEX_DEFAULT_PRO_MODE` in the agent's MCP `env` block; a per-repo config still wins field by field.
|
|
104
113
|
|
|
@@ -151,15 +160,22 @@ prodex status # shows the saved defaults
|
|
|
151
160
|
```sh
|
|
152
161
|
prodex pro browser login # once, headed: sign in
|
|
153
162
|
prodex pro browser login --virtual-display # from then on: no window anywhere
|
|
163
|
+
prodex pro browser login --headed # visible login/captcha when a hidden mode was saved
|
|
154
164
|
```
|
|
155
165
|
|
|
156
|
-
`--virtual-display` (or `PRODEX_VIRTUAL_DISPLAY=1`, which the MCP server and its auto-recovery honour too) starts an X virtual framebuffer and runs the dedicated Chrome on it. It
|
|
166
|
+
`--virtual-display` (or `PRODEX_VIRTUAL_DISPLAY=1`, which the MCP server and its auto-recovery honour too) starts an X virtual framebuffer and runs the dedicated Chrome on it. It uses ordinary headed Chrome without a desktop window, not Chrome's headless mode. Linux and WSL; needs `xvfb` and `xauth`. New displays use local abstract Unix sockets with per-display xauth authentication and no TCP listener. This does not bypass login or protection checks.
|
|
167
|
+
|
|
168
|
+
Already-running browsers and legacy TCP X servers are not stopped or migrated by the update. To migrate, finish pending consults, stop the dedicated browser and its old X server, then launch with `--virtual-display` using the updated prodex. A new launch skips any display number still occupied by a TCP listener.
|
|
169
|
+
|
|
170
|
+
The last recorded window mode is reused by later `login` commands and by CLI/MCP auto-recovery. One explicit mode flag (`--headed`, `--headless`, `--minimized`, or `--virtual-display`) overrides environment and saved state as a whole. With no mode flag, any non-empty mode environment value wins as a whole too, including `PRODEX_HEADLESS=0`, `false`, or `no`; otherwise the saved mode remains. The modes are mutually exclusive, and the normal first-run default remains a visible headed browser. If virtual-display setup fails, recovery stops instead of unexpectedly opening a desktop window.
|
|
157
171
|
|
|
158
172
|
`--minimized` keeps a window but minimizes it. Under WSLg a minimized Chrome still reports itself visible and consults keep working; a normal Linux desktop marks it hidden, and prodex refuses to send into a tab it cannot read, restores the window, and tells you.
|
|
159
173
|
|
|
160
174
|
`--headless` exists and is not usable against ChatGPT today: measured on a signed-in profile, headless Chrome stays on Cloudflare's interstitial past sixty seconds. Only the window is optional; the login is not.
|
|
161
175
|
|
|
162
|
-
|
|
176
|
+
If a hidden or virtual browser needs login, captcha, Cloudflare, or account verification, close that browser yourself and run `prodex pro browser login --headed` to complete the interactive step. Merely omitting `--headless` does not switch modes because the saved mode persists. prodex does not bypass the protection or kill a running browser to change its mode.
|
|
177
|
+
|
|
178
|
+
Before a prompt is submitted, a browser confirmed to have stopped answering its control port can be ended and started fresh, and the receipt says so (`PRODEX_NO_AUTO_CLEAR=1` turns that off). A browser that is merely slow is left alone. After submission, prodex never auto-resends a lost prompt.
|
|
163
179
|
|
|
164
180
|
## Receipts
|
|
165
181
|
|
|
@@ -175,7 +191,9 @@ Everything a consult touches is written under `.bridge/` in the repo it ran from
|
|
|
175
191
|
diagnostics/ screenshots and page-shape snapshots from failed sends, when enabled
|
|
176
192
|
```
|
|
177
193
|
|
|
178
|
-
`prodex init` creates the ledger (a browser send creates it on first use too). `prodex pro latest`, `pro show`, `results show`, `results artifact`, `receipts show` and `sessions show` read them; `--json` on the list commands gives structured output.
|
|
194
|
+
`prodex init` creates the ledger (a browser send creates it on first use too). `prodex pro latest`, `pro show`, `results show`, `results artifact`, `receipts show` and `sessions show` read them; `--json` on the list commands gives structured output. Newly finalized result artifacts are checked against their recorded sha256. Legacy artifacts without hashes remain readable with a `legacy_artifact_unverified` warning; signing result metadata does not verify those bytes. A blocked consult is completed as blocked with its code and next step, so `pro latest` shows what happened even when nothing was sent. `prodex receipts rotate-key` signs new receipts with a fresh key while older ones stay verifiable; `prodex results reseal <task-id> --confirm-current-result` re-signs a legacy result you have reviewed.
|
|
195
|
+
|
|
196
|
+
`sessions cancel` clears a stale session record after an interrupted send; it does not stop a running browser request. Stop that request in the terminal or client that started it.
|
|
179
197
|
|
|
180
198
|
Two sibling tools read the same ledger, found through the bridge registry prodex keeps in `~/.local/share/prodex/bridges.json`: [sessionwiki](https://github.com/youdie006/sessionwiki) indexes every consult as a searchable session, and [swapdex](https://github.com/youdie006/swapdex) lists recent consults after an account switch. Neither is required.
|
|
181
199
|
|
|
@@ -192,7 +210,7 @@ ChatGPT Projects ---------+ v
|
|
|
192
210
|
ChatGPT Pro
|
|
193
211
|
```
|
|
194
212
|
|
|
195
|
-
The browser is a real Chrome launched with `--remote-debugging-port` on `127.0.0.1`, live only while
|
|
213
|
+
The browser is a real Chrome launched with `--remote-debugging-port` on `127.0.0.1`, live only while the browser process is running. prodex checks the page state, confirms the tab is on a ChatGPT conversation it can read, applies the picker selection, types the prompt, waits for the answer to finish, and reads its rendered content. Sends are paced to human speed (one every ten seconds by default, `PRODEX_MIN_SEND_INTERVAL_MS` tunes it) and take a cross-process lock, so two agents on one machine queue rather than fight over the composer.
|
|
196
214
|
|
|
197
215
|
### What it will not do
|
|
198
216
|
|
|
@@ -225,9 +243,9 @@ Reports are deduplicated by blocker code, so something that stays broken adds to
|
|
|
225
243
|
|
|
226
244
|
**Why the pause before sending?** Pacing: `send_pacing: waiting Ns` on stderr. `PRODEX_MIN_SEND_INTERVAL_MS=0` disables it.
|
|
227
245
|
|
|
228
|
-
**The answer timed out.** Pro can take many minutes
|
|
246
|
+
**The answer timed out.** Pro can take many minutes. Do not resend automatically. Recover the original marked turn with `prodex pro browser recover --target-url <thread> --request-id <request_id>` after it finishes.
|
|
229
247
|
|
|
230
|
-
**
|
|
248
|
+
**A consult returned an unrelated answer.** Current sends append a unique visible request marker and accept only the assistant turn following that marker. Ordinary calls also start fresh. A `request_mismatch` blocker is not retryable; inspect the named request instead of treating the returned page's last answer as the consult.
|
|
231
249
|
|
|
232
250
|
**Every send says "still generating" and nothing is being written.** ChatGPT parked the thread on "which response do you prefer?". prodex reports `response_choice_pending` and names the buttons; pick one, or send with `--new-chat`.
|
|
233
251
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { link, mkdir, open, readFile, rm, stat } from "node:fs/promises";
|
|
2
1
|
import os from "node:os";
|
|
3
2
|
import path from "node:path";
|
|
3
|
+
import { withCrossProcessFileLock } from "./safe-file.js";
|
|
4
4
|
// One visible-browser send at a time per machine: the dedicated Chrome is a
|
|
5
5
|
// single shared tab, and two concurrent prodex clients interleave composer
|
|
6
6
|
// input and navigation, silently cross-contaminating each other's threads
|
|
@@ -12,94 +12,6 @@ function lockPath() {
|
|
|
12
12
|
return override;
|
|
13
13
|
return path.join(os.homedir(), ".local", "share", "prodex", "browser-send.lock");
|
|
14
14
|
}
|
|
15
|
-
function holderIsAlive(pid) {
|
|
16
|
-
try {
|
|
17
|
-
process.kill(pid, 0);
|
|
18
|
-
return true;
|
|
19
|
-
}
|
|
20
|
-
catch (error) {
|
|
21
|
-
// EPERM: the process exists but is owned by another user -> alive. Only
|
|
22
|
-
// ESRCH (no such process) means the holder is truly dead and reapable.
|
|
23
|
-
return error.code === "EPERM";
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
async function readHolder(file) {
|
|
27
|
-
try {
|
|
28
|
-
const parsed = JSON.parse(await readFile(file, "utf8"));
|
|
29
|
-
return {
|
|
30
|
-
pid: typeof parsed.pid === "number" ? parsed.pid : undefined,
|
|
31
|
-
started_at: typeof parsed.started_at === "string" ? parsed.started_at : undefined
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
catch {
|
|
35
|
-
return undefined;
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
// A holder whose process is alive but has held the lock far longer than any real
|
|
39
|
-
// send (default 60 min, well beyond the 15-min Pro timeout) is treated as wedged
|
|
40
|
-
// and reapable, so a hung browser cannot block every send on the machine forever.
|
|
41
|
-
// Deliberately generous so it never reaps a genuinely in-flight send; override
|
|
42
|
-
// with PRODEX_SEND_LOCK_STALE_MS.
|
|
43
|
-
function staleMs() {
|
|
44
|
-
const raw = Number(process.env.PRODEX_SEND_LOCK_STALE_MS);
|
|
45
|
-
return Number.isFinite(raw) && raw > 0 ? raw : 3_600_000;
|
|
46
|
-
}
|
|
47
|
-
function holderIsStale(startedAt) {
|
|
48
|
-
if (!startedAt)
|
|
49
|
-
return false;
|
|
50
|
-
const started = Date.parse(startedAt);
|
|
51
|
-
if (Number.isNaN(started))
|
|
52
|
-
return false;
|
|
53
|
-
return Date.now() - started > staleMs();
|
|
54
|
-
}
|
|
55
|
-
// Release only our own lock: if ours was already reaped (e.g. as stale) and a
|
|
56
|
-
// new holder took over, we must not delete their lock on the way out.
|
|
57
|
-
async function releaseIfOwned(file) {
|
|
58
|
-
const holder = await readHolder(file);
|
|
59
|
-
if (holder?.pid === process.pid) {
|
|
60
|
-
await rm(file, { force: true }).catch(() => undefined);
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
let acquireSeq = 0;
|
|
64
|
-
async function tryAcquire(file) {
|
|
65
|
-
await mkdir(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
66
|
-
// Publish atomically: write the pid into a temp file, then hard-link it into
|
|
67
|
-
// place. link() fails with EEXIST when a holder already exists (our exclusivity
|
|
68
|
-
// check) and, unlike open("wx")+writeFile, the lock file is never observed
|
|
69
|
-
// empty - so a concurrent waiter can never mistake a mid-publish lock for a
|
|
70
|
-
// dead one and reap it out from under us (which let two clients send at once).
|
|
71
|
-
// The temp name is unique per acquisition (pid + seq) so two concurrent
|
|
72
|
-
// same-process acquires never share a temp inode and truncate each other.
|
|
73
|
-
const temp = `${file}.${process.pid}.${(acquireSeq += 1)}.tmp`;
|
|
74
|
-
const handle = await open(temp, "w", 0o600);
|
|
75
|
-
try {
|
|
76
|
-
await handle.writeFile(`${JSON.stringify({ pid: process.pid, started_at: new Date().toISOString() })}\n`);
|
|
77
|
-
}
|
|
78
|
-
finally {
|
|
79
|
-
await handle.close();
|
|
80
|
-
}
|
|
81
|
-
try {
|
|
82
|
-
await link(temp, file);
|
|
83
|
-
return true;
|
|
84
|
-
}
|
|
85
|
-
catch (error) {
|
|
86
|
-
if (error.code !== "EEXIST")
|
|
87
|
-
throw error;
|
|
88
|
-
return false;
|
|
89
|
-
}
|
|
90
|
-
finally {
|
|
91
|
-
await rm(temp, { force: true }).catch(() => undefined);
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
async function lockFileExists(file) {
|
|
95
|
-
try {
|
|
96
|
-
await stat(file);
|
|
97
|
-
return true;
|
|
98
|
-
}
|
|
99
|
-
catch {
|
|
100
|
-
return false;
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
15
|
/**
|
|
104
16
|
* Serialize visible-browser sends across processes. Waits up to waitMs for a
|
|
105
17
|
* live holder to finish (0 = fail fast); a lock whose holder process is dead
|
|
@@ -107,46 +19,13 @@ async function lockFileExists(file) {
|
|
|
107
19
|
*/
|
|
108
20
|
export async function withBrowserSendLock(waitMs, onWait, fn) {
|
|
109
21
|
const file = lockPath();
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
// don't delete a fresh lock another reaper just acquired in between.
|
|
120
|
-
const current = await readHolder(file);
|
|
121
|
-
if (current?.pid === holder?.pid) {
|
|
122
|
-
await rm(file, { force: true }).catch(() => undefined);
|
|
123
|
-
}
|
|
124
|
-
// A reap that did not actually remove the file - another user's lock in a
|
|
125
|
-
// shared directory, or a directory sitting in its place - used to retry
|
|
126
|
-
// immediately, skipping both the sleep and the deadline: a hot loop that
|
|
127
|
-
// never returned and never timed out. Waiting here costs a reap that
|
|
128
|
-
// raced nothing; not waiting costs the process.
|
|
129
|
-
if (await lockFileExists(file)) {
|
|
130
|
-
if (Date.now() >= deadline) {
|
|
131
|
-
throw new Error(`A prodex browser send lock at ${file} is held by nothing and could not be removed, so no send can start. Delete that file and retry.`);
|
|
132
|
-
}
|
|
133
|
-
await new Promise((resolve) => setTimeout(resolve, 2_000));
|
|
134
|
-
}
|
|
135
|
-
continue;
|
|
136
|
-
}
|
|
137
|
-
if (Date.now() >= deadline) {
|
|
138
|
-
throw new Error(`Another prodex browser send is in progress (pid ${holder.pid}) and did not finish within the wait budget. Retry once it finishes, or raise --timeout-ms (which is also the queue budget).`);
|
|
139
|
-
}
|
|
140
|
-
if (!waited) {
|
|
141
|
-
waited = true;
|
|
142
|
-
onWait(`another prodex send holds the browser (pid ${holder.pid}); waiting`);
|
|
143
|
-
}
|
|
144
|
-
await new Promise((resolve) => setTimeout(resolve, 2_000));
|
|
145
|
-
}
|
|
146
|
-
try {
|
|
147
|
-
return await fn();
|
|
148
|
-
}
|
|
149
|
-
finally {
|
|
150
|
-
await releaseIfOwned(file);
|
|
151
|
-
}
|
|
22
|
+
const reapClaim = path.join(path.dirname(file), `.${path.basename(file)}.reap`);
|
|
23
|
+
return withCrossProcessFileLock(file, {
|
|
24
|
+
waitMs,
|
|
25
|
+
retryMs: 2_000,
|
|
26
|
+
privateParent: true,
|
|
27
|
+
onWait: (holder) => onWait(`another prodex send holds the browser (pid ${holder.pid ?? "unknown"}); waiting`),
|
|
28
|
+
busyError: (holder) => new Error(`Another prodex browser send is in progress (pid ${holder.pid ?? "unknown"}) and did not finish within the wait budget. Retry once it finishes, or raise --timeout-ms (which is also the queue budget).`),
|
|
29
|
+
unavailableError: () => new Error(`A prodex browser send lock at ${file} is held by nothing and could not be removed, so no send can start. Stop all prodex senders and verify no request is active before deleting that file and the abandoned reaper claim at ${reapClaim}, then retry.`)
|
|
30
|
+
}, fn);
|
|
152
31
|
}
|