@youdie006/prodex 0.25.1 → 0.26.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.
- package/dist/chatgpt-browser.js +112 -2
- package/dist/cli-pro.js +23 -7
- package/package.json +1 -1
package/dist/chatgpt-browser.js
CHANGED
|
@@ -363,6 +363,48 @@ export async function ensureVirtualDisplay(options = {}) {
|
|
|
363
363
|
}
|
|
364
364
|
throw new Error(`No free X display between :${first} and :${first + 9}. Set PRODEX_VIRTUAL_DISPLAY_NUM to a free number.`);
|
|
365
365
|
}
|
|
366
|
+
/**
|
|
367
|
+
* How should the dedicated browser be opened?
|
|
368
|
+
*
|
|
369
|
+
* An explicit flag or environment variable wins; otherwise reopen it the way it
|
|
370
|
+
* was last opened. Without the saved fallback, `pro browser login` - the exact
|
|
371
|
+
* command every `browser_unreachable` blocker tells people to run - put a
|
|
372
|
+
* VISIBLE window back on the desktop of someone who had set up a virtual
|
|
373
|
+
* display, which is the surprise window they went to that trouble to avoid.
|
|
374
|
+
*
|
|
375
|
+
* Choosing a mode explicitly replaces the saved one wholesale rather than
|
|
376
|
+
* merging with it: asking for headless must not also rejoin an old virtual
|
|
377
|
+
* display.
|
|
378
|
+
*/
|
|
379
|
+
export function resolveBrowserWindowMode(args) {
|
|
380
|
+
const env = args.env ?? process.env;
|
|
381
|
+
const flags = args.flags ?? {};
|
|
382
|
+
const fromEnv = (name) => {
|
|
383
|
+
const raw = (env[name] ?? "").trim().toLowerCase();
|
|
384
|
+
if (raw === "")
|
|
385
|
+
return undefined;
|
|
386
|
+
return raw === "1" || raw === "true" || raw === "yes";
|
|
387
|
+
};
|
|
388
|
+
const explicit = {
|
|
389
|
+
headless: flags.headless ?? fromEnv("PRODEX_HEADLESS"),
|
|
390
|
+
virtualDisplay: flags.virtualDisplay ?? fromEnv("PRODEX_VIRTUAL_DISPLAY"),
|
|
391
|
+
minimized: flags.minimized ?? fromEnv("PRODEX_MINIMIZE_WINDOW")
|
|
392
|
+
};
|
|
393
|
+
const chosen = Object.values(explicit).some((value) => value === true);
|
|
394
|
+
if (chosen) {
|
|
395
|
+
return {
|
|
396
|
+
headless: explicit.headless === true,
|
|
397
|
+
virtualDisplay: explicit.virtualDisplay === true,
|
|
398
|
+
minimized: explicit.minimized === true
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
const saved = args.lastLogin;
|
|
402
|
+
return {
|
|
403
|
+
headless: saved?.headless === true,
|
|
404
|
+
virtualDisplay: saved?.virtual_display !== undefined,
|
|
405
|
+
minimized: saved?.minimized === true
|
|
406
|
+
};
|
|
407
|
+
}
|
|
366
408
|
export function resolveHeadlessPreference(explicit, env = process.env) {
|
|
367
409
|
if (typeof explicit === "boolean")
|
|
368
410
|
return explicit;
|
|
@@ -416,6 +458,11 @@ export function isUsableChatGptAnswer(answer) {
|
|
|
416
458
|
if (toolPanelOnly)
|
|
417
459
|
return false;
|
|
418
460
|
}
|
|
461
|
+
// ChatGPT's own interruption notice, which is what a thread shows after the
|
|
462
|
+
// browser dies mid-stream. Measured: recover saved "Pro thinking / Connection
|
|
463
|
+
// interrupted. Waiting for the complete answer" as a recovered answer.
|
|
464
|
+
if (/connection interrupted|연결이\s*중단/i.test(normalized) && normalized.length < 200)
|
|
465
|
+
return false;
|
|
419
466
|
return true;
|
|
420
467
|
}
|
|
421
468
|
/**
|
|
@@ -1745,6 +1792,26 @@ export async function recoverChatGptAnswerFromThread(options) {
|
|
|
1745
1792
|
warnings: []
|
|
1746
1793
|
};
|
|
1747
1794
|
}
|
|
1795
|
+
// Not a research thread: read the ordinary answer from the transcript
|
|
1796
|
+
// too. Recover used to be page-only, so it inherited everything the
|
|
1797
|
+
// page loses - flattened markdown, dropped citation urls - and it once
|
|
1798
|
+
// saved ChatGPT's "Connection interrupted" notice as the answer.
|
|
1799
|
+
const transcript = await evaluateOnPage(page.page, transcriptAnswerExpression(conversationId), {
|
|
1800
|
+
timeoutMs: 60_000
|
|
1801
|
+
});
|
|
1802
|
+
if (transcript.ok && transcript.text.trim().length > 0) {
|
|
1803
|
+
const recovered = resolveTranscriptCitations(transcript.text, transcript.references).trim();
|
|
1804
|
+
if (recovered.length > 0) {
|
|
1805
|
+
return {
|
|
1806
|
+
url,
|
|
1807
|
+
title: "",
|
|
1808
|
+
answer: recovered,
|
|
1809
|
+
modelHints: [],
|
|
1810
|
+
...(transcript.modelSlug ? { modelSlug: transcript.modelSlug } : {}),
|
|
1811
|
+
warnings: []
|
|
1812
|
+
};
|
|
1813
|
+
}
|
|
1814
|
+
}
|
|
1748
1815
|
}
|
|
1749
1816
|
catch {
|
|
1750
1817
|
// Not a research thread, or the transcript API is unavailable: fall
|
|
@@ -2128,12 +2195,21 @@ export async function sendChatGptPrompt(options) {
|
|
|
2128
2195
|
if (!conversationId)
|
|
2129
2196
|
throw new ChatGptBrowserBlockerError(deepResearchUnreadableBlocker(pinnedThreadUrl ?? "https://chatgpt.com/"));
|
|
2130
2197
|
let lastState;
|
|
2198
|
+
let consecutiveResearchReadFailures = 0;
|
|
2131
2199
|
while (Date.now() - started < timeoutMs) {
|
|
2132
2200
|
try {
|
|
2133
2201
|
lastState = await evaluateOnPage(page, deepResearchReportExpression(conversationId), { timeoutMs: 60_000 });
|
|
2202
|
+
consecutiveResearchReadFailures = 0;
|
|
2134
2203
|
}
|
|
2135
2204
|
catch {
|
|
2136
|
-
//
|
|
2205
|
+
// A few failures in a row mean the browser is gone, not busy. Waiting
|
|
2206
|
+
// out a 30-minute budget on a dead browser helps nobody: the research
|
|
2207
|
+
// finishes on ChatGPT's side anyway, so hand back the thread and let
|
|
2208
|
+
// recover collect the report.
|
|
2209
|
+
consecutiveResearchReadFailures += 1;
|
|
2210
|
+
if (consecutiveResearchReadFailures >= CONSECUTIVE_READ_FAILURES_BEFORE_GIVING_UP) {
|
|
2211
|
+
throw new ChatGptBrowserBlockerError(browserLostMidWaitBlocker(pinnedThreadUrl));
|
|
2212
|
+
}
|
|
2137
2213
|
await sleep(5_000);
|
|
2138
2214
|
continue;
|
|
2139
2215
|
}
|
|
@@ -2164,11 +2240,13 @@ export async function sendChatGptPrompt(options) {
|
|
|
2164
2240
|
}
|
|
2165
2241
|
let recoveredNavigations = 0;
|
|
2166
2242
|
let lastTranscriptClassification;
|
|
2243
|
+
let consecutiveReadFailures = 0;
|
|
2167
2244
|
const answerIsStable = createChatGptAnswerStabilityTracker();
|
|
2168
2245
|
while (Date.now() - started < timeoutMs) {
|
|
2169
2246
|
await sleep(1000);
|
|
2170
2247
|
try {
|
|
2171
2248
|
finalState = await evaluateOnPage(page, answerExpression());
|
|
2249
|
+
consecutiveReadFailures = 0;
|
|
2172
2250
|
// First conversation id wins. Re-deriving it every poll would let a tab
|
|
2173
2251
|
// that wandered to another thread redirect the read to a stranger's
|
|
2174
2252
|
// conversation - and the prompt check below is the second line of defence,
|
|
@@ -2206,7 +2284,13 @@ export async function sendChatGptPrompt(options) {
|
|
|
2206
2284
|
throw error;
|
|
2207
2285
|
// Transient CDP failure while the answer is streaming: retry. A throw here
|
|
2208
2286
|
// would discard an already-streamed partial answer and skip the salvage
|
|
2209
|
-
// path below, so keep the last good state and poll again
|
|
2287
|
+
// path below, so keep the last good state and poll again. But a run of
|
|
2288
|
+
// failures is a browser that went away rather than a busy one, and
|
|
2289
|
+
// sitting out the whole budget on it only delays the recovery.
|
|
2290
|
+
consecutiveReadFailures += 1;
|
|
2291
|
+
if (consecutiveReadFailures >= CONSECUTIVE_READ_FAILURES_BEFORE_GIVING_UP) {
|
|
2292
|
+
throw new ChatGptBrowserBlockerError(browserLostMidWaitBlocker(finalState?.url ?? pinnedThreadUrl));
|
|
2293
|
+
}
|
|
2210
2294
|
continue;
|
|
2211
2295
|
}
|
|
2212
2296
|
const runtimeBlocker = chatGptBlockerFromAnswerState(finalState);
|
|
@@ -3073,6 +3157,32 @@ export function chunkComposerText(text, size = COMPOSER_INSERT_CHUNK_CHARS) {
|
|
|
3073
3157
|
* so hand the run back rather than waiting on a page that never renders it -
|
|
3074
3158
|
* deep research draws into a widget iframe, leaving the thread DOM empty.
|
|
3075
3159
|
*/
|
|
3160
|
+
/**
|
|
3161
|
+
* The dedicated browser died while a send was waiting for its answer.
|
|
3162
|
+
*
|
|
3163
|
+
* Observed live: Chrome went away mid deep-research run, every poll threw, the
|
|
3164
|
+
* loop swallowed each failure, and the send sat silent for the rest of a
|
|
3165
|
+
* 30-minute budget - while the research finished on ChatGPT's side. Nothing is
|
|
3166
|
+
* lost except the ability to read it, so fail fast and hand back the thread.
|
|
3167
|
+
*/
|
|
3168
|
+
// Reading the page can fail for a moment (a navigation, a busy renderer). Five
|
|
3169
|
+
// failures in a row is not a moment - it is a browser that went away.
|
|
3170
|
+
const CONSECUTIVE_READ_FAILURES_BEFORE_GIVING_UP = 5;
|
|
3171
|
+
export function browserLostMidWaitBlocker(threadUrl) {
|
|
3172
|
+
// Killing the browser aborts a streaming answer too, so promise nothing
|
|
3173
|
+
// about the answer itself - only say where to look. Deep research is the one
|
|
3174
|
+
// case that genuinely keeps going without us.
|
|
3175
|
+
const where = threadUrl ? ` The consult landed in ${threadUrl}` : "";
|
|
3176
|
+
return {
|
|
3177
|
+
code: "browser_unreachable",
|
|
3178
|
+
message: `The dedicated ChatGPT browser stopped responding while this consult was waiting for its answer.${where}`,
|
|
3179
|
+
retryable: true,
|
|
3180
|
+
next_step: threadUrl
|
|
3181
|
+
? `Run \`prodex pro browser login\` to reopen the browser, then collect the answer with \`prodex pro browser recover --target-url ${threadUrl}\` (MCP: pro_recover with thread ${threadUrl}).`
|
|
3182
|
+
: "Run `prodex pro browser login` to reopen the browser, then retry.",
|
|
3183
|
+
...(threadUrl ? { thread: threadUrl } : {})
|
|
3184
|
+
};
|
|
3185
|
+
}
|
|
3076
3186
|
export function deepResearchUnreadableBlocker(threadUrl) {
|
|
3077
3187
|
return {
|
|
3078
3188
|
code: "deep_research_not_readable",
|
package/dist/cli-pro.js
CHANGED
|
@@ -2,7 +2,7 @@ import { existsSync, statSync } from "node:fs";
|
|
|
2
2
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { buildDryRunBundle } from "./bundle.js";
|
|
5
|
-
import { DEFAULT_CDP_PORT, resolveCdpPort, chatGptVisibilityBlocker, defaultChatGptProfileDir, formatDurationMs, getChatGptBrowserStatus, listChatGptModelOptions, listChatGptSidebarProjects, normalizeChatGptTargetUrl, openChatGptBrowser, parseProMode, parseReasoningEffort, defaultTimeoutForTools, ensureVirtualDisplay, minimizeChatGptWindow, readLastBrowserLoginLaunch, resolveVirtualDisplayPreference, resolveHeadlessPreference, recordBrowserLoginLaunch, recoverChatGptAnswerFromThread, sendChatGptPrompt } from "./chatgpt-browser.js";
|
|
5
|
+
import { DEFAULT_CDP_PORT, resolveCdpPort, chatGptVisibilityBlocker, defaultChatGptProfileDir, formatDurationMs, getChatGptBrowserStatus, listChatGptModelOptions, listChatGptSidebarProjects, normalizeChatGptTargetUrl, openChatGptBrowser, parseProMode, parseReasoningEffort, defaultTimeoutForTools, ensureVirtualDisplay, minimizeChatGptWindow, readLastBrowserLoginLaunch, resolveVirtualDisplayPreference, resolveBrowserWindowMode, resolveHeadlessPreference, recordBrowserLoginLaunch, recoverChatGptAnswerFromThread, sendChatGptPrompt } from "./chatgpt-browser.js";
|
|
6
6
|
import { ASK_PRO_BOOLEAN_FLAGS, ASK_PRO_PREVIEW_VALUE_FLAGS, ASK_PRO_VALUE_FLAGS, assertHelpRequestArgs, assertNoExtraArgs, assertOnlyOptions, findHelpFlagIndexBeforePromptDelimiter, formatCliCommand, hasAskProDryRunMode, hasAskProMode, hasAskProSendMode, isHelpSubcommand, parseAskProArgs, printHelpIfRequested, readFlag, readPortFlag, readPositionalsWithOptions, readNonNegativeIntegerFlag, readPositiveIntegerFlag, readRepeatedFlag, resolveCwdFlag, resolveOptionalFileFlag, unknownSubcommandError } from "./cli-args.js";
|
|
7
7
|
import { printProBrowserHelp, printProHelp } from "./cli-help.js";
|
|
8
8
|
import { listRawResultsForInspection, listTasksForInspection } from "./cli-ledger.js";
|
|
@@ -222,14 +222,30 @@ export async function runProCommand(rest, io, runCliFn) {
|
|
|
222
222
|
// again: Chrome's singleton would just open ANOTHER window (the recurring
|
|
223
223
|
// "extra windows" problem, which then blocks sends as
|
|
224
224
|
// ambiguous_chatgpt_tabs). Reuse the running instance instead.
|
|
225
|
-
|
|
226
|
-
// A real browser on a virtual X display: no window anywhere, and
|
|
227
|
-
// Cloudflare sees an ordinary headed Chrome (headless it rejects).
|
|
228
|
-
const wantsVirtualDisplay = resolveVirtualDisplayPreference(browserArgs.includes("--virtual-display") ? true : undefined);
|
|
229
|
-
if (wantsVirtualDisplay && headless) {
|
|
225
|
+
if (browserArgs.includes("--virtual-display") && browserArgs.includes("--headless")) {
|
|
230
226
|
throw new Error("pro browser login cannot combine --headless and --virtual-display (a virtual display already hides the window).");
|
|
231
227
|
}
|
|
232
|
-
|
|
228
|
+
// Reopen the browser the way it was last opened unless a flag or the
|
|
229
|
+
// environment says otherwise. A virtual-display user who follows the
|
|
230
|
+
// `browser_unreachable` advice used to get a visible window back.
|
|
231
|
+
const savedLaunch = await readLastBrowserLoginLaunch();
|
|
232
|
+
const windowMode = resolveBrowserWindowMode({
|
|
233
|
+
flags: {
|
|
234
|
+
...(browserArgs.includes("--headless") ? { headless: true } : {}),
|
|
235
|
+
...(browserArgs.includes("--virtual-display") ? { virtualDisplay: true } : {}),
|
|
236
|
+
...(browserArgs.includes("--minimized") ? { minimized: true } : {})
|
|
237
|
+
},
|
|
238
|
+
...(savedLaunch ? { lastLogin: savedLaunch } : {})
|
|
239
|
+
});
|
|
240
|
+
const headless = windowMode.headless;
|
|
241
|
+
// A real browser on a virtual X display: no window anywhere, and
|
|
242
|
+
// Cloudflare sees an ordinary headed Chrome (headless it rejects).
|
|
243
|
+
const wantsVirtualDisplay = windowMode.virtualDisplay;
|
|
244
|
+
const virtualDisplay = wantsVirtualDisplay
|
|
245
|
+
? await ensureVirtualDisplay(savedLaunch?.virtual_display !== undefined && !browserArgs.includes("--virtual-display")
|
|
246
|
+
? { displayNumber: savedLaunch.virtual_display }
|
|
247
|
+
: {})
|
|
248
|
+
: undefined;
|
|
233
249
|
const alreadyRunning = (await getChatGptBrowserStatus({ port })).reachable;
|
|
234
250
|
if (alreadyRunning) {
|
|
235
251
|
// One Chrome profile cannot serve a headed and a headless instance at
|