{"record":{"id":"ad34fbffec7a7ef0","repo":"jackwener/OpenCLI","slug":"codex-model-selection-was-inconsistent","errorCode":null,"errorMessage":"Codex model selection was inconsistent.","messagePattern":"Codex model selection was inconsistent\\.","errorType":"exception","errorClass":"CommandExecutionError","httpStatus":null,"severity":"error","filePath":"clis/codex/model.js","lineNumber":235,"sourceCode":"          chosen.dispatchEvent(new MouseEvent('mouseup', cinit));\n          chosen.dispatchEvent(new MouseEvent('click', cinit));\n        } catch {}\n      });\n      return { ok: true, switched: true, chosen: chosenLabel, labels };\n    })()`));\n\n        if (!result.ok) {\n            throw new CommandExecutionError(result.reason, result.detail || '');\n        }\n        if (listOnly) {\n            return result.labels.map((m) => ({ Status: m === current ? 'Active' : 'Available', Model: m }));\n        }\n        const selected = findUniqueModelOption(result.labels || [], name);\n        if (!selected) {\n            throw new CommandExecutionError('No model matched.', `wanted=${name} available=${JSON.stringify(result.labels || [])}`);\n        }\n        if (selected !== result.chosen) {\n            throw new CommandExecutionError('Codex model selection was inconsistent.', `expected=${selected} chosen=${result.chosen}`);\n        }\n        let verified = '';\n        for (let attempt = 0; attempt < 20; attempt += 1) {\n            await page.wait(0.25);\n            const reread = unwrapEvaluateResult(await page.evaluate(`(function() {\n        const re = new RegExp(${patternJson});\n        const composers = Array.from(document.querySelectorAll('[contenteditable=\"true\"]')).filter((el) => el.offsetParent);\n        const last = composers[composers.length - 1];\n        if (!last) return '';\n        let root = last;\n        for (let i = 0; i < 5; i++) root = root.parentElement || root;\n        const btns = Array.from(root.querySelectorAll('button')).filter((b) => b.offsetParent);\n        const match = btns.find((b) => re.test((b.textContent || '').trim()));\n        return match ? (match.textContent || '').trim() : '';\n      })()`));\n            if (modelSelectionVerified(reread, selected)) {\n                verified = reread;\n                break;","sourceCodeStart":217,"sourceCodeEnd":253,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/codex/model.js#L217-L253","documentation":"As a consistency check, the `codex model` command compares the label chosen by the in-page script (`result.chosen`) with the label independently resolved by `findUniqueModelOption` (`selected`). This CommandExecutionError is thrown when they differ, meaning the two matching passes picked different menu items for the same requested name — a sign of nondeterministic or divergent matching logic. A switch should never proceed on that basis.","triggerScenarios":"Running `opencli codex model <name>` where the page script's substring matching (lines 184-201) and the Node-side findUniqueModelOption normalization disagree — e.g., subtle normalization differences (whitespace, 'gpt' stripping, trailing modifiers after kbd removal) cause each pass to select a different label among near-duplicate entries.","commonSituations":"Menu containing near-identical labels differing only in whitespace or badges; a Codex update changing label text between scrape and re-match; labels with keyboard-shortcut remnants altering the page-script match but not the Node match (or vice versa).","solutions":["Retry with the exact full label from `opencli codex model --list` so both passes match the same item uniquely.","Compare expected= vs chosen= in the error detail to see which two labels diverged, then choose a name unique to the intended one.","Update opencli so the in-page matching normalization matches findUniqueModelOption exactly.","If it persists on a specific label, work around by switching via an unambiguous alternative (full version string) and report the drift."],"exampleFix":"// before\nopencli codex model \"speed\"   // page script picks 'Speed ⌘1', node picks 'Speed pro'\n// after\nopencli codex model --list\nopencli codex model \"Speed\"   # exact unique label so both passes agree","handlingStrategy":"validation","validationCode":"const listed = await run('opencli codex model --list');\nconst labels = parseLabels(listed);\n// ensure exactly one unique match so both matching passes agree\nconst norm = s => s.toLowerCase().replace(/\\bgpt[-\\s]*/g,'').replace(/\\s+/g,' ').trim();\nconst hits = labels.filter(l => norm(l) === norm(wanted));\nif (hits.length !== 1) throw new Error('Provide one exact unique label');","typeGuard":"function uniquelyResolvable(labels, name) {\n  const n = s => s.toLowerCase().replace(/\\bgpt[-\\s]*/g,'').replace(/\\s+/g,' ').trim();\n  const exact = labels.filter(l => n(l) === n(name));\n  if (exact.length === 1) return exact[0];\n  const partial = labels.filter(l => n(l).includes(n(name)));\n  return partial.length === 1 ? partial[0] : null;\n}","tryCatchPattern":"try {\n  await switchModel(name);\n} catch (err) {\n  if (err instanceof CommandExecutionError && /inconsistent/.test(err.message)) {\n    // parse expected=/chosen= from detail, retry with the expected label\n  } else throw err;\n}","preventionTips":["Use exact full labels so the in-page and Node matching passes pick the same item","Avoid labels with badges/shortcuts that normalize differently in each pass","Keep opencli's two matching implementations in sync after Codex updates","Retry the switch once when it reproduces intermittently"],"tags":["consistency-check","codex","model-selection","ui-scraping"],"backgroundTag":"selection-mismatch","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}