diff --git a/cli.js b/cli.js
index 039e7a1..c3601a7 100644
--- a/cli.js
+++ b/cli.js
@@ -223,6 +223,239 @@ function resolveWebPort() {
return parsed;
}
+// #region releaseRunPortIfNeeded
+function releaseRunPortIfNeeded(port, host, deps = {}) {
+ const numericPort = parseInt(String(port), 10);
+ if (numericPort !== DEFAULT_WEB_PORT) {
+ return { attempted: false, released: false, pids: [], reason: 'non-default-port' };
+ }
+
+ const processRef = deps.process || process;
+ const runSpawnSync = deps.spawnSync || spawnSync;
+ const logger = deps.logger || console;
+ const killProcess = typeof deps.kill === 'function'
+ ? deps.kill
+ : (typeof processRef.kill === 'function' ? processRef.kill.bind(processRef) : null);
+ const seenPids = new Set();
+ const candidatePids = new Set();
+ const currentPid = Number(processRef.pid);
+ const normalizedHost = typeof host === 'string' ? host.trim().toLowerCase() : '';
+ let released = false;
+ const windowsCommandLineCache = new Map();
+
+ const isManagedRunCommand = (commandLine) => {
+ const normalizedLine = ` ${String(commandLine || '').replace(/\s+/g, ' ').trim()} `;
+ return /(^|[\/\\\s])codexmate(?:\.cmd|\.exe)? run(\s|$)/i.test(normalizedLine)
+ || /(^|[\/\\\s])cli\.js run(\s|$)/i.test(normalizedLine);
+ };
+
+ const normalizeListenerHost = (value) => {
+ const trimmed = String(value || '').trim().toLowerCase();
+ if (!trimmed) {
+ return '';
+ }
+ if (trimmed.startsWith('[') && trimmed.endsWith(']')) {
+ return trimmed.slice(1, -1);
+ }
+ return trimmed.startsWith('::ffff:') ? trimmed.slice('::ffff:'.length) : trimmed;
+ };
+
+ const extractListenerHost = (localAddress) => {
+ const trimmed = String(localAddress || '').trim();
+ if (!trimmed) {
+ return '';
+ }
+ if (trimmed.startsWith('[')) {
+ const closingBracket = trimmed.indexOf(']');
+ if (closingBracket > 0) {
+ return normalizeListenerHost(trimmed.slice(1, closingBracket));
+ }
+ }
+ const lastColon = trimmed.lastIndexOf(':');
+ if (lastColon <= 0) {
+ return normalizeListenerHost(trimmed);
+ }
+ return normalizeListenerHost(trimmed.slice(0, lastColon));
+ };
+
+ const isMatchingWindowsListenerAddress = (localAddress) => {
+ const listenerHost = extractListenerHost(localAddress);
+ if (!listenerHost || !normalizedHost) {
+ return false;
+ }
+ if (normalizedHost === 'localhost') {
+ return listenerHost === '127.0.0.1' || listenerHost === '::1';
+ }
+ if (normalizedHost === '0.0.0.0' || normalizedHost === '::') {
+ return listenerHost === normalizedHost;
+ }
+ return listenerHost === normalizeListenerHost(normalizedHost);
+ };
+
+ const addPidsFromText = (text, targetSet = seenPids) => {
+ if (!targetSet) {
+ return;
+ }
+ const lines = String(text || '').split(/\r?\n/);
+ for (const line of lines) {
+ const tokens = line.trim().split(/\s+/).filter(Boolean);
+ for (const token of tokens) {
+ if (!/^\d+$/.test(token)) {
+ continue;
+ }
+ targetSet.add(Number(token));
+ }
+ }
+ };
+
+ const runCommand = (command, args, options = {}) => {
+ const {
+ stdoutPidSet = seenPids,
+ stderrPidSet = seenPids
+ } = options;
+ const result = runSpawnSync(command, args, { encoding: 'utf8' });
+ if (result && result.stdout) addPidsFromText(result.stdout, stdoutPidSet);
+ if (result && result.stderr) addPidsFromText(result.stderr, stderrPidSet);
+ return result || {};
+ };
+
+ const addManagedRunPidsFromPs = (text, allowedPids = null) => {
+ const lines = String(text || '').split(/\r?\n/);
+ for (const line of lines) {
+ const normalizedLine = ` ${line.replace(/\s+/g, ' ').trim()} `;
+ if (!/(^|[\/\s])codexmate run(\s|$)/.test(normalizedLine) && !/(^|[\/\s])cli\.js run(\s|$)/.test(normalizedLine)) {
+ continue;
+ }
+ const pidMatch = line.match(/^\S+\s+(\d+)\s+/);
+ if (!pidMatch) {
+ continue;
+ }
+ const pid = Number(pidMatch[1]);
+ if (!Number.isFinite(pid) || pid <= 0 || pid === currentPid) {
+ continue;
+ }
+ if (allowedPids && !allowedPids.has(pid)) {
+ continue;
+ }
+ seenPids.add(pid);
+ }
+ };
+
+ const getWindowsProcessCommandLine = (pid) => {
+ if (windowsCommandLineCache.has(pid)) {
+ return windowsCommandLineCache.get(pid);
+ }
+ const result = runCommand(
+ 'powershell',
+ [
+ '-NoProfile',
+ '-Command',
+ `$p = Get-CimInstance Win32_Process -Filter "ProcessId = ${pid}"; if ($p) { $p.CommandLine }`
+ ],
+ { stdoutPidSet: null, stderrPidSet: null }
+ );
+ const commandLine = !result.error && result.status === 0
+ ? String(result.stdout || '').trim()
+ : '';
+ windowsCommandLineCache.set(pid, commandLine);
+ return commandLine;
+ };
+
+ if (processRef.platform === 'win32') {
+ const netstatResult = runCommand('netstat', ['-ano', '-p', 'tcp'], { stdoutPidSet: null, stderrPidSet: null });
+ if (!(netstatResult && netstatResult.error)) {
+ const lines = String(netstatResult.stdout || '').split(/\r?\n/);
+ for (const line of lines) {
+ const parts = line.trim().split(/\s+/);
+ if (parts.length < 5) {
+ continue;
+ }
+ const localAddress = parts[1];
+ const state = parts[3];
+ const pidText = parts[4];
+ if (state !== 'LISTENING' || !localAddress.endsWith(`:${numericPort}`) || !/^\d+$/.test(pidText)) {
+ continue;
+ }
+ if (!isMatchingWindowsListenerAddress(localAddress)) {
+ continue;
+ }
+ candidatePids.add(Number(pidText));
+ }
+ for (const pid of candidatePids) {
+ if (pid === currentPid) {
+ continue;
+ }
+ if (!isManagedRunCommand(getWindowsProcessCommandLine(pid))) {
+ continue;
+ }
+ seenPids.add(pid);
+ const taskkillResult = runCommand(
+ 'taskkill',
+ ['/PID', String(pid), '/F'],
+ { stdoutPidSet: null, stderrPidSet: null }
+ );
+ if (!taskkillResult.error && taskkillResult.status === 0) {
+ released = true;
+ }
+ }
+ }
+ } else {
+ let psResult = null;
+ const readPsResult = () => {
+ if (psResult) {
+ return psResult;
+ }
+ psResult = runCommand('ps', ['-ef'], { stdoutPidSet: null, stderrPidSet: null });
+ return psResult;
+ };
+
+ const lsofResult = runCommand(
+ 'lsof',
+ ['-ti', `tcp:${numericPort}`],
+ { stdoutPidSet: candidatePids, stderrPidSet: null }
+ );
+ const shouldTryFuser = !!(lsofResult && lsofResult.error && lsofResult.error.code === 'ENOENT');
+ if (shouldTryFuser && candidatePids.size === 0) {
+ runCommand(
+ 'fuser',
+ [`${numericPort}/tcp`],
+ { stdoutPidSet: candidatePids, stderrPidSet: candidatePids }
+ );
+ }
+ if (candidatePids.size > 0) {
+ const managedPsResult = readPsResult();
+ if (!(managedPsResult && managedPsResult.error)) {
+ addManagedRunPidsFromPs(managedPsResult.stdout, candidatePids);
+ }
+ }
+ }
+
+ if (processRef.platform !== 'win32' && killProcess && !released && seenPids.size > 0) {
+ for (const pid of seenPids) {
+ if (pid === currentPid) {
+ continue;
+ }
+ try {
+ killProcess(pid, 'SIGKILL');
+ released = true;
+ } catch (_) {}
+ }
+ }
+
+ if (released) {
+ logger.log(`~ 已释放端口 ${numericPort} 占用`);
+ }
+
+ return {
+ attempted: true,
+ released,
+ pids: Array.from(seenPids)
+ .filter((pid) => pid !== currentPid)
+ .sort((a, b) => a - b)
+ };
+}
+// #endregion releaseRunPortIfNeeded
+
function resolveWebHost(options = {}) {
const optionHost = typeof options.host === 'string' ? options.host.trim() : '';
if (optionHost) {
@@ -236,7 +469,6 @@ function resolveWebHost(options = {}) {
}
const EMPTY_CONFIG_FALLBACK_TEMPLATE = `model = "gpt-5.3-codex"
-model_reasoning_effort = "high"
model_context_window = ${DEFAULT_MODEL_CONTEXT_WINDOW}
model_auto_compact_token_limit = ${DEFAULT_MODEL_AUTO_COMPACT_TOKEN_LIMIT}
disable_response_storage = true
@@ -10845,6 +11077,7 @@ function cmdStart(options = {}) {
const port = resolveWebPort();
const host = resolveWebHost(options);
+ releaseRunPortIfNeeded(port, host);
let serverHandle = createWebServer({
htmlPath,
diff --git a/tests/e2e/test-config.js b/tests/e2e/test-config.js
index 7d399d9..9cedd54 100644
--- a/tests/e2e/test-config.js
+++ b/tests/e2e/test-config.js
@@ -408,6 +408,10 @@ preferred_auth_method = "shadow-key"
/^\s*model_auto_compact_token_limit\s*=\s*185000\s*$/m.test(legacyTemplateDefaults.template),
'legacy get-config-template should restore default model_auto_compact_token_limit'
);
+ assert(
+ !/^\s*model_reasoning_effort\s*=.+$/m.test(legacyTemplateDefaults.template),
+ 'legacy get-config-template should keep default medium reasoning without model_reasoning_effort'
+ );
const legacyAddDup = await legacyApi('add-provider', {
name: 'foo.bar',
url: 'https://dup.example.com/v1',
diff --git a/tests/unit/provider-share-command.test.mjs b/tests/unit/provider-share-command.test.mjs
index c9a1e89..94f441d 100644
--- a/tests/unit/provider-share-command.test.mjs
+++ b/tests/unit/provider-share-command.test.mjs
@@ -757,6 +757,87 @@ test('loadAll can refresh in background without flipping the global loading stat
assert.strictEqual(context.initError, '');
});
+test('loadAll falls back to medium for unsupported reasoning effort values while preserving xhigh', async () => {
+ const loadAllSource = extractBlockBySignature(
+ appSource,
+ 'async loadAll(options = {}) {'
+ ).replace(/^async loadAll/, 'async function loadAll');
+ const responses = [
+ {
+ provider: 'alpha',
+ model: 'alpha-model',
+ serviceTier: 'fast',
+ modelReasoningEffort: 'bogus',
+ modelContextWindow: 200000,
+ modelAutoCompactTokenLimit: 180000,
+ configReady: true,
+ initNotice: ''
+ },
+ {
+ provider: 'alpha',
+ model: 'alpha-model',
+ serviceTier: 'fast',
+ modelReasoningEffort: 'xhigh',
+ modelContextWindow: 200000,
+ modelAutoCompactTokenLimit: 180000,
+ configReady: true,
+ initNotice: ''
+ }
+ ];
+ let statusIndex = 0;
+ const loadAll = instantiateFunction(loadAllSource, 'loadAll', {
+ defaultModelContextWindow: 190000,
+ defaultModelAutoCompactTokenLimit: 185000,
+ api: async (action) => {
+ if (action === 'status') {
+ return responses[statusIndex++] || responses[responses.length - 1];
+ }
+ if (action === 'list') {
+ return {
+ providers: [{ name: 'alpha', url: 'https://api.example.com/v1', hasKey: true }]
+ };
+ }
+ throw new Error(`Unexpected api action: ${action}`);
+ }
+ });
+
+ const createContext = () => ({
+ loading: false,
+ initError: '',
+ currentProvider: 'stale-provider',
+ currentModel: 'stale-model',
+ serviceTier: 'fast',
+ modelReasoningEffort: 'high',
+ modelContextWindowInput: '190000',
+ modelAutoCompactTokenLimitInput: '185000',
+ editingCodexBudgetField: '',
+ providersList: [],
+ normalizePositiveIntegerInput(value, label, fallback = '') {
+ const raw = value === undefined || value === null || value === ''
+ ? String(fallback || '')
+ : String(value);
+ const text = raw.trim();
+ const numeric = Number.parseInt(text, 10);
+ if (!Number.isFinite(numeric) || numeric <= 0) {
+ return { ok: false, error: `${label} invalid` };
+ }
+ return { ok: true, value: numeric, text: String(numeric) };
+ },
+ showMessage() {},
+ maybeShowStarPrompt() {},
+ async loadModelsForProvider() {},
+ async loadCodexAuthProfiles() {}
+ });
+
+ const invalidContext = createContext();
+ await loadAll.call(invalidContext);
+ assert.strictEqual(invalidContext.modelReasoningEffort, 'medium');
+
+ const xhighContext = createContext();
+ await loadAll.call(xhighContext);
+ assert.strictEqual(xhighContext.modelReasoningEffort, 'xhigh');
+});
+
test('loadAll treats provider list fetch failures as startup errors and skips model refresh', async () => {
const loadAllSource = extractBlockBySignature(
appSource,
diff --git a/tests/unit/web-run-host.test.mjs b/tests/unit/web-run-host.test.mjs
index 892f87a..d3e4d55 100644
--- a/tests/unit/web-run-host.test.mjs
+++ b/tests/unit/web-run-host.test.mjs
@@ -118,6 +118,16 @@ const resolveWebHostSource = extractFunctionBySignature(
'function resolveWebHost(options = {}) {',
'resolveWebHost'
);
+const cmdStartSource = extractFunctionBySignature(
+ cliContent,
+ 'function cmdStart(options = {}) {',
+ 'cmdStart'
+);
+const releaseRunPortIfNeededSource = extractFunctionBySignature(
+ cliContent,
+ 'function releaseRunPortIfNeeded(port, host, deps = {}) {',
+ 'releaseRunPortIfNeeded'
+);
const resolveWebHost = instantiateFunction(resolveWebHostSource, 'resolveWebHost', {
DEFAULT_WEB_HOST: defaultHostMatch[1],
process: { env: {} }
@@ -153,6 +163,277 @@ test('web auto-open uses IPv6 loopback when binding to IPv6 any address', () =>
);
});
+test('releaseRunPortIfNeeded skips non-default ports', () => {
+ const calls = [];
+ const releaseRunPortIfNeeded = instantiateFunction(releaseRunPortIfNeededSource, 'releaseRunPortIfNeeded', {
+ DEFAULT_WEB_PORT: 3737,
+ spawnSync(command, args) {
+ calls.push([command, args]);
+ return { status: 0, stdout: '', stderr: '' };
+ },
+ process: { platform: 'linux' },
+ console: { log() {} }
+ });
+
+ const result = releaseRunPortIfNeeded(3999, '0.0.0.0');
+ assert.deepStrictEqual(result, {
+ attempted: false,
+ released: false,
+ pids: [],
+ reason: 'non-default-port'
+ });
+ assert.deepStrictEqual(calls, []);
+});
+
+test('releaseRunPortIfNeeded clears default port only after lsof pids map to managed run processes', () => {
+ const calls = [];
+ const killed = [];
+ const logs = [];
+ const releaseRunPortIfNeeded = instantiateFunction(releaseRunPortIfNeededSource, 'releaseRunPortIfNeeded', {
+ DEFAULT_WEB_PORT: 3737,
+ spawnSync(command, args) {
+ calls.push([command, args]);
+ if (command === 'lsof') {
+ return { status: 0, stdout: '1234\n8888\n', stderr: '' };
+ }
+ if (command === 'ps') {
+ return {
+ status: 0,
+ stdout: [
+ 'UID PID PPID C STIME TTY TIME CMD',
+ 'u0_a876 1234 1000 0 1970 ? 00:00:00 node /repo/cli.js run --no-browser',
+ 'u0_a876 8888 1000 0 1970 ? 00:00:00 node /repo/other-server.js'
+ ].join('\n'),
+ stderr: ''
+ };
+ }
+ throw new Error(`unexpected command: ${command}`);
+ },
+ process: {
+ platform: 'linux',
+ kill(pid, signal) {
+ killed.push([pid, signal]);
+ }
+ },
+ console: { log(message) { logs.push(message); } }
+ });
+
+ const result = releaseRunPortIfNeeded(3737, '0.0.0.0');
+ assert.deepStrictEqual(calls, [
+ ['lsof', ['-ti', 'tcp:3737']],
+ ['ps', ['-ef']]
+ ]);
+ assert.deepStrictEqual(killed, [[1234, 'SIGKILL']]);
+ assert.deepStrictEqual(result, {
+ attempted: true,
+ released: true,
+ pids: [1234]
+ });
+ assert.deepStrictEqual(logs, ['~ 已释放端口 3737 占用']);
+});
+
+test('releaseRunPortIfNeeded falls back to non-destructive fuser pids when lsof is unavailable', () => {
+ const calls = [];
+ const killed = [];
+ const releaseRunPortIfNeeded = instantiateFunction(releaseRunPortIfNeededSource, 'releaseRunPortIfNeeded', {
+ DEFAULT_WEB_PORT: 3737,
+ spawnSync(command, args) {
+ calls.push([command, args]);
+ if (command === 'lsof') {
+ return { error: { code: 'ENOENT' }, status: null, stdout: '', stderr: '' };
+ }
+ if (command === 'fuser') {
+ return { status: 0, stdout: '', stderr: '2222 3333' };
+ }
+ if (command === 'ps') {
+ return {
+ status: 0,
+ stdout: [
+ 'UID PID PPID C STIME TTY TIME CMD',
+ 'u0_a876 2222 1000 0 1970 ? 00:00:00 node /repo/cli.js run --no-browser',
+ 'u0_a876 3333 1000 0 1970 ? 00:00:00 /usr/bin/codexmate run'
+ ].join('\n'),
+ stderr: ''
+ };
+ }
+ throw new Error(`unexpected command: ${command}`);
+ },
+ process: {
+ platform: 'linux',
+ kill(pid, signal) {
+ killed.push([pid, signal]);
+ }
+ },
+ console: { log() {} }
+ });
+
+ const result = releaseRunPortIfNeeded(3737, '0.0.0.0');
+ assert.deepStrictEqual(calls, [
+ ['lsof', ['-ti', 'tcp:3737']],
+ ['fuser', ['3737/tcp']],
+ ['ps', ['-ef']]
+ ]);
+ assert.deepStrictEqual(killed, [
+ [2222, 'SIGKILL'],
+ [3333, 'SIGKILL']
+ ]);
+ assert.deepStrictEqual(result, {
+ attempted: true,
+ released: true,
+ pids: [2222, 3333]
+ });
+});
+
+test('releaseRunPortIfNeeded falls back to ps scan for managed run processes', () => {
+ const calls = [];
+ const killed = [];
+ const releaseRunPortIfNeeded = instantiateFunction(releaseRunPortIfNeededSource, 'releaseRunPortIfNeeded', {
+ DEFAULT_WEB_PORT: 3737,
+ spawnSync(command, args) {
+ calls.push([command, args]);
+ if (command === 'lsof') {
+ return { status: 0, stdout: '9001\n9002\n', stderr: '' };
+ }
+ if (command === 'ps') {
+ return {
+ status: 0,
+ stdout: [
+ 'UID PID PPID C STIME TTY TIME CMD',
+ 'u0_a876 9001 1000 0 1970 ? 00:00:00 node /repo/cli.js run --no-browser',
+ 'u0_a876 9002 1000 0 1970 ? 00:00:00 /usr/bin/codexmate run',
+ 'u0_a876 9100 1000 0 1970 ? 00:00:00 node /repo/cli.js config'
+ ].join('\n'),
+ stderr: ''
+ };
+ }
+ throw new Error(`unexpected command: ${command}`);
+ },
+ process: {
+ platform: 'linux',
+ pid: 9002,
+ kill(pid, signal) {
+ killed.push([pid, signal]);
+ }
+ },
+ console: { log() {} }
+ });
+
+ const result = releaseRunPortIfNeeded(3737, '0.0.0.0');
+ assert.deepStrictEqual(calls, [
+ ['lsof', ['-ti', 'tcp:3737']],
+ ['ps', ['-ef']]
+ ]);
+ assert.deepStrictEqual(killed, [[9001, 'SIGKILL']]);
+ assert.deepStrictEqual(result, {
+ attempted: true,
+ released: true,
+ pids: [9001]
+ });
+});
+
+test('releaseRunPortIfNeeded skips ps-based kill when no port-scoped owner pids can be identified', () => {
+ const calls = [];
+ const killed = [];
+ const releaseRunPortIfNeeded = instantiateFunction(releaseRunPortIfNeededSource, 'releaseRunPortIfNeeded', {
+ DEFAULT_WEB_PORT: 3737,
+ spawnSync(command, args) {
+ calls.push([command, args]);
+ if (command === 'lsof') {
+ return { error: { code: 'ENOENT' }, status: null, stdout: '', stderr: '' };
+ }
+ if (command === 'fuser') {
+ return { error: { code: 'ENOENT' }, status: null, stdout: '', stderr: '' };
+ }
+ if (command === 'ps') {
+ throw new Error('ps should not run without port-scoped pid candidates');
+ }
+ throw new Error(`unexpected command: ${command}`);
+ },
+ process: {
+ platform: 'linux',
+ kill(pid, signal) {
+ killed.push([pid, signal]);
+ }
+ },
+ console: { log() {} }
+ });
+
+ const result = releaseRunPortIfNeeded(3737, '0.0.0.0');
+ assert.deepStrictEqual(calls, [
+ ['lsof', ['-ti', 'tcp:3737']],
+ ['fuser', ['3737/tcp']]
+ ]);
+ assert.deepStrictEqual(killed, []);
+ assert.deepStrictEqual(result, {
+ attempted: true,
+ released: false,
+ pids: []
+ });
+});
+
+test('releaseRunPortIfNeeded only taskkills Windows listeners that match host and managed command line', () => {
+ const calls = [];
+ const logs = [];
+ const releaseRunPortIfNeeded = instantiateFunction(releaseRunPortIfNeededSource, 'releaseRunPortIfNeeded', {
+ DEFAULT_WEB_PORT: 3737,
+ spawnSync(command, args) {
+ calls.push([command, args]);
+ if (command === 'netstat') {
+ return {
+ status: 0,
+ stdout: [
+ ' TCP 0.0.0.0:3737 0.0.0.0:0 LISTENING 1111',
+ ' TCP 127.0.0.1:3737 0.0.0.0:0 LISTENING 2222',
+ ' TCP 0.0.0.0:3737 0.0.0.0:0 LISTENING 3333'
+ ].join('\r\n'),
+ stderr: ''
+ };
+ }
+ if (command === 'powershell') {
+ if (String(args[2]).includes('1111')) {
+ return { status: 0, stdout: 'node C:\\repo\\cli.js run --no-browser\r\n', stderr: '' };
+ }
+ if (String(args[2]).includes('3333')) {
+ return { status: 0, stdout: 'node C:\\repo\\other-server.js\r\n', stderr: '' };
+ }
+ throw new Error(`unexpected powershell args: ${args.join(' ')}`);
+ }
+ if (command === 'taskkill') {
+ return { status: 0, stdout: '', stderr: '' };
+ }
+ throw new Error(`unexpected command: ${command}`);
+ },
+ process: { platform: 'win32', pid: 9999 },
+ console: { log(message) { logs.push(message); } }
+ });
+
+ const result = releaseRunPortIfNeeded(3737, '0.0.0.0');
+ assert.deepStrictEqual(calls, [
+ ['netstat', ['-ano', '-p', 'tcp']],
+ ['powershell', ['-NoProfile', '-Command', '$p = Get-CimInstance Win32_Process -Filter "ProcessId = 1111"; if ($p) { $p.CommandLine }']],
+ ['taskkill', ['/PID', '1111', '/F']],
+ ['powershell', ['-NoProfile', '-Command', '$p = Get-CimInstance Win32_Process -Filter "ProcessId = 3333"; if ($p) { $p.CommandLine }']]
+ ]);
+ assert.deepStrictEqual(result, {
+ attempted: true,
+ released: true,
+ pids: [1111]
+ });
+ assert.deepStrictEqual(logs, ['~ 已释放端口 3737 占用']);
+});
+
+test('cmdStart releases the resolved port before creating the web server', () => {
+ const resolveIndex = cmdStartSource.indexOf('resolveWebPort(');
+ const releaseIndex = cmdStartSource.indexOf('releaseRunPortIfNeeded(port, host)');
+ const createIndex = cmdStartSource.indexOf('createWebServer(');
+
+ assert(resolveIndex >= 0, 'cmdStart should resolve the web port');
+ assert(releaseIndex >= 0, 'cmdStart should release the run port with host before startup');
+ assert(createIndex >= 0, 'cmdStart should create the web server');
+ assert(resolveIndex < releaseIndex, 'cmdStart should resolve the port before releasing it');
+ assert(releaseIndex < createIndex, 'cmdStart should release the port before creating the web server');
+});
+
const getCodexSkillsDirSource = extractFunctionBySignature(
cliContent,
'function getCodexSkillsDir() {',
diff --git a/tests/unit/web-ui-behavior-parity.test.mjs b/tests/unit/web-ui-behavior-parity.test.mjs
index 5182ed1..9c3b0c7 100644
--- a/tests/unit/web-ui-behavior-parity.test.mjs
+++ b/tests/unit/web-ui-behavior-parity.test.mjs
@@ -105,7 +105,7 @@ function createLoadAllContext() {
currentProvider: 'existing-provider',
currentModel: 'existing-model',
serviceTier: 'fast',
- modelReasoningEffort: 'high',
+ modelReasoningEffort: 'medium',
modelContextWindowInput: 'dirty-context',
modelAutoCompactTokenLimitInput: 'dirty-limit',
editingCodexBudgetField: 'modelContextWindowInput',
diff --git a/web-ui/app.js b/web-ui/app.js
index 4c4da4f..cc13d0e 100644
--- a/web-ui/app.js
+++ b/web-ui/app.js
@@ -33,7 +33,7 @@ document.addEventListener('DOMContentLoaded', () => {
currentProvider: '',
currentModel: '',
serviceTier: 'fast',
- modelReasoningEffort: 'high',
+ modelReasoningEffort: 'medium',
modelContextWindowInput: String(DEFAULT_MODEL_CONTEXT_WINDOW),
modelAutoCompactTokenLimitInput: String(DEFAULT_MODEL_AUTO_COMPACT_TOKEN_LIMIT),
editingCodexBudgetField: '',
diff --git a/web-ui/modules/app.methods.startup-claude.mjs b/web-ui/modules/app.methods.startup-claude.mjs
index 1e3afa7..703b8df 100644
--- a/web-ui/modules/app.methods.startup-claude.mjs
+++ b/web-ui/modules/app.methods.startup-claude.mjs
@@ -40,7 +40,8 @@ export function createStartupClaudeMethods(options = {}) {
const effort = typeof statusRes.modelReasoningEffort === 'string'
? statusRes.modelReasoningEffort.trim().toLowerCase()
: '';
- this.modelReasoningEffort = effort || 'high';
+ const allowedReasoningEfforts = new Set(['low', 'medium', 'high', 'xhigh']);
+ this.modelReasoningEffort = allowedReasoningEfforts.has(effort) ? effort : 'medium';
}
{
const contextWindow = this.normalizePositiveIntegerInput(
diff --git a/web-ui/partials/index/panel-config-codex.html b/web-ui/partials/index/panel-config-codex.html
index 32f8516..0c971e7 100644
--- a/web-ui/partials/index/panel-config-codex.html
+++ b/web-ui/partials/index/panel-config-codex.html
@@ -78,8 +78,8 @@
推理强度