@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 CHANGED
@@ -66,7 +66,9 @@ export async function buildDryRunBundle(root, input) {
66
66
  }
67
67
  return {
68
68
  schema_version: SCHEMA_VERSION,
69
- id: makeBridgeId("sess", `${randomUUID().slice(0, 8)}-${input.prompt}`),
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,
@@ -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
- const el = document.querySelector('#prompt-textarea,[contenteditable="true"]');
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, beforeSend) {
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
- const blocker = await withManualStepForWindowMode(sourceAwareBrowserBlocker(browserSendBlockerFromError(error), sourceCli, browserCommandOptions), resolveCdpPort(browserPort), sourceCli, browserCommandOptions);
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
- followupBudget = await reserveFollowup(store, {
2040
- thread,
2041
- taskId: continuedFrom,
2042
- userApproved: input.user_approved === true,
2043
- limit
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) {
@@ -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 () => reserveFollowupUnderLock(store, { ...input, conversationKey }));
64
+ }, async () => run(conversationKey));
48
65
  }
49
- async function reserveFollowupUnderLock(store, input) {
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. Reservations are counted before sends
56
- and persist across reconnects, older task references, and different session keys in
57
- the same bridge and conversation. Failed/uncertain attempts count too. At the limit,
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**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@youdie006/prodex",
3
- "version": "0.40.49",
3
+ "version": "0.40.50",
4
4
  "description": "Local receipt bus for coordinating Codex execution with ChatGPT Pro/Projects consultation.",
5
5
  "author": "youdie006",
6
6
  "license": "MIT",