@youdie006/prodex 0.40.30 → 0.40.32
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 +1 -1
- package/dist/bundle.js +9 -1
- package/dist/chatgpt-browser.js +84 -13
- package/docs/releasing.md +77 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -93,7 +93,7 @@ Useful flags on every send:
|
|
|
93
93
|
|---|---|
|
|
94
94
|
| `--new-chat` | Explicitly request the default: ordinary consults start in a fresh chat. The shared current tab is never an implicit destination. |
|
|
95
95
|
| `--session-key id` | Identify one caller for scoped `--continue`. Falls back to `PRODEX_SESSION_KEY`, then `CODEX_THREAD_ID`. |
|
|
96
|
-
| `--file path` | Inline a text file's contents into the prompt
|
|
96
|
+
| `--file path` | Inline a text file's contents into the prompt, up to 500 lines; a longer file is refused with a pointer to `--attach`. Repeatable. |
|
|
97
97
|
| `--attach path` | Upload the file itself: the only way to hand ChatGPT a pdf, pptx, xlsx or image. Paths must live inside the repo. |
|
|
98
98
|
| `--tool web-search` | Select a rendered composer tool. Automatic deep-research report retrieval is currently unsupported and is blocked before sending. |
|
|
99
99
|
| `--project "name"` | Send inside an existing sidebar project. `--project-new` creates one first. `prodex pro browser projects` lists exact names. |
|
package/dist/bundle.js
CHANGED
|
@@ -32,6 +32,8 @@ export function looksLikeBinaryFileContent(content) {
|
|
|
32
32
|
}
|
|
33
33
|
return control / sample.length > 0.1;
|
|
34
34
|
}
|
|
35
|
+
/** The most lines `--file` puts into a prompt; longer files go through --attach. */
|
|
36
|
+
export const INLINE_FILE_MAX_LINES = 500;
|
|
35
37
|
export async function buildDryRunBundle(root, input) {
|
|
36
38
|
const sections = [
|
|
37
39
|
"# prodex consult dry run",
|
|
@@ -46,7 +48,13 @@ export async function buildDryRunBundle(root, input) {
|
|
|
46
48
|
// The prompt leads so the instruction is never buried under file dumps.
|
|
47
49
|
const sendSections = [input.prompt.trim()];
|
|
48
50
|
for (const file of input.files) {
|
|
49
|
-
const content = await readRepoFile(root, file, { maxLines:
|
|
51
|
+
const content = await readRepoFile(root, file, { maxLines: INLINE_FILE_MAX_LINES });
|
|
52
|
+
// Sending only the first lines without saying so made ChatGPT answer
|
|
53
|
+
// confidently about a file it never saw whole (measured 2026-10-13).
|
|
54
|
+
if (content.total_lines > content.end_line) {
|
|
55
|
+
throw new Error(`--file ${file} has ${content.total_lines} lines; --file inlines at most ${INLINE_FILE_MAX_LINES}. ` +
|
|
56
|
+
`Upload the whole file instead: --attach ${file}.`);
|
|
57
|
+
}
|
|
50
58
|
if (looksLikeBinaryFileContent(content.content)) {
|
|
51
59
|
throw new Error(`--file ${file} is not a text file, and --file inlines a file's TEXT into the prompt. ` +
|
|
52
60
|
`Upload it instead: --attach ${file}.`);
|
package/dist/chatgpt-browser.js
CHANGED
|
@@ -256,6 +256,9 @@ export const CHATGPT_RENDERED_MESSAGE_TEXT_JS = `
|
|
|
256
256
|
const isBlock = (node) => node.nodeType === 1 && !skipped(node) &&
|
|
257
257
|
(BLOCK_TAGS.includes(tagOf(node)) || isCodeBlock(node) || (isMath(node) && isDisplayMath(node)) ||
|
|
258
258
|
(tagOf(node) === "span" && findAll(node, (n) => BLOCK_TAGS.includes(tagOf(n))).length > 0));
|
|
259
|
+
// The renderer web-search answers use since 2026-10-10 marks a source as a
|
|
260
|
+
// badge inside a popover trigger; its urls only exist once it is opened.
|
|
261
|
+
const isSourceBadge = (node) => attr(node, "data-d-component") === "popover-trigger" && findAll(node, (n) => attr(n, "data-pill") !== null).length > 0;
|
|
259
262
|
const pill = (node) => {
|
|
260
263
|
const label = collapse(node.textContent || "").trim();
|
|
261
264
|
if (!label) return "";
|
|
@@ -268,7 +271,7 @@ export const CHATGPT_RENDERED_MESSAGE_TEXT_JS = `
|
|
|
268
271
|
const piece = inlineNode(child);
|
|
269
272
|
if (!piece) continue;
|
|
270
273
|
// A source pill sits right after its sentence; keep one space before it.
|
|
271
|
-
if (child.nodeType === 1 && attr(child, "data-chatgpt-copy-reference") !== null && out && !isSpaceChar(out[out.length - 1])) out += " ";
|
|
274
|
+
if (child.nodeType === 1 && (attr(child, "data-chatgpt-copy-reference") !== null || isSourceBadge(child)) && out && !isSpaceChar(out[out.length - 1])) out += " ";
|
|
272
275
|
out += piece;
|
|
273
276
|
}
|
|
274
277
|
return out;
|
|
@@ -280,11 +283,12 @@ export const CHATGPT_RENDERED_MESSAGE_TEXT_JS = `
|
|
|
280
283
|
// A task list item's box (measured: button[role=checkbox][aria-checked]).
|
|
281
284
|
if (isCheckbox(node)) return (attr(node, "aria-checked") === "true" || attr(node, "checked") !== null ? "[x]" : "[ ]") + " ";
|
|
282
285
|
if (isMath(node)) return isDisplayMath(node) ? "$$" + mathSource(node) + "$$" : "$" + mathSource(node) + "$";
|
|
283
|
-
if (attr(node, "data-chatgpt-copy-reference") !== null) return pill(node);
|
|
286
|
+
if (attr(node, "data-chatgpt-copy-reference") !== null || isSourceBadge(node)) return pill(node);
|
|
284
287
|
if (attr(node, "data-markdown-copy") === "inline-code" || tag === "code") return BT + (node.textContent || "") + BT;
|
|
285
288
|
if (tag === "br") return NL;
|
|
286
289
|
const inner = inline(node);
|
|
287
|
-
|
|
290
|
+
// The newer renderer marks bold as span[data-d-default-strong].
|
|
291
|
+
if (tag === "strong" || tag === "b" || attr(node, "data-d-default-strong") !== null) return inner.trim() ? "**" + inner.trim() + "**" : "";
|
|
288
292
|
if (tag === "em" || tag === "i") return inner.trim() ? "*" + inner.trim() + "*" : "";
|
|
289
293
|
if (tag === "del" || tag === "s") return inner.trim() ? "~~" + inner.trim() + "~~" : "";
|
|
290
294
|
if (tag === "a") {
|
|
@@ -428,7 +432,21 @@ export const CHATGPT_MODEL_HINTS_JS = `
|
|
|
428
432
|
}
|
|
429
433
|
return seen.slice(0, 30);
|
|
430
434
|
};`;
|
|
431
|
-
|
|
435
|
+
/**
|
|
436
|
+
* ChatGPT collapses a long user turn behind "Show more" and leaves the rest
|
|
437
|
+
* out of the DOM (measured 2026-10-13: 10,250 of 12,095 characters, the request
|
|
438
|
+
* marker among the missing). Clicking it only changes what is displayed, and
|
|
439
|
+
* the full text renders after the click returns, so the caller reads again.
|
|
440
|
+
*/
|
|
441
|
+
export const CHATGPT_EXPAND_USER_TURN_JS = `
|
|
442
|
+
const expandCollapsedUserTurn = (node) => {
|
|
443
|
+
if (!node || typeof node.querySelector !== 'function') return false;
|
|
444
|
+
const more = node.querySelector('button[aria-expanded="false"]');
|
|
445
|
+
if (!more) return false;
|
|
446
|
+
more.click();
|
|
447
|
+
return true;
|
|
448
|
+
};`;
|
|
449
|
+
const CHATGPT_MESSAGE_NODES_JS = `${CHATGPT_RENDERED_MESSAGE_TEXT_JS}${CHATGPT_EXPAND_USER_TURN_JS}
|
|
432
450
|
// ChatGPT keeps a previously opened conversation mounted but hidden
|
|
433
451
|
// (measured 2026-10-06: two turns without a box on a project home), and
|
|
434
452
|
// counting them made a fresh chat look like an existing conversation.
|
|
@@ -3675,6 +3693,7 @@ export async function recoverChatGptAnswerFromThread(options) {
|
|
|
3675
3693
|
let state;
|
|
3676
3694
|
let generating = false;
|
|
3677
3695
|
let stableRuns = 0;
|
|
3696
|
+
let expandingPolls = 0;
|
|
3678
3697
|
let lastAnswer = "";
|
|
3679
3698
|
let lastObservedUrl = "";
|
|
3680
3699
|
let completed = false;
|
|
@@ -3715,6 +3734,12 @@ export async function recoverChatGptAnswerFromThread(options) {
|
|
|
3715
3734
|
lastAnswer = "";
|
|
3716
3735
|
continue;
|
|
3717
3736
|
}
|
|
3737
|
+
if (chatGptUserTurnStillExpanding(state, expandingPolls)) {
|
|
3738
|
+
expandingPolls += 1;
|
|
3739
|
+
stableRuns = 0;
|
|
3740
|
+
lastAnswer = "";
|
|
3741
|
+
continue;
|
|
3742
|
+
}
|
|
3718
3743
|
if (options.requestId && !chatGptRequestMarkerMatches(state.lastUserText ?? "", options.requestId)) {
|
|
3719
3744
|
throw new ChatGptBrowserBlockerError({
|
|
3720
3745
|
code: "request_mismatch",
|
|
@@ -4184,6 +4209,7 @@ export async function sendChatGptPrompt(options) {
|
|
|
4184
4209
|
// prompt posts the composer clears and no send button is found.
|
|
4185
4210
|
const promptPostedExpression = `(() => {${CHATGPT_MESSAGE_NODES_JS}
|
|
4186
4211
|
const last = chatMessageNodes().filter((message) => message.role === "user").at(-1);
|
|
4212
|
+
if (last && expandCollapsedUserTurn(last.textNode)) return false;
|
|
4187
4213
|
return Boolean(last && userMessageText(last.textNode).includes(${JSON.stringify(`[prodex-request:${requestId}]`)}));
|
|
4188
4214
|
})()`;
|
|
4189
4215
|
// A dispatch can reach Chrome even if its acknowledgement is lost.
|
|
@@ -4228,6 +4254,7 @@ export async function sendChatGptPrompt(options) {
|
|
|
4228
4254
|
const acceptDeadline = computePromptAcceptanceDeadline(timeoutMs, started);
|
|
4229
4255
|
let accepted = false;
|
|
4230
4256
|
let finalState;
|
|
4257
|
+
let expandingPolls = 0;
|
|
4231
4258
|
while (Date.now() < acceptDeadline) {
|
|
4232
4259
|
await sleep(500);
|
|
4233
4260
|
try {
|
|
@@ -4250,6 +4277,10 @@ export async function sendChatGptPrompt(options) {
|
|
|
4250
4277
|
accepted = true;
|
|
4251
4278
|
break;
|
|
4252
4279
|
}
|
|
4280
|
+
if (chatGptUserTurnStillExpanding(finalState, expandingPolls)) {
|
|
4281
|
+
expandingPolls += 1;
|
|
4282
|
+
continue;
|
|
4283
|
+
}
|
|
4253
4284
|
if (hasChatGptPromptAcceptance(beforeSubmit, finalState))
|
|
4254
4285
|
throw requestMismatch(normalizedTargetUrl);
|
|
4255
4286
|
emitProgress("waiting", "prompt posting");
|
|
@@ -4291,6 +4322,7 @@ export async function sendChatGptPrompt(options) {
|
|
|
4291
4322
|
: undefined;
|
|
4292
4323
|
let consecutiveReadFailures = 0;
|
|
4293
4324
|
let answerSettled = false;
|
|
4325
|
+
expandingPolls = 0;
|
|
4294
4326
|
let nonTextPolls = 0;
|
|
4295
4327
|
let nonTextSettled = false;
|
|
4296
4328
|
const answerIsStable = createChatGptAnswerStabilityTracker();
|
|
@@ -4299,6 +4331,10 @@ export async function sendChatGptPrompt(options) {
|
|
|
4299
4331
|
try {
|
|
4300
4332
|
const observedState = await evaluateOnPage(page, answerExpression());
|
|
4301
4333
|
consecutiveReadFailures = 0;
|
|
4334
|
+
if (chatGptUserTurnStillExpanding(observedState, expandingPolls)) {
|
|
4335
|
+
expandingPolls += 1;
|
|
4336
|
+
continue;
|
|
4337
|
+
}
|
|
4302
4338
|
// Freeze the first conversation identity the accepted page exposes. A
|
|
4303
4339
|
// later tab move must never rewrite result metadata to another thread.
|
|
4304
4340
|
if (!pinnedConversationId) {
|
|
@@ -5901,16 +5937,15 @@ async function insertComposerTextViaCdp(cdp, text, page, toolLabels = []) {
|
|
|
5901
5937
|
// Both failures come from crossing the CDP boundary mid-edit, so large text
|
|
5902
5938
|
// is inserted by a single in-page execCommand instead: the editor applies it
|
|
5903
5939
|
// as one input event and nothing can interleave. Measured: 67 KB inserted in
|
|
5904
|
-
// ~20s, verified byte-for-byte.
|
|
5940
|
+
// ~20s, verified byte-for-byte; 57 KB took 63-126 s on 2026-10-13.
|
|
5905
5941
|
if (text.length <= COMPOSER_INSERT_CHUNK_CHARS) {
|
|
5906
5942
|
await cdp.send("Input.insertText", { text });
|
|
5907
5943
|
}
|
|
5908
5944
|
else {
|
|
5909
|
-
// A
|
|
5910
|
-
//
|
|
5911
|
-
//
|
|
5912
|
-
|
|
5913
|
-
const budgetMs = Math.max(60_000, Math.ceil(text.length / 1_000) * 1_000);
|
|
5945
|
+
// A large insert outlasts the default 20s CDP command budget, so this one
|
|
5946
|
+
// call gets its own connection with a size-scaled budget instead of
|
|
5947
|
+
// loosening the budget for every command on the shared connection.
|
|
5948
|
+
const budgetMs = composerInsertBudgetMs(text.length);
|
|
5914
5949
|
const slowCdp = page ? await connectCdp(page.webSocketDebuggerUrl, budgetMs) : undefined;
|
|
5915
5950
|
try {
|
|
5916
5951
|
const target = slowCdp ?? cdp;
|
|
@@ -5930,6 +5965,16 @@ async function insertComposerTextViaCdp(cdp, text, page, toolLabels = []) {
|
|
|
5930
5965
|
throw new Error(state.reason ?? "Composer stayed empty after text insertion");
|
|
5931
5966
|
}
|
|
5932
5967
|
export const COMPOSER_INSERT_CHUNK_CHARS = 4_000;
|
|
5968
|
+
/**
|
|
5969
|
+
* How long one in-page insert of `chars` characters may take. Measured
|
|
5970
|
+
* 2026-10-13: 10 s for 15 KB, 34 s for 30 KB, and 63 s then 126 s for the same
|
|
5971
|
+
* 57 KB - far slower than when this path was built (67 KB in ~20 s), and the
|
|
5972
|
+
* old one-second-per-KB budget cut a 57 KB prompt off. 5 ms per character
|
|
5973
|
+
* covers the slowest measured run with room to spare.
|
|
5974
|
+
*/
|
|
5975
|
+
export function composerInsertBudgetMs(chars) {
|
|
5976
|
+
return Math.max(60_000, chars * 5);
|
|
5977
|
+
}
|
|
5933
5978
|
/**
|
|
5934
5979
|
* Split composer text into insertText-sized pieces. Splits on code points (not
|
|
5935
5980
|
* UTF-16 units) so a surrogate pair - an emoji, or any astral character - can
|
|
@@ -6057,10 +6102,34 @@ function chatGptRequestMarkerMatches(userText, requestId) {
|
|
|
6057
6102
|
const markers = [...normalizeChatGptPromptText(userText).matchAll(/\[prodex-request:([a-f0-9]{32})\]/g)];
|
|
6058
6103
|
return markers.at(-1)?.[1] === requestId && markers.filter((match) => match[1] === requestId).length === 1;
|
|
6059
6104
|
}
|
|
6105
|
+
/**
|
|
6106
|
+
* A collapsed user turn is not judged until its expanded text has rendered.
|
|
6107
|
+
* Bounded, so a turn that collapses again on every render is judged in the end
|
|
6108
|
+
* instead of holding the send until its timeout.
|
|
6109
|
+
*/
|
|
6110
|
+
export function chatGptUserTurnStillExpanding(state, skippedPolls = 0) {
|
|
6111
|
+
return state.lastUserCollapsed === true && skippedPolls < USER_TURN_EXPAND_MAX_POLLS;
|
|
6112
|
+
}
|
|
6113
|
+
export const USER_TURN_EXPAND_MAX_POLLS = 10;
|
|
6060
6114
|
/** Full prompt and per-send identity; wrappers may contain tool/file labels. */
|
|
6061
6115
|
export function chatGptRequestMatchesUserTurn(userText, sentPrompt, requestId) {
|
|
6062
|
-
|
|
6063
|
-
|
|
6116
|
+
if (!chatGptRequestMarkerMatches(userText, requestId))
|
|
6117
|
+
return false;
|
|
6118
|
+
if (normalizeChatGptPromptText(userText).includes(normalizeChatGptPromptText(sentPrompt)))
|
|
6119
|
+
return true;
|
|
6120
|
+
// The container's ChatGPT renders a sent turn as markdown since 2026-10-10:
|
|
6121
|
+
// a --file prompt's fence lines disappear and the inner fences shift, so
|
|
6122
|
+
// every such send posted and was then refused. Fence lines carry no request
|
|
6123
|
+
// text; everything else, punctuation and inline backticks included, must
|
|
6124
|
+
// still match in order.
|
|
6125
|
+
return withoutFenceLines(userText).includes(withoutFenceLines(sentPrompt));
|
|
6126
|
+
}
|
|
6127
|
+
function withoutFenceLines(value) {
|
|
6128
|
+
return value
|
|
6129
|
+
.replace(AUTOLINKED_URL_PATTERN, "$1")
|
|
6130
|
+
.replace(/\\([\\`*_{}[\]()#+\-.!>~|])/g, "$1")
|
|
6131
|
+
.replace(/^[ \t]*```[\w-]*[ \t]*$/gm, "")
|
|
6132
|
+
.replace(/\s+/g, "");
|
|
6064
6133
|
}
|
|
6065
6134
|
/**
|
|
6066
6135
|
* Does this transcript belong to the consult that is waiting on it?
|
|
@@ -6351,10 +6420,11 @@ export function answerExpression() {
|
|
|
6351
6420
|
if (tagged) modelSlug = tagged.getAttribute('data-message-model-slug') || undefined;
|
|
6352
6421
|
}
|
|
6353
6422
|
// User turns stay as typed: the request marker is matched against them.
|
|
6354
|
-
return { role, node, text: role === "assistant" ? renderedMessageText(textNode) : userMessageText(textNode), modelSlug };
|
|
6423
|
+
return { role, node, textNode, text: role === "assistant" ? renderedMessageText(textNode) : userMessageText(textNode), modelSlug };
|
|
6355
6424
|
});
|
|
6356
6425
|
const assistantMessages = messages.filter((message) => message.role === "assistant");
|
|
6357
6426
|
const userMessages = messages.filter((message) => message.role === "user");
|
|
6427
|
+
const lastUserCollapsed = expandCollapsedUserTurn(userMessages.at(-1)?.textNode);
|
|
6358
6428
|
// A turn without an assistant message is NOT an answer. Treating one as an
|
|
6359
6429
|
// answer (a 0.21.3 fallback for deep research, which is read from the
|
|
6360
6430
|
// transcript now) turned a tool's progress panel into a 28-character
|
|
@@ -6388,6 +6458,7 @@ export function answerExpression() {
|
|
|
6388
6458
|
assistantMessageCount: assistantMessages.length,
|
|
6389
6459
|
userMessageCount: userMessages.length,
|
|
6390
6460
|
lastUserText: userMessages.at(-1)?.text || "",
|
|
6461
|
+
lastUserCollapsed,
|
|
6391
6462
|
// An image result is rendered outside every message unit and leaves no
|
|
6392
6463
|
// assistant message at all (measured 2026-09-29), so it is reported here
|
|
6393
6464
|
// for the waiter to recognize: a generated image that comes after the
|
package/docs/releasing.md
CHANGED
|
@@ -16,12 +16,85 @@ Release target: `v0.40.13` on `main`. The GitHub Release records the resolved co
|
|
|
16
16
|
|
|
17
17
|
The release adds a guarded, opt-in transition and more accurate blocker reporting. It does not claim that headless Pro consultation is verified on either deployment target or that authentication can be retained indefinitely.
|
|
18
18
|
|
|
19
|
+
## Browser Incident 2026-10-10: earlyoom
|
|
20
|
+
|
|
21
|
+
**What happened**
|
|
22
|
+
- The host's dedicated browser (port 9333) stopped answering twice: around 11:49 and at 17:45 KST. The browser container exited at 11:49 KST (exit 1, `chromium exited (SIGTRAP)`, restart policy `no`) and stayed down.
|
|
23
|
+
- The hourly canary logged `skipped - the browser is not available` from 18:17 KST on, and filed nothing.
|
|
24
|
+
|
|
25
|
+
**Cause**
|
|
26
|
+
- The machine's `earlyoom` (override from 2026-08-14: `-m 15 -s 40`) sent SIGTERM to Chrome and Chromium processes: 25 between 11:45 and 11:49, 17 at 17:45. It acts when available memory is under 15% and free swap under 40%. Swap was 88% used (1 GiB free of 8), so every dip under 15% memory picked a process, and Chrome's processes carried the highest badness (800-876).
|
|
27
|
+
- Earlier days show the same: 248 Chrome kills on 2026-10-08 (09:00-11:59 KST), which likely explains that day's browser losses.
|
|
28
|
+
- At 19:03 earlyoom also killed `chrome-headless`, `swapdex`, `ssh` and `dbus-daemon` processes, so the pressure is machine-wide, not prodex's.
|
|
29
|
+
|
|
30
|
+
**Recovery**
|
|
31
|
+
- Host browser restarted once on the Xvfb display (`login --virtual-display`, logged in, 4 s); the container started from the same image `547e852632b5` and its tab reopened (2 s). The canary from `12aab1e` (PR #24, re-read before reporting broken) then reported `ok` on build `314720d0`.
|
|
32
|
+
|
|
33
|
+
**Not changed**
|
|
34
|
+
- The earlyoom configuration and the container restart policy. Both are the maintainer's machine-level choices.
|
|
35
|
+
|
|
36
|
+
## 0.40.31 Release Record
|
|
37
|
+
|
|
38
|
+
Release commit `0b10dd1` on `main` (cut after PR #23, merge `d5296e0`); tag `v0.40.31`.
|
|
39
|
+
|
|
40
|
+
**Publishing**
|
|
41
|
+
- `publish.yml` run 38019182789 passed all six platforms on the first attempt, then npm publish with SLSA v1 provenance and the GitHub Release (2026-10-10 03:03 UTC).
|
|
42
|
+
- `npm run release:verify` passed on the release commit after a build.
|
|
43
|
+
|
|
44
|
+
**Installed**
|
|
45
|
+
- WSL, M3, Mac mini: `prodex --version` reports 0.40.31. Running services were not restarted: 4 stdio MCP servers on WSL and 4 on M3 keep older code until their clients reconnect. The Mac mini had none running.
|
|
46
|
+
- The maintainer watchdog checkout (`~/.local/share/prodex-maint/repo`) was pulled to `0b10dd1` and rebuilt, so the hourly canary now uses the live build check and the renderer markers.
|
|
47
|
+
|
|
48
|
+
**Verified live**
|
|
49
|
+
- Host 0.40.31: a Markdown `--file` new-chat send answered (`opal-5`).
|
|
50
|
+
- The host's dedicated browser had stopped answering on port 9333 after about 11:18 KST; it was restarted once on the Xvfb display (`login --virtual-display`, logged in, 5 s). Cause not established.
|
|
51
|
+
- The canary's first run right after that restart reported `broken: model selector`, because the composer had not rendered yet; the next run was `ok` on build `314720d0`. The model picker lists GPT-6, GPT-5.6 Sol and GPT-5.5.
|
|
52
|
+
|
|
53
|
+
## Browser Container Rebuild 2026-10-10
|
|
54
|
+
|
|
55
|
+
**Patches**
|
|
56
|
+
- `container/redesign-patches` `5047ef9` ports 0.40.28-0.40.30 and PR #23 (newer answer renderer, sent turn drawn as markdown) onto the container branch. It is based on `b5eac7d`; `feat/headless-browser-compatibility` itself is unchanged.
|
|
57
|
+
|
|
58
|
+
**Image**
|
|
59
|
+
- `547e852632b5`, built from `5047ef9`; the branch suite passed (2027 passed, 3 skipped).
|
|
60
|
+
- Both swaps were gated on the send lock being free. After the swap the browser had no ChatGPT tab, so `prodex pro browser login --wait` reopened it (logged in, 3 s). MCP clients of the container need `/mcp` reconnects.
|
|
61
|
+
|
|
62
|
+
**Verified live**
|
|
63
|
+
- Image `9b51e0639a37` (the port without the fence fix): web search with bold and `[GitHub+1]` badges, continued and temporary attach, recover, models (the picker now lists GPT-6), smoke in a new chat, `pro list` in 5 s. A Markdown `--file` send posted but was refused as unverified: the container's ChatGPT drew the sent turn as markdown and dropped the ```` ```text ```` fence line.
|
|
64
|
+
- Image `547e852632b5`: the same `--file` send answered 2/2, and the smoke answered `PRODEX_PRO_SMOKE_OK`.
|
|
65
|
+
|
|
66
|
+
**Not a bug**
|
|
67
|
+
- A host 0.40.30 `--file` run failed during this check only because another send held the browser lock past its wait budget; the next run answered.
|
|
68
|
+
|
|
69
|
+
## 0.40.30 Release Record
|
|
70
|
+
|
|
71
|
+
Release commit `e566efc` on `main` (cut after PR #22, merge `c0964f2`, plus the docs commit `09e80bc`); tag `v0.40.30`.
|
|
72
|
+
|
|
73
|
+
**Publishing**
|
|
74
|
+
- `publish.yml` run 37798469373 passed all six platforms on the first attempt, then npm publish with SLSA v1 provenance and the GitHub Release (2026-10-08 15:32 UTC).
|
|
75
|
+
- `npm run release:verify` passed on the release commit after a build.
|
|
76
|
+
|
|
77
|
+
**What it fixes**
|
|
78
|
+
- `pro browser smoke` always uses a fresh chat instead of whatever conversation was open.
|
|
79
|
+
- Its default budget is 5 minutes instead of 90 s.
|
|
80
|
+
- The page canary and the watchdog changes from the same PR are maintainer tooling and are not in the package. See Maintainer Watchdog.
|
|
81
|
+
|
|
82
|
+
**Installation on 2026-10-09**
|
|
83
|
+
- Global npm with `--prefer-online`, each verified with `prodex --version` = 0.40.30.
|
|
84
|
+
- WSL, M3, Mac mini: installed. Running services were not restarted: 21 stdio MCP servers on WSL and 10 on M3 keep older code until their clients reconnect. The Mac mini had none running.
|
|
85
|
+
- M4: not reachable (hostname does not resolve); not updated.
|
|
86
|
+
|
|
87
|
+
**Live check after install (installed 0.40.30, shared WSL browser)**
|
|
88
|
+
- With the tab on an existing conversation, `pro browser smoke` answered `PRODEX_PRO_SMOKE_OK` in a new conversation, not the open one.
|
|
89
|
+
- A new-chat attachment answered.
|
|
90
|
+
- The hourly watchdog had already logged `canary: ok` runs for build `4c511f80`.
|
|
91
|
+
|
|
19
92
|
## Maintainer Watchdog
|
|
20
93
|
|
|
21
94
|
On the maintainer's WSL machine, cron runs the UI watchdog every hour (since 2026-10-08):
|
|
22
95
|
|
|
23
96
|
```
|
|
24
|
-
17 * * * * PATH=<node bin>:/usr/bin:/bin PRODEX_CLI=~/.local/share/prodex-maint/prodex PRODEX_WATCHDOG_CWD=/mnt/d/MyProject/gptprouse <node> ~/.local/share/prodex-maint/repo/scripts/ui-watchdog.mjs --canary --file-issue >> ~/.local/state/prodex-watchdog.log 2>&1
|
|
97
|
+
17 * * * * PATH=<node bin>:/usr/bin:/bin PRODEX_CLI=~/.local/share/prodex-maint/prodex PRODEX_WATCHDOG_CWD=/mnt/d/MyProject/gptprouse <node> ~/.local/share/prodex-maint/repo/scripts/ui-watchdog.mjs --canary --revive --file-issue >> ~/.local/state/prodex-watchdog.log 2>&1
|
|
25
98
|
```
|
|
26
99
|
|
|
27
100
|
**What each run does**
|
|
@@ -29,8 +102,10 @@ On the maintainer's WSL machine, cron runs the UI watchdog every hour (since 202
|
|
|
29
102
|
- It records ChatGPT's build id and whether each control prodex relies on is present.
|
|
30
103
|
- It compares with `~/.local/share/prodex/page-canary.json`.
|
|
31
104
|
- It skips while a consult holds the send lock.
|
|
105
|
+
- A broken read is read again up to twice, 10 s apart, and counts only if it stays broken. Right after a browser restart on 2026-10-10 the first read found no model selector because the composer had not rendered yet.
|
|
32
106
|
- Only when the canary reports changed (3) or broken (2) does the watchdog run `pro browser smoke` (a fresh chat, 5-minute budget). If that fails, it files or extends a GitHub issue by blocker code.
|
|
33
107
|
- A busy browser is logged as skipped and never filed.
|
|
108
|
+
- With `--revive` (since 2026-10-10), a canary that finds the dedicated browser gone, or without a ChatGPT tab, makes the watchdog run `pro browser login --virtual-display --wait` once and read the page again. It does this at most once every 6 hours (`~/.local/state/prodex-maint/revive.json`), so a machine short of memory does not get a new Chrome every hour. A consult holding the lock, or a tab that stopped answering, is never touched. This came from the earlyoom incident below, where the browser stayed down for hours.
|
|
34
109
|
|
|
35
110
|
**Where it runs from**
|
|
36
111
|
- `~/.local/share/prodex-maint/repo` is a clone of `main`, built with `npm ci && npm run build`.
|
|
@@ -45,6 +120,7 @@ On the maintainer's WSL machine, cron runs the UI watchdog every hour (since 202
|
|
|
45
120
|
- It timed out at 90 s on high efforts (issue #4).
|
|
46
121
|
- It filed a busy browser as broken (issue #20, closed as a false positive).
|
|
47
122
|
- The crontab before the change is saved at `~/.local/state/prodex-maint/crontab.backup-20261008`.
|
|
123
|
+
- `--revive` was added to the cron line on 2026-10-11 after PR #25 (merge `9c9a1b4`); the checkout was pulled to that merge and rebuilt, and one manual run was `ok`. The crontab before that edit is saved at `~/.local/state/prodex-maint/crontab.backup-20261011`.
|
|
48
124
|
|
|
49
125
|
## 0.40.29 Release Record
|
|
50
126
|
|