@youdie006/prodex 0.40.46 → 0.40.48
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 +115 -29
- package/docs/releasing.md +41 -0
- package/package.json +1 -1
package/dist/chatgpt-browser.js
CHANGED
|
@@ -315,11 +315,32 @@ export const CHATGPT_RENDERED_MESSAGE_TEXT_JS = `
|
|
|
315
315
|
// The renderer web-search answers use since 2026-10-10 marks a source as a
|
|
316
316
|
// badge inside a popover trigger; its urls only exist once it is opened.
|
|
317
317
|
const isSourceBadge = (node) => attr(node, "data-d-component") === "popover-trigger" && findAll(node, (n) => attr(n, "data-pill") !== null).length > 0;
|
|
318
|
+
// ChatGPT's link component appends utm_source=chatgpt.com to every link
|
|
319
|
+
// it renders (measured 2026-10-28), a parameter the answer never wrote.
|
|
320
|
+
const untracked = (href) => {
|
|
321
|
+
const hashAt = href.indexOf("#");
|
|
322
|
+
const base = hashAt === -1 ? href : href.slice(0, hashAt);
|
|
323
|
+
const queryAt = base.indexOf("?");
|
|
324
|
+
if (queryAt === -1) return href;
|
|
325
|
+
const params = base.slice(queryAt + 1).split("&").filter((param) => param !== "utm_source=chatgpt.com");
|
|
326
|
+
return base.slice(0, queryAt) + (params.length > 0 ? "?" + params.join("&") : "") + (hashAt === -1 ? "" : href.slice(hashAt));
|
|
327
|
+
};
|
|
318
328
|
const pill = (node) => {
|
|
319
329
|
const label = collapse(node.textContent || "").trim();
|
|
320
330
|
if (!label) return "";
|
|
321
331
|
const link = findAll(node, (n) => tagOf(n) === "a" && (attr(n, "href") || "").startsWith("http"))[0];
|
|
322
|
-
return link ? "[" + label + "](" + attr(link, "href") + ")" : "[" + label + "]";
|
|
332
|
+
return link ? "[" + label + "](" + untracked(attr(link, "href")) + ")" : "[" + label + "]";
|
|
333
|
+
};
|
|
334
|
+
// Inline code goes in a backtick run longer than any inside it, padded
|
|
335
|
+
// with a space when it starts or ends with a backtick, or has a space at
|
|
336
|
+
// both ends that markdown would strip. One backtick each side turned the
|
|
337
|
+
// code a\`b into a one-letter span and a stray backtick (measured
|
|
338
|
+
// 2026-10-29).
|
|
339
|
+
const inlineCode = (value) => {
|
|
340
|
+
const longestRun = (value.match(new RegExp(BT + "+", "g")) || []).reduce((most, run) => Math.max(most, run.length), 0);
|
|
341
|
+
const ticks = BT.repeat(longestRun + 1);
|
|
342
|
+
const pad = value.startsWith(BT) || value.endsWith(BT) || (value.startsWith(" ") && value.endsWith(" ") && value.trim() !== "") ? " " : "";
|
|
343
|
+
return ticks + pad + value + pad + ticks;
|
|
323
344
|
};
|
|
324
345
|
const inline = (node, keep) => {
|
|
325
346
|
const keepLines = keepsLineBreaks(node, keep === true);
|
|
@@ -341,16 +362,28 @@ export const CHATGPT_RENDERED_MESSAGE_TEXT_JS = `
|
|
|
341
362
|
if (isCheckbox(node)) return (attr(node, "aria-checked") === "true" || attr(node, "checked") !== null ? "[x]" : "[ ]") + " ";
|
|
342
363
|
if (isMath(node)) return isDisplayMath(node) ? "$$" + mathSource(node) + "$$" : "$" + mathSource(node) + "$";
|
|
343
364
|
if (attr(node, "data-chatgpt-copy-reference") !== null || isSourceBadge(node)) return pill(node);
|
|
344
|
-
if (attr(node, "data-markdown-copy") === "inline-code" || tag === "code") return
|
|
365
|
+
if (attr(node, "data-markdown-copy") === "inline-code" || tag === "code") return inlineCode(node.textContent || "");
|
|
345
366
|
if (tag === "br") return NL;
|
|
346
367
|
const inner = inline(node, keep);
|
|
347
|
-
// The newer renderer marks bold as span[data-d-default-strong]
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
368
|
+
// The newer renderer marks bold as span[data-d-default-strong], italic
|
|
369
|
+
// as span[data-d-font-style=italic] and strikethrough as
|
|
370
|
+
// span[data-d-text-decoration=line-through] (measured 2026-10-28; only
|
|
371
|
+
// bold was read, so the other two came back as plain words). One
|
|
372
|
+
// element can carry several, so each applies in turn.
|
|
373
|
+
const strong = tag === "strong" || tag === "b" || attr(node, "data-d-default-strong") !== null;
|
|
374
|
+
const italic = tag === "em" || tag === "i" || attr(node, "data-d-font-style") === "italic";
|
|
375
|
+
const struck = tag === "del" || tag === "s" || (attr(node, "data-d-text-decoration") || "").includes("line-through");
|
|
376
|
+
if (strong || italic || struck) {
|
|
377
|
+
let marked = inner.trim();
|
|
378
|
+
if (!marked) return "";
|
|
379
|
+
if (strong) marked = "**" + marked + "**";
|
|
380
|
+
if (italic) marked = "*" + marked + "*";
|
|
381
|
+
if (struck) marked = "~~" + marked + "~~";
|
|
382
|
+
return marked;
|
|
383
|
+
}
|
|
351
384
|
if (tag === "a") {
|
|
352
385
|
const href = attr(node, "href") || "";
|
|
353
|
-
return href.startsWith("http") && inner.trim() ? "[" + inner.trim() + "](" + href + ")" : inner;
|
|
386
|
+
return href.startsWith("http") && inner.trim() ? "[" + inner.trim() + "](" + untracked(href) + ")" : inner;
|
|
354
387
|
}
|
|
355
388
|
return inner;
|
|
356
389
|
};
|
|
@@ -384,16 +417,23 @@ export const CHATGPT_RENDERED_MESSAGE_TEXT_JS = `
|
|
|
384
417
|
};
|
|
385
418
|
const table = (node) => {
|
|
386
419
|
const cell = (value) => collapse(value).trim().split("|").join(String.fromCharCode(92) + "|");
|
|
387
|
-
const
|
|
420
|
+
const cellNodes = findAll(node, (n) => tagOf(n) === "tr").map((row) => children(row).filter((c) => c.nodeType === 1));
|
|
421
|
+
const rows = cellNodes.map((cells) => cells.map((c) => cell(inline(c))));
|
|
388
422
|
if (rows.length === 0) return "";
|
|
423
|
+
// The newer renderer marks a column written :---, :---: or ---: with
|
|
424
|
+
// data-d-align start, center or end on its cells, and a plain column
|
|
425
|
+
// with none (measured 2026-10-29); every column came back as ---.
|
|
426
|
+
const rule = (c) => {
|
|
427
|
+
const align = attr(c, "data-d-align");
|
|
428
|
+
return align === "start" ? ":---" : align === "center" ? ":---:" : align === "end" ? "---:" : "---";
|
|
429
|
+
};
|
|
389
430
|
const line = (cells) => "| " + cells.join(" | ") + " |";
|
|
390
|
-
return [line(rows[0]), line(
|
|
431
|
+
return [line(rows[0]), line(cellNodes[0].map(rule)), ...rows.slice(1).map(line)].join(NL);
|
|
391
432
|
};
|
|
392
|
-
const list = (node,
|
|
433
|
+
const list = (node, keep) => {
|
|
393
434
|
const ordered = tagOf(node) === "ol";
|
|
394
435
|
let number = parseInt(attr(node, "start") || "1", 10);
|
|
395
436
|
if (!Number.isFinite(number)) number = 1;
|
|
396
|
-
const indent = " ".repeat(depth);
|
|
397
437
|
const items = [];
|
|
398
438
|
for (const item of children(node)) {
|
|
399
439
|
if (item.nodeType !== 1 || tagOf(item) !== "li") continue;
|
|
@@ -405,16 +445,25 @@ export const CHATGPT_RENDERED_MESSAGE_TEXT_JS = `
|
|
|
405
445
|
const keepItemLines = keepsLineBreaks(item, keep === true);
|
|
406
446
|
for (const child of children(item)) {
|
|
407
447
|
const tag = child.nodeType === 1 ? tagOf(child) : "";
|
|
408
|
-
if (tag === "ul" || tag === "ol") { nested.push(list(child,
|
|
409
|
-
if (isBlock(child)) { if (buffer.trim()) words.push(buffer.trim()); buffer = ""; const b = block(child,
|
|
448
|
+
if (tag === "ul" || tag === "ol") { nested.push(list(child, keepItemLines)); continue; }
|
|
449
|
+
if (isBlock(child)) { if (buffer.trim()) words.push(buffer.trim()); buffer = ""; const b = block(child, keepItemLines); if (b) words.push(b); continue; }
|
|
410
450
|
buffer += inlineNode(child, keepItemLines);
|
|
411
451
|
}
|
|
412
452
|
if (buffer.trim()) words.push(buffer.trim());
|
|
413
|
-
|
|
453
|
+
// A block spanning lines (a code block, a table) is a paragraph of its own.
|
|
454
|
+
const body = words.reduce((text, word) => (!text ? word : text + (text.includes(NL) || word.includes(NL) ? NL + NL : " ") + word), "");
|
|
455
|
+
// An item's later lines sit at its content column, after the marker
|
|
456
|
+
// and its space. They came back at column 0, or three spaces in for a
|
|
457
|
+
// nested list, so a code block, table, quote or second paragraph
|
|
458
|
+
// ended the item and a list under item 10 was not nested (measured
|
|
459
|
+
// 2026-10-29). Nested content is laid out from column 0 and shifted.
|
|
460
|
+
const pad = " ".repeat(marker.length + 1);
|
|
461
|
+
const lines = (body + (nested.length ? NL + nested.join(NL) : "")).split(NL);
|
|
462
|
+
items.push(marker + " " + lines.map((line, at) => (at === 0 || !line ? line : pad + line)).join(NL));
|
|
414
463
|
}
|
|
415
464
|
return items.join(NL);
|
|
416
465
|
};
|
|
417
|
-
const block = (node,
|
|
466
|
+
const block = (node, keep) => {
|
|
418
467
|
if (node.nodeType === 3) return (keep === true ? collapseKeepingLines(node.textContent || "") : collapse(node.textContent || "")).trim();
|
|
419
468
|
if (node.nodeType !== 1 || skipped(node)) return "";
|
|
420
469
|
const tag = tagOf(node);
|
|
@@ -423,25 +472,27 @@ export const CHATGPT_RENDERED_MESSAGE_TEXT_JS = `
|
|
|
423
472
|
if (isMermaidPreview(node)) return mermaidBlock(node);
|
|
424
473
|
if (tag === "table") return table(node);
|
|
425
474
|
if (/^h[1-6]$/.test(tag)) return "#".repeat(Number(tag[1])) + " " + inline(node).trim();
|
|
426
|
-
if (tag === "ul" || tag === "ol") return list(node,
|
|
427
|
-
|
|
428
|
-
|
|
475
|
+
if (tag === "ul" || tag === "ol") return list(node, keepsLineBreaks(node, keep === true));
|
|
476
|
+
// The newer renderer draws a rule as an empty
|
|
477
|
+
// div[data-d-component=divider] (measured 2026-10-29).
|
|
478
|
+
if (tag === "hr" || attr(node, "data-d-component") === "divider") return "---";
|
|
479
|
+
if (tag === "blockquote") return blocks(node, keep).split(NL).map((line) => "> " + line).join(NL);
|
|
429
480
|
if (tag === "p") return inline(node, keep).trim();
|
|
430
|
-
return blocks(node,
|
|
481
|
+
return blocks(node, keep);
|
|
431
482
|
};
|
|
432
|
-
const blocks = (node,
|
|
483
|
+
const blocks = (node, keep) => {
|
|
433
484
|
const keepLines = keepsLineBreaks(node, keep === true);
|
|
434
485
|
const parts = [];
|
|
435
486
|
let buffer = "";
|
|
436
487
|
const flush = () => { if (buffer.trim()) parts.push(buffer.trim()); buffer = ""; };
|
|
437
488
|
for (const child of children(node)) {
|
|
438
|
-
if (isBlock(child)) { flush(); const b = block(child,
|
|
489
|
+
if (isBlock(child)) { flush(); const b = block(child, keepLines); if (b) parts.push(b); continue; }
|
|
439
490
|
buffer += inlineNode(child, keepLines);
|
|
440
491
|
}
|
|
441
492
|
flush();
|
|
442
493
|
return parts.join(NL + NL);
|
|
443
494
|
};
|
|
444
|
-
const markdown = blocks(root
|
|
495
|
+
const markdown = blocks(root).trim();
|
|
445
496
|
return markdown || root.innerText || "";
|
|
446
497
|
};
|
|
447
498
|
// A sent user turn renders inline code as <code>, dropping its backticks
|
|
@@ -2164,6 +2215,13 @@ export function menuOpenExpression() {
|
|
|
2164
2215
|
export function menuClosedExpression() {
|
|
2165
2216
|
return `!document.querySelector('[data-testid="composer-intelligence-picker-content"],[role="menu"]:has([role="slider"])')`;
|
|
2166
2217
|
}
|
|
2218
|
+
/** One visible model selector, marked closed, with no picker on the page. */
|
|
2219
|
+
export function modelSelectorReadsClosedExpression() {
|
|
2220
|
+
return `(() => {
|
|
2221
|
+
const triggers = [...document.querySelectorAll('button[aria-label="Select ChatGPT model"][aria-haspopup="menu"],[data-codex-intelligence-trigger="true"][aria-haspopup="menu"]')].filter((el) => el.getClientRects().length > 0);
|
|
2222
|
+
return triggers.length === 1 && triggers[0].getAttribute("aria-expanded") === "false" && !${menuOpenExpression()};
|
|
2223
|
+
})()`;
|
|
2224
|
+
}
|
|
2167
2225
|
function toLabelCandidates(label) {
|
|
2168
2226
|
return typeof label === "string" ? [label] : [...label];
|
|
2169
2227
|
}
|
|
@@ -2236,6 +2294,28 @@ async function verifiedClickAt(cdp, x, y, label) {
|
|
|
2236
2294
|
await cdp.send("Input.dispatchMouseEvent", { type: "mousePressed", x, y, button: "left", clickCount: 1 });
|
|
2237
2295
|
await cdp.send("Input.dispatchMouseEvent", { type: "mouseReleased", x, y, button: "left", clickCount: 1 });
|
|
2238
2296
|
}
|
|
2297
|
+
const MODEL_MENU_FIRST_PRESS_MS = 1_500;
|
|
2298
|
+
/**
|
|
2299
|
+
* Waits for the model picker after the selector was pressed, pressing it once
|
|
2300
|
+
* more if the page ignored that press.
|
|
2301
|
+
*
|
|
2302
|
+
* Measured 2026-10-29 with the host under load: in two of every four sends
|
|
2303
|
+
* that started from a conversation, a trusted pointerdown and pointerup reached
|
|
2304
|
+
* the selector while it read closed and nothing happened - aria-expanded stayed
|
|
2305
|
+
* false and no menu was added - so the send failed as "model menu did not
|
|
2306
|
+
* open". A selector that still reads closed is pressed again; one that reads
|
|
2307
|
+
* open is left alone, since a press would shut it.
|
|
2308
|
+
*/
|
|
2309
|
+
export async function waitForModelMenu(cdp) {
|
|
2310
|
+
if (await waitForExpressionTrue(cdp, menuOpenExpression(), MODEL_MENU_FIRST_PRESS_MS))
|
|
2311
|
+
return true;
|
|
2312
|
+
if (await cdp.evaluate(modelSelectorReadsClosedExpression())) {
|
|
2313
|
+
const again = await cdp.evaluate(modelButtonRectExpression());
|
|
2314
|
+
if (again.ok && again.x !== undefined && again.y !== undefined)
|
|
2315
|
+
await verifiedClickAt(cdp, again.x, again.y, "model selector");
|
|
2316
|
+
}
|
|
2317
|
+
return waitForExpressionTrue(cdp, menuOpenExpression(), MENU_OPEN_TIMEOUT_MS);
|
|
2318
|
+
}
|
|
2239
2319
|
/**
|
|
2240
2320
|
* Whether the model picker BUTTON already advertises the requested model, so
|
|
2241
2321
|
* the menu never has to be opened. ChatGPT moved the models behind a "Model"
|
|
@@ -3349,7 +3429,7 @@ async function selectModelReasoning(cdp, options, selectionWarnings = []) {
|
|
|
3349
3429
|
button = fresh;
|
|
3350
3430
|
}
|
|
3351
3431
|
}
|
|
3352
|
-
const opened = await
|
|
3432
|
+
const opened = await waitForModelMenu(cdp);
|
|
3353
3433
|
if (!opened)
|
|
3354
3434
|
throw new Error("ChatGPT model menu did not open after clicking the selector");
|
|
3355
3435
|
// Current ChatGPT: one power slider (Instant/Medium/High/Extra High/Pro on
|
|
@@ -5116,7 +5196,7 @@ export async function listChatGptModelOptions(input = {}) {
|
|
|
5116
5196
|
}
|
|
5117
5197
|
try {
|
|
5118
5198
|
await verifiedClickAt(cdp, button.x, button.y, "model selector");
|
|
5119
|
-
const opened = await
|
|
5199
|
+
const opened = await waitForModelMenu(cdp);
|
|
5120
5200
|
if (!opened)
|
|
5121
5201
|
throw new Error("ChatGPT model menu did not open after clicking the selector");
|
|
5122
5202
|
const options = await cdp.evaluate(modelMenuOptionsExpression());
|
|
@@ -6281,9 +6361,8 @@ export function pickLandedConversation(candidates, sentPrompt) {
|
|
|
6281
6361
|
* nothing is the safe answer here.
|
|
6282
6362
|
*/
|
|
6283
6363
|
export function transcriptContainsWholeSentPrompt(userText, sentPrompt) {
|
|
6284
|
-
const normalize = (value) => value
|
|
6364
|
+
const normalize = (value) => undoMarkdownEscapes(value)
|
|
6285
6365
|
.replace(AUTOLINKED_URL_PATTERN, "$1")
|
|
6286
|
-
.replace(/\\([\\`*_{}[\]()#+\-.!>~|])/g, "$1")
|
|
6287
6366
|
.replace(/\s+/g, " ")
|
|
6288
6367
|
.trim();
|
|
6289
6368
|
const seen = normalize(userText);
|
|
@@ -6298,6 +6377,14 @@ const NORMALIZED_PROMPT_MATCH_CHARS = 120;
|
|
|
6298
6377
|
// halves differently ("Function\_(x)" vs "Function_\(x\)", measured
|
|
6299
6378
|
// 2026-10-21), so the pattern is applied after markdown escapes are undone.
|
|
6300
6379
|
const AUTOLINKED_URL_PATTERN = /\[(https?:\/\/[^\]\s]+)\]\(\1\)/g;
|
|
6380
|
+
// The composer escapes more than the markdown it would otherwise misread: an
|
|
6381
|
+
// autolinked url's href kept "\\&" where its text had "&" (measured
|
|
6382
|
+
// 2026-10-28). Any ASCII punctuation can carry a markdown escape, so every such
|
|
6383
|
+
// escape is undone, on both sides alike.
|
|
6384
|
+
const MARKDOWN_ESCAPE_PATTERN = /\\([!-\/:-@[-`{-~])/g;
|
|
6385
|
+
function undoMarkdownEscapes(value) {
|
|
6386
|
+
return value.replace(MARKDOWN_ESCAPE_PATTERN, "$1");
|
|
6387
|
+
}
|
|
6301
6388
|
/**
|
|
6302
6389
|
* ChatGPT displays a sent turn's HTML entities decoded (measured 2026-10-19:
|
|
6303
6390
|
* "& <b>" was shown as "& <b>"), so both sides are compared decoded.
|
|
@@ -6314,7 +6401,7 @@ function decodeHtmlEntities(value) {
|
|
|
6314
6401
|
});
|
|
6315
6402
|
}
|
|
6316
6403
|
function normalizeChatGptPromptText(value) {
|
|
6317
|
-
const unescaped = decodeHtmlEntities(value)
|
|
6404
|
+
const unescaped = undoMarkdownEscapes(decodeHtmlEntities(value)).replace(AUTOLINKED_URL_PATTERN, "$1");
|
|
6318
6405
|
const renderedFences = unescaped.replace(/(^|\r?\n)```text[ \t]*\r?\n([\s\S]*?)\r?\n```(?=\r?\n|$)/g, "$1text\n$2");
|
|
6319
6406
|
return renderedFences.replace(/\s+/g, " ").trim();
|
|
6320
6407
|
}
|
|
@@ -6345,8 +6432,7 @@ export function chatGptRequestMatchesUserTurn(userText, sentPrompt, requestId) {
|
|
|
6345
6432
|
return withoutFenceLines(userText).includes(withoutFenceLines(sentPrompt));
|
|
6346
6433
|
}
|
|
6347
6434
|
function withoutFenceLines(value) {
|
|
6348
|
-
return decodeHtmlEntities(value)
|
|
6349
|
-
.replace(/\\([\\`*_{}[\]()#+\-.!>~|])/g, "$1")
|
|
6435
|
+
return undoMarkdownEscapes(decodeHtmlEntities(value))
|
|
6350
6436
|
.replace(AUTOLINKED_URL_PATTERN, "$1")
|
|
6351
6437
|
.replace(/^[ \t]*```[\w-]*[ \t]*$/gm, "")
|
|
6352
6438
|
.replace(/\s+/g, "");
|
package/docs/releasing.md
CHANGED
|
@@ -35,6 +35,47 @@ 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.47 Release Record
|
|
39
|
+
|
|
40
|
+
Release commit `609b1c9` on `main` (cut after PRs #71-#73, last merge `40d7eee`); tag `v0.40.47`. Released promptly because of #72: on 0.40.46 any prompt carrying a url with "&" (a query string, bare or in a markdown link) was answered and then refused as `request_mismatch`, so the answer was not returned. #71 (italic and strikethrough in the newer renderer) and #73 (the `utm_source=chatgpt.com` parameter ChatGPT appends to rendered links) went out with it.
|
|
41
|
+
|
|
42
|
+
**Publishing**
|
|
43
|
+
- `npm run release:verify` passed on the release commit after a build, with nothing else running on the machine.
|
|
44
|
+
- `publish.yml` run 40656237042 passed all six platforms, then npm publish with SLSA v1 provenance and the GitHub Release, which lists the three entries from `CHANGELOG.md`.
|
|
45
|
+
|
|
46
|
+
**Installed**
|
|
47
|
+
- WSL, M3, Mac mini: `prodex --version` reports 0.40.47. 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 `609b1c9` and rebuilt.
|
|
49
|
+
|
|
50
|
+
**Container**
|
|
51
|
+
- `container/redesign-patches` `63b060c` ports #71, `62376fa` #72 and `facbd9d` #73 (#71 and #73 did not cherry-pick cleanly and their hunks were applied by exact replacement; tsc, build, the full suite, 2128 tests, and `release:verify` pass on `facbd9d`).
|
|
52
|
+
- WSL, on the user's request after the release: the image was built from `facbd9d` (`c03be48c060d`) and the container swapped while its send lock was idle. The old container had been `unhealthy` (its health check's fetch failed) and its last consult, from another session at 03:41 UTC, was blocked as `browser_unreachable`; that consult was left for its owner to recover. The new container came up healthy, `pro browser check` reported logged in with a composer, and an Instant smoke whose prompt linked `https://example.com/s?q=a&page=2` returned `PRODEX_PRO_SMOKE_OK`, status done. Its `prodex --version` still prints 0.40.18 because the branch never bumps its version.
|
|
53
|
+
- M3, on 2026-10-29: the build over ssh was refused by the agent's permission classifier, so the user ran the build-and-swap script. The image was built from `facbd9d` in the M3 container checkout (`cfaf881d2fd6`, replacing the 0.40.42 port `59747258b693`) and the container swapped while its send lock was idle. It came up healthy, `pro browser check` reported logged in with a composer, and the same Instant smoke (prompt linking `https://example.com/s?q=a&page=2`) returned `PRODEX_PRO_SMOKE_OK`, status done.
|
|
54
|
+
|
|
55
|
+
**Verified live on installed 0.40.47 (WSL, scratch repo, Instant)**
|
|
56
|
+
- A send whose prompt linked `https://example.com/s?q=a&page=2` returned status done. The user turn was stored with `\&` in the autolink's href half, the shape 0.40.46 refused.
|
|
57
|
+
- Its answer came back as "The ~~old~~ *new* [search](https://example.com/s?q=a&page=2) page." while the page's anchor href was `https://example.com/s?q=a&page=2&utm_source=chatgpt.com`.
|
|
58
|
+
|
|
59
|
+
## 0.40.46 Release Record
|
|
60
|
+
|
|
61
|
+
Release commit `807f245` on `main` (cut after PRs #69 and #70, last merge `9253ee9`); tag `v0.40.46`. A batch released on the user's go-ahead: terminal-column widths in the interactive picker (#69) and line breaks kept inside answers from ChatGPT's newer `white-space: pre-line` renderer (#70).
|
|
62
|
+
|
|
63
|
+
**Publishing**
|
|
64
|
+
- `npm run release:verify` passed on the release commit after a build, with nothing else running on the machine.
|
|
65
|
+
- `publish.yml` run 40645773352 passed all six platforms, then npm publish with SLSA v1 provenance and the GitHub Release, which lists the two entries from `CHANGELOG.md`.
|
|
66
|
+
|
|
67
|
+
**Installed**
|
|
68
|
+
- WSL, M3, Mac mini: `prodex --version` reports 0.40.46, and each installed `dist/chatgpt-browser.js` carries the line-break reader. Running services were not restarted: no host stdio MCP servers on WSL, 1 on M3, none on the Mac mini.
|
|
69
|
+
- The maintainer watchdog checkout was pulled to `807f245` and rebuilt.
|
|
70
|
+
|
|
71
|
+
**Container**
|
|
72
|
+
- `container/redesign-patches` `5d021d1` ports #69 and `ad24ce2` ports #70 (the #70 cherry-pick conflicted in `src/chatgpt-browser.ts` and the PR diff was applied instead; tsc, build, the suite and `release:verify` pass on it).
|
|
73
|
+
- The images were still not rebuilt: `docker compose build` is refused by the agent's permission classifier on WSL and over ssh on M3. The running WSL container is the #49-#58 port and the M3 container the 0.40.42 port.
|
|
74
|
+
|
|
75
|
+
**Verified**
|
|
76
|
+
- Before the release, on the live tab: the reader in 0.40.45 returned "alpha beta" for an answer showing "alpha" and "beta" on two lines; the #70 build returned "alpha\nbeta" from the same page, with the list after it unchanged.
|
|
77
|
+
- After installing, the tab no longer showed a multi-line answer, so the line-break fix was not re-read live on the installed build; the installed reader ran on the current answer without error.
|
|
78
|
+
|
|
38
79
|
## 0.40.45 Release Record
|
|
39
80
|
|
|
40
81
|
Release commit `5e5f965` on `main` (cut after PRs #59 and #63-#68, last merge `52243f4`); tag `v0.40.45`. A batch released on the user's go-ahead: machine-wide send pacing (#59), accurate advice for a busy tab (#63), word-bounded project-name redaction (#64), the IPv6 loopback message (#65), no cancelling of finished sessions (#66), a blank saved default project stopped before the browser (#67), and the project-delete preview (#68).
|