@youdie006/prodex 0.40.49 → 0.40.50
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/bundle.js +3 -1
- package/dist/chatgpt-browser.js +18 -4
- package/dist/cli-pro.js +26 -11
- package/dist/followup-budget.js +24 -2
- package/docs/clients.md +9 -4
- package/docs/releasing.md +101 -0
- package/package.json +1 -1
package/dist/bundle.js
CHANGED
|
@@ -66,7 +66,9 @@ export async function buildDryRunBundle(root, input) {
|
|
|
66
66
|
}
|
|
67
67
|
return {
|
|
68
68
|
schema_version: SCHEMA_VERSION,
|
|
69
|
-
id
|
|
69
|
+
// The id is listed wherever sessions are (MCP bridge_list_sessions among
|
|
70
|
+
// them), so it carries no words from the prompt.
|
|
71
|
+
id: makeBridgeId("sess", randomUUID().slice(0, 8)),
|
|
70
72
|
mode: "manual_copy",
|
|
71
73
|
prompt: input.prompt,
|
|
72
74
|
files,
|
package/dist/chatgpt-browser.js
CHANGED
|
@@ -2713,12 +2713,19 @@ export function focusPowerSliderExpression() {
|
|
|
2713
2713
|
* only (the exact "Select ChatGPT model" control first, the older heuristic
|
|
2714
2714
|
* second), and an unrendered composer is reported as not found, which the
|
|
2715
2715
|
* caller's poll waits out.
|
|
2716
|
+
*
|
|
2717
|
+
* An open "Edit message" editor is a form inside the transcript, ahead of the
|
|
2718
|
+
* composer in the document, and has no trigger. Measured 2026-11-03: taking it
|
|
2719
|
+
* refused every follow-up that selected a model until the page was reloaded,
|
|
2720
|
+
* so editors inside a message are skipped. Only message ancestors exclude one
|
|
2721
|
+
* here, not aria-hidden as in the typing lookup: an open menu aria-hides the
|
|
2722
|
+
* composer itself.
|
|
2716
2723
|
*/
|
|
2717
2724
|
export function modelButtonRectExpression() {
|
|
2718
2725
|
return `(() => {${CLICK_POINT_SNIPPET}
|
|
2719
2726
|
const composer = [...document.querySelectorAll('#prompt-textarea,[contenteditable="true"],textarea')].find((el) => {
|
|
2720
2727
|
const r = el.getBoundingClientRect();
|
|
2721
|
-
return r.width > 0 && r.height > 0;
|
|
2728
|
+
return r.width > 0 && r.height > 0 && !(el.parentElement && el.parentElement.closest('[data-message-author-role],[data-chatgpt-search-unit-key]'));
|
|
2722
2729
|
});
|
|
2723
2730
|
if (!composer) return { ok: false, reason: "model selector button not found: the composer has not rendered" };
|
|
2724
2731
|
const scope = composer.closest('form');
|
|
@@ -4581,6 +4588,7 @@ export async function sendChatGptPrompt(options) {
|
|
|
4581
4588
|
if (last && expandCollapsedUserTurn(last.textNode)) return false;
|
|
4582
4589
|
return Boolean(last && userMessageText(last.textNode).includes(${JSON.stringify(`[prodex-request:${requestId}]`)}));
|
|
4583
4590
|
})()`;
|
|
4591
|
+
await options.beforePromptSubmit?.();
|
|
4584
4592
|
// A dispatch can reach Chrome even if its acknowledgement is lost.
|
|
4585
4593
|
submissionAttempted = true;
|
|
4586
4594
|
await cdp.send("Input.dispatchKeyEvent", enterKeyEvent("keyDown"));
|
|
@@ -6103,9 +6111,11 @@ export function composerToolsButtonRectExpression() {
|
|
|
6103
6111
|
return `(() => {${CLICK_POINT_SNIPPET}
|
|
6104
6112
|
const legacy = document.querySelector('[data-testid="composer-plus-btn"]');
|
|
6105
6113
|
if (legacy) return clickPoint(legacy);
|
|
6114
|
+
// An open "Edit message" editor sits in the transcript ahead of the
|
|
6115
|
+
// composer, in a form without this button; see modelButtonRectExpression.
|
|
6106
6116
|
const composer = [...document.querySelectorAll('#prompt-textarea,[contenteditable="true"],textarea')].find((el) => {
|
|
6107
6117
|
const r = el.getBoundingClientRect();
|
|
6108
|
-
return r.width > 0 && r.height > 0;
|
|
6118
|
+
return r.width > 0 && r.height > 0 && !(el.parentElement && el.parentElement.closest('[data-message-author-role],[data-chatgpt-search-unit-key]'));
|
|
6109
6119
|
});
|
|
6110
6120
|
const form = composer && composer.closest('form');
|
|
6111
6121
|
if (!form) return { ok: false, reason: "composer tools button not found: the composer has not rendered" };
|
|
@@ -6154,11 +6164,15 @@ export function composerToolEntryRectExpression(label) {
|
|
|
6154
6164
|
/** Tool tokens currently sitting in the composer. */
|
|
6155
6165
|
export function activeComposerToolsExpression(labels) {
|
|
6156
6166
|
const labelsJson = JSON.stringify(labels);
|
|
6157
|
-
return `(() => {
|
|
6167
|
+
return `(() => {${composerExpressionHelpers()}
|
|
6158
6168
|
// The token normally lands inside the editor, but some builds render it as
|
|
6159
6169
|
// a pill in the surrounding composer form - check both before deciding a
|
|
6160
6170
|
// selection did not take.
|
|
6161
|
-
|
|
6171
|
+
// Read the composer a send types into. Measured 2026-11-03: a new chat
|
|
6172
|
+
// leaves the home page's composer mounted in a hidden workspace ahead of
|
|
6173
|
+
// the conversation's, and the first editor in the document was that one,
|
|
6174
|
+
// so a follow-up's tool was on but read as off.
|
|
6175
|
+
const el = findChatGptComposerCandidate();
|
|
6162
6176
|
const form = el ? (el.closest("form") || el.parentElement) : null;
|
|
6163
6177
|
const text = ((el ? el.innerText || "" : "") + String.fromCharCode(10) + (form ? form.innerText || "" : "")).toLowerCase();
|
|
6164
6178
|
// Case-insensitively: prodex carries the label as the menu spells it
|
package/dist/cli-pro.js
CHANGED
|
@@ -14,7 +14,7 @@ import { getTokenExpiryStatus, loadBrowserDefaults, loadLocalConfig } from "./co
|
|
|
14
14
|
import { sendPacingMarkerPath, withBrowserSendLock } from "./browser-send-lock.js";
|
|
15
15
|
import { blockerCause, buildBlockerReport, CATCH_ALL_CODES } from "./blocker-report.js";
|
|
16
16
|
import { isChatGptConversationUrl, projectIdFromSidebar, resolveContinuationThread } from "./continue-thread.js";
|
|
17
|
-
import { FollowupApprovalRequired, reserveFollowup, resolveMaxAutoFollowups } from "./followup-budget.js";
|
|
17
|
+
import { assertFollowupAvailable, FollowupApprovalRequired, reserveFollowup, resolveMaxAutoFollowups } from "./followup-budget.js";
|
|
18
18
|
import { readBridgeRoots } from "./registry.js";
|
|
19
19
|
import { ProdexRequestIdSchema, SessionKeySchema } from "./schema.js";
|
|
20
20
|
import { BridgeStore, MAX_FETCHABLE_RESULT_ARTIFACT_BYTES } from "./store.js";
|
|
@@ -1170,7 +1170,10 @@ export function createBrowserSendProgressPrinter(write, heartbeatMs = 10_000) {
|
|
|
1170
1170
|
write(`progress: ${PROGRESS_PHASE_LABELS[event.phase]}${event.detail ? ` (${event.detail})` : ""}`);
|
|
1171
1171
|
};
|
|
1172
1172
|
}
|
|
1173
|
-
export async function runAskProCommand(rest, io,
|
|
1173
|
+
export async function runAskProCommand(rest, io,
|
|
1174
|
+
// Runs before any task is created. The step it returns runs once the prompt
|
|
1175
|
+
// is typed, right before it is submitted.
|
|
1176
|
+
beforeSend) {
|
|
1174
1177
|
const parsedAskPro = parseAskProArgs(rest);
|
|
1175
1178
|
const hasDryRunMode = parsedAskPro.optionArgs.includes("--dry-run");
|
|
1176
1179
|
const hasSendMode = parsedAskPro.optionArgs.includes("--send");
|
|
@@ -1472,11 +1475,11 @@ export async function runAskProCommand(rest, io, beforeSend) {
|
|
|
1472
1475
|
if (hasSendMode) {
|
|
1473
1476
|
// MCP approval checkpoints run after target validation but before any
|
|
1474
1477
|
// task creation, pacing, browser recovery, or prompt send.
|
|
1475
|
-
await beforeSend?.({
|
|
1478
|
+
const beforePromptSubmit = (await beforeSend?.({
|
|
1476
1479
|
store: targetStore,
|
|
1477
1480
|
...(normalizedTargetUrl ? { thread: normalizedTargetUrl } : {}),
|
|
1478
1481
|
...(continuedFromTaskId ? { continuedFrom: continuedFromTaskId } : {})
|
|
1479
|
-
});
|
|
1482
|
+
})) || undefined;
|
|
1480
1483
|
const browserCommandOptions = {
|
|
1481
1484
|
cwd: targetCwd,
|
|
1482
1485
|
port: parsedAskPro.optionArgs.includes("--port") ? browserPort : undefined
|
|
@@ -1555,6 +1558,7 @@ export async function runAskProCommand(rest, io, beforeSend) {
|
|
|
1555
1558
|
proMode: selectionProMode,
|
|
1556
1559
|
effort: selectionEffort,
|
|
1557
1560
|
...(allowSelectionFallback ? { allowSelectionFallback: true } : {}),
|
|
1561
|
+
...(beforePromptSubmit ? { beforePromptSubmit } : {}),
|
|
1558
1562
|
onProgress: createBrowserSendProgressPrinter(io.stderr)
|
|
1559
1563
|
});
|
|
1560
1564
|
});
|
|
@@ -1592,7 +1596,11 @@ export async function runAskProCommand(rest, io, beforeSend) {
|
|
|
1592
1596
|
}
|
|
1593
1597
|
}
|
|
1594
1598
|
catch (error) {
|
|
1595
|
-
|
|
1599
|
+
// Another follow-up spent the budget while this one waited for the
|
|
1600
|
+
// browser. Nothing was submitted; the caller asks the user instead.
|
|
1601
|
+
const blocker = error instanceof FollowupApprovalRequired
|
|
1602
|
+
? { code: "followup_approval_required", message: error.message, retryable: false }
|
|
1603
|
+
: await withManualStepForWindowMode(sourceAwareBrowserBlocker(browserSendBlockerFromError(error), sourceCli, browserCommandOptions), resolveCdpPort(browserPort), sourceCli, browserCommandOptions);
|
|
1596
1604
|
const message = blocker.next_step ? `${blocker.message} Next: ${blocker.next_step}` : errorMessage(error);
|
|
1597
1605
|
// The blocker text can quote the requested/default project name (e.g. a
|
|
1598
1606
|
// "project not found" error). Local stdout/stderr keep it (useful to the
|
|
@@ -1659,6 +1667,8 @@ export async function runAskProCommand(rest, io, beforeSend) {
|
|
|
1659
1667
|
blocker
|
|
1660
1668
|
}, null, 2));
|
|
1661
1669
|
}
|
|
1670
|
+
if (error instanceof FollowupApprovalRequired)
|
|
1671
|
+
throw error;
|
|
1662
1672
|
throw new Error(formatBlockedConsultRecordedMessage(message, task.id, sourceCli, { cwd: targetCwd }));
|
|
1663
1673
|
}
|
|
1664
1674
|
const answerArtifactText = formatProConsultArtifact(consult);
|
|
@@ -2036,12 +2046,17 @@ export async function performBrowserConsultForMcp(cwd, input, onProgress) {
|
|
|
2036
2046
|
if (!thread || !continuedFrom)
|
|
2037
2047
|
return;
|
|
2038
2048
|
continuationTarget = { taskId: continuedFrom, thread };
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2049
|
+
const reservation = { thread, taskId: continuedFrom, userApproved: input.user_approved === true, limit };
|
|
2050
|
+
await assertFollowupAvailable(store, reservation);
|
|
2051
|
+
// Spent only when the prompt is about to be submitted, and once: a retry
|
|
2052
|
+
// after browser recovery is the same follow-up.
|
|
2053
|
+
let reserved = false;
|
|
2054
|
+
return async () => {
|
|
2055
|
+
if (reserved)
|
|
2056
|
+
return;
|
|
2057
|
+
followupBudget = await reserveFollowup(store, reservation);
|
|
2058
|
+
reserved = true;
|
|
2059
|
+
};
|
|
2045
2060
|
});
|
|
2046
2061
|
}
|
|
2047
2062
|
catch (error) {
|
package/dist/followup-budget.js
CHANGED
|
@@ -33,7 +33,24 @@ export function resolveMaxAutoFollowups(envValue = process.env.PRODEX_MAX_AUTO_F
|
|
|
33
33
|
}
|
|
34
34
|
return value;
|
|
35
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* Refuse a follow-up the budget no longer covers, without spending any of it.
|
|
38
|
+
*
|
|
39
|
+
* The follow-up is reserved only once its prompt is about to be submitted.
|
|
40
|
+
* Measured 2026-11-02 in the browser container: two follow-ups in one
|
|
41
|
+
* conversation, refused as composer_not_ready before anything was typed, were
|
|
42
|
+
* recorded as its follow-ups 3 and 4 of 5. This check keeps an exhausted budget
|
|
43
|
+
* from creating a task or touching the browser at all.
|
|
44
|
+
*/
|
|
45
|
+
export async function assertFollowupAvailable(store, input) {
|
|
46
|
+
await withFollowupBudgetLock(store, input, async (conversationKey) => {
|
|
47
|
+
await readConversationLedger(store, { conversationKey, ...input });
|
|
48
|
+
});
|
|
49
|
+
}
|
|
36
50
|
export async function reserveFollowup(store, input) {
|
|
51
|
+
return withFollowupBudgetLock(store, input, async (conversationKey) => reserveFollowupUnderLock(store, { ...input, conversationKey }));
|
|
52
|
+
}
|
|
53
|
+
async function withFollowupBudgetLock(store, input, run) {
|
|
37
54
|
assertFollowupLimit(input.limit);
|
|
38
55
|
const conversationKey = conversationKeyFromThread(input.thread);
|
|
39
56
|
await store.ensure();
|
|
@@ -44,9 +61,10 @@ export async function reserveFollowup(store, input) {
|
|
|
44
61
|
privateParent: true,
|
|
45
62
|
busyError: (holder) => new Error(`Another follow-up reservation is in progress (pid ${holder.pid ?? "unknown"})`),
|
|
46
63
|
unavailableError: () => new Error("The follow-up budget lock could not be recovered. Stop all prodex processes before removing the lock and its matching .reap claim, then retry.")
|
|
47
|
-
}, async () =>
|
|
64
|
+
}, async () => run(conversationKey));
|
|
48
65
|
}
|
|
49
|
-
|
|
66
|
+
/** The conversation's ledger so far; throws when the budget does not cover another follow-up. */
|
|
67
|
+
async function readConversationLedger(store, input) {
|
|
50
68
|
const reservations = await readTrustedReservations(store);
|
|
51
69
|
const conversationReservations = reservations
|
|
52
70
|
.filter((reservation) => reservation.metadata.conversation_key === input.conversationKey)
|
|
@@ -59,6 +77,10 @@ async function reserveFollowupUnderLock(store, input) {
|
|
|
59
77
|
remaining: 0
|
|
60
78
|
});
|
|
61
79
|
}
|
|
80
|
+
return previous;
|
|
81
|
+
}
|
|
82
|
+
async function reserveFollowupUnderLock(store, input) {
|
|
83
|
+
const previous = await readConversationLedger(store, input);
|
|
62
84
|
const budget = input.userApproved === true
|
|
63
85
|
? { limit: input.limit, used: 0, remaining: input.limit }
|
|
64
86
|
: {
|
package/docs/clients.md
CHANGED
|
@@ -52,12 +52,17 @@ follow-up checkpoint (integer 0-1000; default 5). For example:
|
|
|
52
52
|
PRODEX_MAX_AUTO_FOLLOWUPS = "8"
|
|
53
53
|
```
|
|
54
54
|
|
|
55
|
-
The initial fresh consult is not a follow-up.
|
|
56
|
-
|
|
57
|
-
|
|
55
|
+
The initial fresh consult is not a follow-up. A follow-up is reserved once its prompt
|
|
56
|
+
is typed, right before it is submitted, and reservations persist across reconnects,
|
|
57
|
+
older task references, and different session keys in the same bridge and
|
|
58
|
+
conversation. A send refused before that point, such as `composer_not_ready`, does
|
|
59
|
+
not count; an attempt that fails or is uncertain after submitting does. At the limit,
|
|
58
60
|
the tool returns `status: "awaiting_user"`, `task_id: null`, the intended continuation
|
|
59
61
|
arguments, and `followup_budget: {limit, used, remaining}` without creating a consult
|
|
60
|
-
task or sending a prompt. This response-only status is not a ledger task status.
|
|
62
|
+
task or sending a prompt. This response-only status is not a ledger task status. If
|
|
63
|
+
another follow-up in the same conversation uses the last of the budget while this one
|
|
64
|
+
waits for the browser, this one stops before submitting with the same response, and
|
|
65
|
+
its consult task is recorded as blocked with `followup_approval_required`.
|
|
61
66
|
|
|
62
67
|
Ask the user whether to continue. Only after an explicit user request/approval may
|
|
63
68
|
the caller send that continuation with `user_approved: true`. This human-directed
|
package/docs/releasing.md
CHANGED
|
@@ -16,6 +16,107 @@ 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
|
+
## 0.40.49 Release Record
|
|
20
|
+
|
|
21
|
+
Release commit `74b9c4d` on `main` (cut after PRs #79-#83, last merge `be947df`, docs `7b6a3bd`); tag `v0.40.49`, published 2026-11-01. Released on the user's go-ahead as a batch: #79 (the page's render error saved as the answer), #80 (blank `--tool`), #81 (`--continue` matching a project by the end of its name) and #83 (projects behind the sidebar's "Show more"). #82 changed a test only and has no changelog entry.
|
|
22
|
+
|
|
23
|
+
**Before the cut**
|
|
24
|
+
- `main` CI on `7b6a3bd` (docs only) failed on macOS Intel: `npm test` ran past the release check's 15-minute budget on a slow runner, with no failing test. On the four runs before it that step took 10.5 to 11.8 minutes. The failed job was rerun and passed.
|
|
25
|
+
|
|
26
|
+
**Publishing**
|
|
27
|
+
- `npm run release:verify` passed on the release commit after a build.
|
|
28
|
+
- `publish.yml` run 41283791768 passed all six platforms, then npm publish with SLSA v1 provenance and the GitHub Release, which lists the four entries from `CHANGELOG.md`.
|
|
29
|
+
|
|
30
|
+
**Installed**
|
|
31
|
+
- WSL, M3, Mac mini: `prodex --version` reports 0.40.49.
|
|
32
|
+
- The maintainer watchdog checkout was pulled to `74b9c4d` and rebuilt.
|
|
33
|
+
- MacBook M4 (`m4`), added to the install targets this release. It had two copies:
|
|
34
|
+
- `~/.local/bin/prodex`, which its Codex MCP entry runs directly, links into `~/.gemini/harness-antigravity/runtime-prodex` (an npm project, 0.40.32). It was updated there with `npm i @youdie006/prodex@0.40.49`.
|
|
35
|
+
- A global copy under `/opt/homebrew` (0.40.0) was updated with `npm i -g --prefix /opt/homebrew`.
|
|
36
|
+
- The link pointed at `dist/cli.js`, which is not executable in the new install, because the package's executable has been `prodex.mjs` since 0.40.18. Run directly, it failed with "permission denied", so Codex could not have started the MCP server again. The link now points at the runtime's `node_modules/.bin/prodex`. Through it, `--version` reports 0.40.49 and `prodex mcp` answers `initialize` as 0.40.49.
|
|
37
|
+
- Running services were not restarted. They keep the old code until their client starts them again:
|
|
38
|
+
- M4: Codex's `prodex mcp`, started before the install.
|
|
39
|
+
- M3: a `prodex mcp` running since 2026-09-15.
|
|
40
|
+
- Mac mini and WSL: none on the host. WSL's MCP entry goes through the browser container.
|
|
41
|
+
|
|
42
|
+
**Verified live on installed 0.40.49 (WSL host browser, scratch repo)**
|
|
43
|
+
- `pro browser projects` listed 25 projects; 0.40.48 listed 9 of the same 25.
|
|
44
|
+
- `pro browser smoke` returned `requestVerified: true`.
|
|
45
|
+
|
|
46
|
+
**After the release**
|
|
47
|
+
- PR #84 (merge `b7b5d61`, unreleased): session ids no longer carry the start of the prompt.
|
|
48
|
+
|
|
49
|
+
**Container**
|
|
50
|
+
- `container/redesign-patches` `f2d13e6` ports #84 on top of the ports of #79-#83 (`f93b63f`, `661b2c6`, `106bd0a`, `facd4fd`) and the 1024 process limit (`55f2f3e`). tsc, the full suite (2161 tests) and `release:verify` pass on it.
|
|
51
|
+
- The agent's permission classifier refused the WSL `docker compose build`, so the user ran the build-and-swap scripts. Both images were built from `f2d13e6`. Both swaps waited for the container's send lock to be free.
|
|
52
|
+
- M3 (colima `prodex-check`): image `4bc8fac19752`, started 07:57 UTC.
|
|
53
|
+
- It came up healthy with the 1024 process limit, using 146.
|
|
54
|
+
- `pro browser check` reported logged in with a composer.
|
|
55
|
+
- `pro browser smoke` answered `PRODEX_PRO_SMOKE_OK` with its request verified.
|
|
56
|
+
- A dry-run `pro ask "Draft the payroll memo"` (nothing sent) got the id `sess_20261101_075935_93598ff4`, with no prompt words in it.
|
|
57
|
+
- WSL: image `e2e3523b640a`.
|
|
58
|
+
- The first run built the image but did not swap, because a Pro consult from another session had held the send lock since 07:54 UTC.
|
|
59
|
+
- The swap waited for that consult, which finished `done`. The lock cleared at 08:06:32, and the container was recreated at 08:06:35.
|
|
60
|
+
- It came up healthy with the 1024 process limit, using 177, and logged no thread errors.
|
|
61
|
+
- `pro browser check`, `smoke` and the dry-run id check gave the same results as on M3 (id `sess_20261101_080804_33e445d8`).
|
|
62
|
+
- The swap dropped every MCP client of the WSL container, so each WSL Claude Code session needs `/mcp`.
|
|
63
|
+
- Both containers' `prodex --version` still prints 0.40.18, because the container branch never bumps its version.
|
|
64
|
+
|
|
65
|
+
**MCP clients kept connected across container swaps (2026-11-01, after the swaps)**
|
|
66
|
+
- Both swaps had dropped every MCP client of their container: `scripts/container-client.mjs mcp` piped the client into `docker exec` and ended with it, so each Claude Code session needed `/mcp` typed by hand.
|
|
67
|
+
- `container/redesign-patches` `964b847` turns that command into a relay that reconnects:
|
|
68
|
+
- it answers requests in flight with an error that names `pro_recover`;
|
|
69
|
+
- it runs `docker exec` again until the container is back, holding new requests for up to 60 seconds;
|
|
70
|
+
- it replays the client's handshake, tells the client its tools may have changed, and then passes the held requests on.
|
|
71
|
+
- Verified:
|
|
72
|
+
- 6 new tests; suite 2167 passed; `release:verify` ok.
|
|
73
|
+
- On a throwaway container from the same image, one client connection listed 20 tools, survived a restart and then a delete-and-recreate, and listed 20 tools again each time.
|
|
74
|
+
- WSL deployment:
|
|
75
|
+
- The prodex MCP entries used to run the script from `/mnt/d/myproject/gptprouse`, a checkout another task keeps on its own branch.
|
|
76
|
+
- Three Claude configs now run it from a checkout pinned for this purpose, `~/.local/share/prodex-maint/container-src` at `964b847`: `~/.claude.json` and the two swapdex slot configs that had the entry.
|
|
77
|
+
- The Codex config (`~/.codex/config.toml`) was changed the same way.
|
|
78
|
+
- `claude mcp get prodex` reports Connected. A handshake through it reached the WSL container (server 0.40.18, 20 tools).
|
|
79
|
+
- M3 deployment:
|
|
80
|
+
- Its Claude and Codex configs run an installed copy, `~/.local/share/prodex/container-installed-8d7a094-wP3RK2/container-client.mjs`.
|
|
81
|
+
- That file was replaced in place with `964b847`'s; the old one is kept beside it as `container-client.mjs.8d7a094.bak`. The configs are unchanged.
|
|
82
|
+
- A handshake through it reached the M3 container.
|
|
83
|
+
- Sessions that were open during the swaps still need one `/mcp`. After that, a container restart or swap no longer disconnects them.
|
|
84
|
+
- When `container-client.mjs` changes again, both copies must be updated:
|
|
85
|
+
- WSL: move the pinned checkout to the new commit;
|
|
86
|
+
- M3: replace the installed copy.
|
|
87
|
+
|
|
88
|
+
## Container consults ran on the last model selected (2026-11-01)
|
|
89
|
+
|
|
90
|
+
**What happened**
|
|
91
|
+
- The user reported that some sessions' consults did not use Pro but whatever model the browser last had.
|
|
92
|
+
- In the WSL container's results, 20 consults since 2026-10-10 carry `model_selection_warning: no model/effort was selected`. Today's 07:54 UTC Pro consult from another session is one of them.
|
|
93
|
+
- Cause: a consult that names no model, with no saved or environment default, keeps the picker's current selection. That is the designed fallback, but in a shared browser the current selection is whatever another session (or an Instant smoke test) picked last.
|
|
94
|
+
- The container's MCP server had neither default. `container-client.mjs mcp` passed no variables into the container, so an MCP entry's env block could not reach it, and the bridge in the container had no saved config.
|
|
95
|
+
- The host entries on WSL that run `prodex mcp` directly set `PRODEX_DEFAULT_MODEL=Pro` and were not affected.
|
|
96
|
+
|
|
97
|
+
**Fixes**
|
|
98
|
+
- `container/redesign-patches` `cd3b671`: `mcp` passes the client's `PRODEX_DEFAULT_MODEL`, `_EFFORT`, `_PRO_MODE` and `_PROJECT` into the container, and adds `PRODEX_DEFAULT_MODEL=Pro` when none of them names a model or a reasoning step. A consult's own `model` or `effort` still wins.
|
|
99
|
+
- Verified: 3 new tests, suite 2170 passed, `release:verify` ok.
|
|
100
|
+
- In the container, the resolved defaults went from `null` to `{"model":"Pro"}`.
|
|
101
|
+
- A consult that named no model, sent through the new client, came back done.
|
|
102
|
+
- Deployed to the WSL pinned checkout (`~/.local/share/prodex-maint/container-src` at `cd3b671`) and to the M3 installed copy.
|
|
103
|
+
- The 16 WSL sessions had reconnected (17:35-17:37 KST) before `cd3b671` was in place, so their relays still run the version without the default.
|
|
104
|
+
- So the bridge inside each container now saves Pro: `setup --cwd /home/node/bridge --model Pro` on WSL and M3. That writes `.bridge/config.local.json` with a loopback HTTP profile; no HTTP server runs there.
|
|
105
|
+
- Each consult reads this default, so it applies to connected clients at once. A consult through one of the old relays, naming no model, came back done from Pro.
|
|
106
|
+
- Each such consult carries the warning `model_is_now_an_effort`, because the current picker has Pro as an effort step. The model default is kept because a model or effort the consult names itself overrides it. A saved effort default would be applied on top of a model the consult names.
|
|
107
|
+
|
|
108
|
+
**M4 and Mac mini (host `prodex mcp`, 2026-11-03)**
|
|
109
|
+
- Neither machine's Claude or Codex entry set `PRODEX_DEFAULT_MODEL`. On M4, `.bridge/results` held one consult of five with the no-model warning (`~/.gemini/harness-antigravity/verification`).
|
|
110
|
+
- M4:
|
|
111
|
+
- `~/.claude.json` now has `env: {PRODEX_DEFAULT_MODEL: Pro}`, set with `claude mcp remove` and `add-json`.
|
|
112
|
+
- `~/.codex/config.toml` `[mcp_servers.prodex]` now has `env = { PRODEX_DEFAULT_MODEL = "Pro" }`. A backup is kept as `config.toml.bak-prodex-default-20261103`, and `codex mcp get prodex` parses it.
|
|
113
|
+
- The three running `prodex mcp` processes keep their old environment until their clients restart them.
|
|
114
|
+
- Mac mini: the same change, made on the user's explicit request after the first attempt was refused:
|
|
115
|
+
- Claude `~/.claude.json` now carries `env: {PRODEX_DEFAULT_MODEL: Pro}`.
|
|
116
|
+
- Codex `~/.codex/config.toml` has the same `env` line, with the backup `config.toml.bak-prodex-default-20261103`. `codex mcp get prodex` shows the variable set.
|
|
117
|
+
- No `prodex mcp` was running there, so the next client start uses it.
|
|
118
|
+
- A WSL Codex session started on 2026-10-28 still runs the client from the old path until it restarts. Its config, `~/.codex/config.toml` (linked from every swapdex slot), already points at the pinned checkout.
|
|
119
|
+
|
|
19
120
|
## Browser Incident 2026-10-30: WSL browser container out of threads
|
|
20
121
|
|
|
21
122
|
**What happened**
|