@youdie006/prodex 0.16.13 → 0.16.15
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/browser-send-lock.js +79 -0
- package/dist/cli-pro.js +11 -7
- package/package.json +1 -1
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { mkdir, open, readFile, rm } from "node:fs/promises";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
// One visible-browser send at a time per machine: the dedicated Chrome is a
|
|
5
|
+
// single shared tab, and two concurrent prodex clients interleave composer
|
|
6
|
+
// input and navigation, silently cross-contaminating each other's threads
|
|
7
|
+
// (measured live: one client's token prompt landed inside the other client's
|
|
8
|
+
// consult thread, and a 15-minute consult never actually posted).
|
|
9
|
+
function lockPath() {
|
|
10
|
+
const override = process.env.PRODEX_SEND_LOCK_FILE;
|
|
11
|
+
if (override)
|
|
12
|
+
return override;
|
|
13
|
+
return path.join(os.homedir(), ".local", "share", "prodex", "browser-send.lock");
|
|
14
|
+
}
|
|
15
|
+
function holderIsAlive(pid) {
|
|
16
|
+
try {
|
|
17
|
+
process.kill(pid, 0);
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
async function readHolderPid(file) {
|
|
25
|
+
try {
|
|
26
|
+
const parsed = JSON.parse(await readFile(file, "utf8"));
|
|
27
|
+
return typeof parsed.pid === "number" ? parsed.pid : undefined;
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
async function tryAcquire(file) {
|
|
34
|
+
await mkdir(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
35
|
+
try {
|
|
36
|
+
const handle = await open(file, "wx", 0o600);
|
|
37
|
+
await handle.writeFile(`${JSON.stringify({ pid: process.pid, started_at: new Date().toISOString() })}\n`);
|
|
38
|
+
await handle.close();
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
if (error.code !== "EEXIST")
|
|
43
|
+
throw error;
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Serialize visible-browser sends across processes. Waits up to waitMs for a
|
|
49
|
+
* live holder to finish (0 = fail fast); a lock whose holder process is dead
|
|
50
|
+
* is reaped immediately.
|
|
51
|
+
*/
|
|
52
|
+
export async function withBrowserSendLock(waitMs, onWait, fn) {
|
|
53
|
+
const file = lockPath();
|
|
54
|
+
const deadline = Date.now() + Math.max(0, waitMs);
|
|
55
|
+
let waited = false;
|
|
56
|
+
for (;;) {
|
|
57
|
+
if (await tryAcquire(file))
|
|
58
|
+
break;
|
|
59
|
+
const holder = await readHolderPid(file);
|
|
60
|
+
if (holder === undefined || !holderIsAlive(holder)) {
|
|
61
|
+
await rm(file, { force: true }).catch(() => undefined);
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (Date.now() >= deadline) {
|
|
65
|
+
throw new Error(`Another prodex browser send is in progress (pid ${holder}). Wait for it to finish, or pass --busy-wait-ms to queue behind it.`);
|
|
66
|
+
}
|
|
67
|
+
if (!waited) {
|
|
68
|
+
waited = true;
|
|
69
|
+
onWait(`another prodex send holds the browser (pid ${holder}); waiting`);
|
|
70
|
+
}
|
|
71
|
+
await new Promise((resolve) => setTimeout(resolve, 2_000));
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
return await fn();
|
|
75
|
+
}
|
|
76
|
+
finally {
|
|
77
|
+
await rm(file, { force: true }).catch(() => undefined);
|
|
78
|
+
}
|
|
79
|
+
}
|
package/dist/cli-pro.js
CHANGED
|
@@ -8,6 +8,7 @@ import { listRawResultsForInspection, listTasksForInspection } from "./cli-ledge
|
|
|
8
8
|
import { formatBrowserDefaults, redactServerUrl } from "./cli-server.js";
|
|
9
9
|
import { errorMessage, firstLine, formatBlockedConsultRecordedMessage, formatProLatestCommand, formatBrowserCheckCommand, formatBrowserLoginCommand, formatBrowserSmokeCommand, formatBrowserTargetAskCommand, formatInitCommand, formatSetupCommand, isMissingFileError, computeSendPacingWaitMs, isUntrustedResultError, resolveMinSendIntervalMs, sourceAwareBrowserBlocker, sourceAwareBrowserNextStep, sourceAwareResultError, sourceAwareResultMessage, sourceAwareSetupMessage } from "./cli-shared.js";
|
|
10
10
|
import { getTokenExpiryStatus, loadBrowserDefaults, loadLocalConfig } from "./config.js";
|
|
11
|
+
import { withBrowserSendLock } from "./browser-send-lock.js";
|
|
11
12
|
import { BridgeStore, MAX_FETCHABLE_RESULT_ARTIFACT_BYTES } from "./store.js";
|
|
12
13
|
export async function runChatgptCommand(rest, io) {
|
|
13
14
|
const [subcommand, ...chatgptArgs] = rest;
|
|
@@ -80,12 +81,12 @@ export async function runChatgptCommand(rest, io) {
|
|
|
80
81
|
};
|
|
81
82
|
let result;
|
|
82
83
|
try {
|
|
83
|
-
result = await sendChatGptPrompt({
|
|
84
|
+
result = await withBrowserSendLock(0, (detail) => io.stderr(`progress: ${detail}`), () => sendChatGptPrompt({
|
|
84
85
|
port,
|
|
85
86
|
prompt: smokePrompt,
|
|
86
87
|
timeoutMs,
|
|
87
88
|
onProgress: createBrowserSendProgressPrinter(io.stderr)
|
|
88
|
-
});
|
|
89
|
+
}));
|
|
89
90
|
}
|
|
90
91
|
catch (error) {
|
|
91
92
|
const blocker = sourceAwareBrowserBlocker(browserSendBlockerFromError(error), sourceCli, commandOptions);
|
|
@@ -580,10 +581,13 @@ export async function runAskProCommand(rest, io) {
|
|
|
580
581
|
// from the confirmed tab).
|
|
581
582
|
const selectionModel = explicitModel ?? browserDefaults?.model;
|
|
582
583
|
const selectionProjectNew = explicitProjectNew;
|
|
583
|
-
// A persisted default project
|
|
584
|
-
//
|
|
584
|
+
// A persisted default project APPLIES under --new-chat: since 0.16.11 a
|
|
585
|
+
// fresh chat inside the project is exactly what "--new-chat + project"
|
|
586
|
+
// produces, and the whole point of pinning a default project is that
|
|
587
|
+
// consults stop landing in the general chat list. Only --target-url
|
|
588
|
+
// (pinned tab) and --project-new suppress it.
|
|
585
589
|
const selectionProject = explicitProject ??
|
|
586
|
-
(normalizedTargetUrl || selectionProjectNew !== undefined
|
|
590
|
+
(normalizedTargetUrl || selectionProjectNew !== undefined ? undefined : browserDefaults?.project);
|
|
587
591
|
const reasoningAxisChosen = explicitProMode !== undefined || explicitEffort !== undefined;
|
|
588
592
|
const selectionProMode = explicitProMode ?? (reasoningAxisChosen ? undefined : browserDefaults?.pro_mode);
|
|
589
593
|
const selectionEffort = explicitEffort ?? (reasoningAxisChosen ? undefined : browserDefaults?.effort);
|
|
@@ -655,7 +659,7 @@ export async function runAskProCommand(rest, io) {
|
|
|
655
659
|
}
|
|
656
660
|
throw new Error(formatBlockedConsultRecordedMessage(blocker.message, task.id, sourceCli, { cwd: targetCwd }));
|
|
657
661
|
}
|
|
658
|
-
const sendOnce = () => sendChatGptPrompt({
|
|
662
|
+
const sendOnce = () => withBrowserSendLock(busyWaitMs ?? 0, (detail) => io.stderr(`progress: ${detail}`), () => sendChatGptPrompt({
|
|
659
663
|
port: browserPort,
|
|
660
664
|
prompt: bundle.text,
|
|
661
665
|
targetUrl: normalizedTargetUrl,
|
|
@@ -668,7 +672,7 @@ export async function runAskProCommand(rest, io) {
|
|
|
668
672
|
proMode: selectionProMode,
|
|
669
673
|
effort: selectionEffort,
|
|
670
674
|
onProgress: createBrowserSendProgressPrinter(io.stderr)
|
|
671
|
-
});
|
|
675
|
+
}));
|
|
672
676
|
// One-command recovery: interactive terminals (or explicit --auto-login)
|
|
673
677
|
// launch the dedicated browser and retry once when no browser runs.
|
|
674
678
|
// Scripts and agents keep the plain blocker unless they opt in.
|