From 7576bcd3ff91849510859c9ffd2218e7448dafc0 Mon Sep 17 00:00:00 2001 From: Eyejoker Date: Sat, 2 May 2026 01:42:04 +0900 Subject: [PATCH] Update agent SDKs and add gated Codex goals support (#114) * add gated codex goals support * sync README SDK versions * bump claude agent sdk * add codex goals settings toggle --- README.md | 9 +- apps/dashboard/src/SettingsPanel.test.ts | 1 + apps/dashboard/src/SettingsPanel.tsx | 103 ++++++++++++++++++ apps/dashboard/src/api.ts | 32 ++++++ runners/agent-runner/bun.lock | 20 ++-- runners/agent-runner/package.json | 2 +- runners/codex-runner/bun.lock | 16 +-- runners/codex-runner/package.json | 4 +- runners/codex-runner/src/app-server-client.ts | 88 ++++++++++++++- runners/codex-runner/src/index.ts | 6 + .../test/app-server-client.test.ts | 87 +++++++++++++++ src/agent-runner-environment.test.ts | 73 +++++++++++++ src/agent-runner-environment.ts | 10 ++ src/agent-runner.test.ts | 56 ++++++++++ src/agent-runner.ts | 9 +- src/settings-store.test.ts | 45 ++++++++ src/settings-store.ts | 34 ++++++ src/types.ts | 1 + src/web-dashboard-settings-routes.test.ts | 22 ++++ src/web-dashboard-settings-routes.ts | 28 +++++ 20 files changed, 616 insertions(+), 30 deletions(-) create mode 100644 runners/codex-runner/test/app-server-client.test.ts create mode 100644 src/settings-store.test.ts diff --git a/README.md b/README.md index 55cf0ba..1cdc3cd 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # EJClaw ![Version](https://img.shields.io/badge/version-0.2.3-blue) -![Claude Agent SDK](https://img.shields.io/badge/Claude_Agent_SDK-0.2.101-blueviolet) -![Codex SDK](https://img.shields.io/badge/Codex_SDK-0.120.0-green) +![Claude Agent SDK](https://img.shields.io/badge/Claude_Agent_SDK-0.2.126-blueviolet) +![Codex SDK](https://img.shields.io/badge/Codex_SDK-0.128.0-green) ![Bun](https://img.shields.io/badge/Bun-1.3+-f9f1e1?logo=bun&logoColor=black) ![Discord](https://img.shields.io/badge/Discord-Tribunal-5865F2?logo=discord&logoColor=white) @@ -115,6 +115,11 @@ Discord ──► SQLite (WAL) ──► GroupQueue ──┬──► Owner (ho - Codex CLI - Discord 봇 토큰 3개(owner / reviewer / arbiter) +현재 runner 번들 기준 버전: + +- Claude Agent SDK: `@anthropic-ai/claude-agent-sdk@0.2.126` +- Codex SDK/CLI: `@openai/codex@0.128.0` + ### 설치 ```bash diff --git a/apps/dashboard/src/SettingsPanel.test.ts b/apps/dashboard/src/SettingsPanel.test.ts index ec7aa2f..42ec421 100644 --- a/apps/dashboard/src/SettingsPanel.test.ts +++ b/apps/dashboard/src/SettingsPanel.test.ts @@ -34,6 +34,7 @@ describe('SettingsPanel', () => { expect(html).toContain('모델'); expect(html).toContain('MoA 참조 모델'); expect(html).toContain('패스트 모드'); + expect(html).toContain('Codex 실험 기능'); expect(html).toContain('불러오는 중'); expect(html).toContain('Claude'); expect(html).toContain('계정'); diff --git a/apps/dashboard/src/SettingsPanel.tsx b/apps/dashboard/src/SettingsPanel.tsx index 978039c..eed5ddb 100644 --- a/apps/dashboard/src/SettingsPanel.tsx +++ b/apps/dashboard/src/SettingsPanel.tsx @@ -3,17 +3,20 @@ import { useEffect, useState } from 'react'; import { type ClaudeAccountSummary, type CodexAccountSummary, + type CodexFeatureSnapshot, type FastModeSnapshot, type ModelConfigSnapshot, type ModelRoleConfig, addClaudeAccount, deleteAccount, fetchAccounts, + fetchCodexFeatures, fetchFastMode, fetchModelConfig, refreshAllCodexAccounts as refreshAllCodexAccountsApi, refreshCodexAccount as refreshCodexAccountApi, setCurrentCodexAccount as setCurrentCodexAccountApi, + updateCodexFeatures, updateFastMode, updateModels, } from './api'; @@ -82,6 +85,8 @@ export function SettingsPanel({ + + ); @@ -302,6 +307,104 @@ function FastModeSettings() { ); } +function CodexFeatureSettings({ + onRestartStack, +}: { + onRestartStack: () => void; +}) { + const [state, setState] = useState(null); + const [busy, setBusy] = useState(false); + const [error, setError] = useState(null); + const [savedAt, setSavedAt] = useState(null); + + useEffect(() => { + let cancelled = false; + fetchCodexFeatures() + .then((snapshot) => { + if (cancelled) return; + setState(snapshot); + setError(null); + }) + .catch((err) => { + if (cancelled) return; + setError(err instanceof Error ? err.message : String(err)); + }); + return () => { + cancelled = true; + }; + }, []); + + async function toggleGoals() { + if (!state) return; + const previous = state; + const optimistic = { ...state, goals: !state.goals }; + setState(optimistic); + setBusy(true); + setError(null); + try { + const fresh = await updateCodexFeatures({ goals: optimistic.goals }); + setState(fresh); + setSavedAt(Date.now()); + } catch (err) { + setState(previous); + setError(err instanceof Error ? err.message : String(err)); + } finally { + setBusy(false); + } + } + + return ( +
+

Codex 실험 기능

+ {error ?

{error}

: null} + {!state ? ( +

불러오는 중…

+ ) : ( + <> + +
+ {savedAt ? ( + + 저장됨. 적용하려면 스택 재시작 필요. + + ) : null} + +
+ + )} +
+ ); +} + function formatExpiry( iso: string | null, ): { label: string; cls: string } | null { diff --git a/apps/dashboard/src/api.ts b/apps/dashboard/src/api.ts index b317573..baf96e6 100644 --- a/apps/dashboard/src/api.ts +++ b/apps/dashboard/src/api.ts @@ -388,6 +388,10 @@ export interface FastModeSnapshot { claude: boolean; } +export interface CodexFeatureSnapshot { + goals: boolean; +} + export interface MoaReferenceStatus { model: string; checkedAt: string; @@ -538,6 +542,34 @@ export async function updateFastMode( return (await response.json()) as FastModeSnapshot; } +export async function fetchCodexFeatures(): Promise { + return fetchJson('/api/settings/codex-features'); +} + +export async function updateCodexFeatures( + input: Partial, +): Promise { + const response = await fetch('/api/settings/codex-features', { + method: 'PUT', + headers: { + accept: 'application/json', + 'content-type': 'application/json', + }, + body: JSON.stringify(input), + }); + if (!response.ok) { + let msg = `update codex features failed: ${response.status}`; + try { + const payload = (await response.json()) as { error?: string }; + if (payload.error) msg = payload.error; + } catch { + /* ignore */ + } + throw new Error(msg); + } + return (await response.json()) as CodexFeatureSnapshot; +} + export async function fetchMoaSettings(): Promise { return fetchJson('/api/settings/moa'); } diff --git a/runners/agent-runner/bun.lock b/runners/agent-runner/bun.lock index b550b8c..ed607ce 100644 --- a/runners/agent-runner/bun.lock +++ b/runners/agent-runner/bun.lock @@ -5,7 +5,7 @@ "": { "name": "ejclaw-agent-runner", "dependencies": { - "@anthropic-ai/claude-agent-sdk": "0.2.114", + "@anthropic-ai/claude-agent-sdk": "0.2.126", "@modelcontextprotocol/sdk": "^1.12.1", "cron-parser": "^5.0.0", "ejclaw-runners-shared": "file:../shared", @@ -18,23 +18,23 @@ }, }, "packages": { - "@anthropic-ai/claude-agent-sdk": ["@anthropic-ai/claude-agent-sdk@0.2.114", "", { "dependencies": { "@anthropic-ai/sdk": "^0.81.0", "@modelcontextprotocol/sdk": "^1.29.0" }, "optionalDependencies": { "@anthropic-ai/claude-agent-sdk-darwin-arm64": "0.2.114", "@anthropic-ai/claude-agent-sdk-darwin-x64": "0.2.114", "@anthropic-ai/claude-agent-sdk-linux-arm64": "0.2.114", "@anthropic-ai/claude-agent-sdk-linux-arm64-musl": "0.2.114", "@anthropic-ai/claude-agent-sdk-linux-x64": "0.2.114", "@anthropic-ai/claude-agent-sdk-linux-x64-musl": "0.2.114", "@anthropic-ai/claude-agent-sdk-win32-arm64": "0.2.114", "@anthropic-ai/claude-agent-sdk-win32-x64": "0.2.114" }, "peerDependencies": { "zod": "^4.0.0" } }, "sha512-plJ+j17jew9tDMHir/90hXrwoB8cZ9GrIyG19zIJcFyQ8pVhRXjZRJCtF2ElfPoiwkxMmNu1Klqyui4xP4shPg=="], + "@anthropic-ai/claude-agent-sdk": ["@anthropic-ai/claude-agent-sdk@0.2.126", "", { "dependencies": { "@anthropic-ai/sdk": "^0.81.0", "@modelcontextprotocol/sdk": "^1.29.0" }, "optionalDependencies": { "@anthropic-ai/claude-agent-sdk-darwin-arm64": "0.2.126", "@anthropic-ai/claude-agent-sdk-darwin-x64": "0.2.126", "@anthropic-ai/claude-agent-sdk-linux-arm64": "0.2.126", "@anthropic-ai/claude-agent-sdk-linux-arm64-musl": "0.2.126", "@anthropic-ai/claude-agent-sdk-linux-x64": "0.2.126", "@anthropic-ai/claude-agent-sdk-linux-x64-musl": "0.2.126", "@anthropic-ai/claude-agent-sdk-win32-arm64": "0.2.126", "@anthropic-ai/claude-agent-sdk-win32-x64": "0.2.126" }, "peerDependencies": { "zod": "^4.0.0" } }, "sha512-4ZrVu0XUEwNG6wxvsLgppRAmSfAf3oeEMEUPhgazb0AXUUe/7W8MxwZKJWOffqSLWaNYzOt3ZCIL7NJY6toqWw=="], - "@anthropic-ai/claude-agent-sdk-darwin-arm64": ["@anthropic-ai/claude-agent-sdk-darwin-arm64@0.2.114", "", { "os": "darwin", "cpu": "arm64" }, "sha512-0/6LWrNilWpmiX6Xrj5plsBmCrCdKGERgAlKUZQEJZplnfuweFAJu7WXZB4KBaUpGlPO91zB/yqDh6kp5aZFbA=="], + "@anthropic-ai/claude-agent-sdk-darwin-arm64": ["@anthropic-ai/claude-agent-sdk-darwin-arm64@0.2.126", "", { "os": "darwin", "cpu": "arm64" }, "sha512-JFlJBbeAlx7Ic5s4lGUN9SppobryXk/lIqPCvhp6KrJTQIerh3MIBzxsVIJ0MaDut7jVni/oYgsvDni7NIyqHA=="], - "@anthropic-ai/claude-agent-sdk-darwin-x64": ["@anthropic-ai/claude-agent-sdk-darwin-x64@0.2.114", "", { "os": "darwin", "cpu": "x64" }, "sha512-sOHxq1rEO/KZg2iEZILTPn62lMRRMPqtxKx41uGLi3xjVDrAej6Ury9dDZjYBKkK9n4kBylXV0Oom2CZ14dDYw=="], + "@anthropic-ai/claude-agent-sdk-darwin-x64": ["@anthropic-ai/claude-agent-sdk-darwin-x64@0.2.126", "", { "os": "darwin", "cpu": "x64" }, "sha512-J8BpMj16NK9FUaG3HnHSivyp4Xww9DKWHiC8QSHT9oiT8pH5IG7nl0jxyjIq/lY79evlTY+ubgDVWlMUhUAN/g=="], - "@anthropic-ai/claude-agent-sdk-linux-arm64": ["@anthropic-ai/claude-agent-sdk-linux-arm64@0.2.114", "", { "os": "linux", "cpu": "arm64" }, "sha512-j/SfEoN6+fyEsp8EuPe+xKcGfsZtaBmdUUH+YSRk5H/lYgy38yNsDhdt+AJMQcdMKfHsiwZ3Y9Ajoe9G9wNwHQ=="], + "@anthropic-ai/claude-agent-sdk-linux-arm64": ["@anthropic-ai/claude-agent-sdk-linux-arm64@0.2.126", "", { "os": "linux", "cpu": "arm64" }, "sha512-LM+mnfQsgI+1i5mYZwIPDDf14NGBu5wbhzm5U8P11dCa2p8sXmKoWpkbO16BFM2NxeW44I/RXCxE5qFsbz4zcg=="], - "@anthropic-ai/claude-agent-sdk-linux-arm64-musl": ["@anthropic-ai/claude-agent-sdk-linux-arm64-musl@0.2.114", "", { "os": "linux", "cpu": "arm64" }, "sha512-Mhd7bumTwWvkgjSJnYvCgyt8DfmLiUoK92mfvAKxHX7i5YSw+h5Kprqh2Cap+2SBbpwZvnwIoEYGCxhGwE5ddg=="], + "@anthropic-ai/claude-agent-sdk-linux-arm64-musl": ["@anthropic-ai/claude-agent-sdk-linux-arm64-musl@0.2.126", "", { "os": "linux", "cpu": "arm64" }, "sha512-GO0BnIUw3LQ3XAy+nipAabkN0GwQGPhHB6ITI4XLoR99fLHB3TA6WfyvTf0fnpxd25A+c/+UsAoxz4zBQaHlhA=="], - "@anthropic-ai/claude-agent-sdk-linux-x64": ["@anthropic-ai/claude-agent-sdk-linux-x64@0.2.114", "", { "os": "linux", "cpu": "x64" }, "sha512-wbaExKDleLlm2zHEhb74GKMLVhtO0IUmFhdimQcdL6CdTkmDE8ZJi53tYWE9+jq+XWNRXoM2yEmKPzXoUmsJng=="], + "@anthropic-ai/claude-agent-sdk-linux-x64": ["@anthropic-ai/claude-agent-sdk-linux-x64@0.2.126", "", { "os": "linux", "cpu": "x64" }, "sha512-yaOTDcYCdscxC0LKg9w8IwSa5g+993WggFZJBTZpqvflA2+WMQeTarDnKlsFTCw9XUZkL8XZeBALYJGx0HutuA=="], - "@anthropic-ai/claude-agent-sdk-linux-x64-musl": ["@anthropic-ai/claude-agent-sdk-linux-x64-musl@0.2.114", "", { "os": "linux", "cpu": "x64" }, "sha512-c1URsameGHAcghen+mY6jvr2oypiAPHXJIdP4huxR25zPdXWv2x+BCy+vcRVeajsq4VmFzAyQJwaM+BXkmXjAw=="], + "@anthropic-ai/claude-agent-sdk-linux-x64-musl": ["@anthropic-ai/claude-agent-sdk-linux-x64-musl@0.2.126", "", { "os": "linux", "cpu": "x64" }, "sha512-ByJGO0+mu7EplxSFSCIHd7QWsXdrF3qgtzQ177o/j+oSppLoqR1ot5ktf8aw5oR3CC5lFHg4tqd6TnneQpEoIg=="], - "@anthropic-ai/claude-agent-sdk-win32-arm64": ["@anthropic-ai/claude-agent-sdk-win32-arm64@0.2.114", "", { "os": "win32", "cpu": "arm64" }, "sha512-qeWdUpQymcKCA92osPmffG4QogrOSvuffPvm6c2OlMDjCPYs8vKG7bSe1Vq5tP9tfBszKPVJWBDh+2ANkNissQ=="], + "@anthropic-ai/claude-agent-sdk-win32-arm64": ["@anthropic-ai/claude-agent-sdk-win32-arm64@0.2.126", "", { "os": "win32", "cpu": "arm64" }, "sha512-gv3MOsOBkCx3LajOOIjD7AKsOtz/qNHsS2oshGt2GVoy7JA3XbCDeCetDjM6SorV4SE+7F/IH0UJdXe5ejI/Zg=="], - "@anthropic-ai/claude-agent-sdk-win32-x64": ["@anthropic-ai/claude-agent-sdk-win32-x64@0.2.114", "", { "os": "win32", "cpu": "x64" }, "sha512-nVr43WwsKvWA6rojw15qBS/f31srukdLxy1KwKzpftlpmkzQ9Lh8uhIafOmoIPzz67f8VJ8JqHE0caA5YrhX9A=="], + "@anthropic-ai/claude-agent-sdk-win32-x64": ["@anthropic-ai/claude-agent-sdk-win32-x64@0.2.126", "", { "os": "win32", "cpu": "x64" }, "sha512-oRV75HwyoOd1/t5+kipAM2g62CaElpKGvSBx3Ys4lCwCiFUyOnmet/O+hRXENsY6ShDeQZEcJL2UWljr2d5NQw=="], "@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.81.0", "", { "dependencies": { "json-schema-to-ts": "^3.1.1" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["zod"], "bin": { "anthropic-ai-sdk": "bin/cli" } }, "sha512-D4K5PvEV6wPiRtVlVsJHIUhHAmOZ6IT/I9rKlTf84gR7GyyAurPJK7z9BOf/AZqC5d1DhYQGJNKRmV+q8dGhgw=="], diff --git a/runners/agent-runner/package.json b/runners/agent-runner/package.json index 1727ebd..e75ff9b 100644 --- a/runners/agent-runner/package.json +++ b/runners/agent-runner/package.json @@ -10,7 +10,7 @@ "start": "bun dist/index.js" }, "dependencies": { - "@anthropic-ai/claude-agent-sdk": "0.2.114", + "@anthropic-ai/claude-agent-sdk": "0.2.126", "@modelcontextprotocol/sdk": "^1.12.1", "cron-parser": "^5.0.0", "ejclaw-runners-shared": "file:../shared", diff --git a/runners/codex-runner/bun.lock b/runners/codex-runner/bun.lock index efcfdf2..5e3c4a3 100644 --- a/runners/codex-runner/bun.lock +++ b/runners/codex-runner/bun.lock @@ -5,7 +5,7 @@ "": { "name": "ejclaw-codex-runner", "dependencies": { - "@openai/codex": "^0.124.0", + "@openai/codex": "0.128.0", "ejclaw-runners-shared": "file:../shared", }, "devDependencies": { @@ -15,19 +15,19 @@ }, }, "packages": { - "@openai/codex": ["@openai/codex@0.124.0", "", { "optionalDependencies": { "@openai/codex-darwin-arm64": "npm:@openai/codex@0.124.0-darwin-arm64", "@openai/codex-darwin-x64": "npm:@openai/codex@0.124.0-darwin-x64", "@openai/codex-linux-arm64": "npm:@openai/codex@0.124.0-linux-arm64", "@openai/codex-linux-x64": "npm:@openai/codex@0.124.0-linux-x64", "@openai/codex-win32-arm64": "npm:@openai/codex@0.124.0-win32-arm64", "@openai/codex-win32-x64": "npm:@openai/codex@0.124.0-win32-x64" }, "bin": { "codex": "bin/codex.js" } }, "sha512-1EVAuPyAQZ8zIVMw3bPJ6a4R8ifLAZ7LGsOyknj5c2he9AFXVRCmWx12WrdZJ25wcBvOEKt1n1Zx+QAj0EVGbQ=="], + "@openai/codex": ["@openai/codex@0.128.0", "", { "optionalDependencies": { "@openai/codex-darwin-arm64": "npm:@openai/codex@0.128.0-darwin-arm64", "@openai/codex-darwin-x64": "npm:@openai/codex@0.128.0-darwin-x64", "@openai/codex-linux-arm64": "npm:@openai/codex@0.128.0-linux-arm64", "@openai/codex-linux-x64": "npm:@openai/codex@0.128.0-linux-x64", "@openai/codex-win32-arm64": "npm:@openai/codex@0.128.0-win32-arm64", "@openai/codex-win32-x64": "npm:@openai/codex@0.128.0-win32-x64" }, "bin": { "codex": "bin/codex.js" } }, "sha512-+xp6ODmFfBNnexIWRHApEaPXot2j6gyM8A5we/5IS/uY4eYHj4arETct4hQ5M4eO+MK7JY3ZU4xhuobhlysr0A=="], - "@openai/codex-darwin-arm64": ["@openai/codex@0.124.0-darwin-arm64", "", { "os": "darwin", "cpu": "arm64" }, "sha512-lnuqeAdl+RjPWsqVZ8rrPskRIOZmzlyr8e5q/wFVEnMPsm9dWgjRW1PKa84UmDSQVOuz9GObF28DEHFyC4A8NA=="], + "@openai/codex-darwin-arm64": ["@openai/codex@0.128.0-darwin-arm64", "", { "os": "darwin", "cpu": "arm64" }, "sha512-w+6zohfHx/kHBdles/CyFKaY57u9I3nK8QI9+NrdwMliKA0b7xn13yblRNkMpe09j6vL1oAWoxYsMOQ/vjBGug=="], - "@openai/codex-darwin-x64": ["@openai/codex@0.124.0-darwin-x64", "", { "os": "darwin", "cpu": "x64" }, "sha512-Br+VlL83IOu96Urw+mkZ1PQXLsQXGAYEH6kXNt4ULuVt7qAdQY2FKYwIgLLVLl0vpMk8qWxdfdVMqhiu2uCHlg=="], + "@openai/codex-darwin-x64": ["@openai/codex@0.128.0-darwin-x64", "", { "os": "darwin", "cpu": "x64" }, "sha512-SDbn6fO22Puy8xmMIbZi4f2znMrUEPwABApke4mo+4ihaauwuVjeqzXvW5SPJz5ty/bG11/mSupQgReT7T8BBw=="], - "@openai/codex-linux-arm64": ["@openai/codex@0.124.0-linux-arm64", "", { "os": "linux", "cpu": "arm64" }, "sha512-QXafFVQJxPfU1LSCI5afuHsF5evKAcbQpFuKou0Kl241/HG/zYHdwoWj546zH844gJgegIPAxPf36+RSkUsbbA=="], + "@openai/codex-linux-arm64": ["@openai/codex@0.128.0-linux-arm64", "", { "os": "linux", "cpu": "arm64" }, "sha512-+SvH73H60qvCXFuQGP/EsmR//s1hHMBR22PvJkXvM/hdnTIGucx+JqRUjAWdmmQ1IU6j3kgwVvdLW/6ICB+M6w=="], - "@openai/codex-linux-x64": ["@openai/codex@0.124.0-linux-x64", "", { "os": "linux", "cpu": "x64" }, "sha512-cJ4QfQIrz2gkXVWephiGo9JDyD3qLKhvkTTLk7gI8rKd7MZpVXn9/1e7pZCQnJVA7Dk6ocA5G+sxnR78SoIejw=="], + "@openai/codex-linux-x64": ["@openai/codex@0.128.0-linux-x64", "", { "os": "linux", "cpu": "x64" }, "sha512-2lnSPA05CRRuKAzFW8BCmmNCSieDcToLwfC2ALLbBYilGLgzhRibjlDglK9F1BkEzfohSSWJu4PBbRu/aG60lQ=="], - "@openai/codex-win32-arm64": ["@openai/codex@0.124.0-win32-arm64", "", { "os": "win32", "cpu": "arm64" }, "sha512-oDSI/CQh0kagBwWVPG9EiUBfHiY27ju3LPrjTVZg4VMpVJVNA31JTK8rHMZ6G/2gfNmOl6DMBA6+0ICxSkkshg=="], + "@openai/codex-win32-arm64": ["@openai/codex@0.128.0-win32-arm64", "", { "os": "win32", "cpu": "arm64" }, "sha512-ECJvsqmYFdA9pn42xxK3Odp/G16AjmBW0BglX8L0PwPjqbstbmlew9bfHf7xvL+SNfNl4NmyotW0+RNo1phgaA=="], - "@openai/codex-win32-x64": ["@openai/codex@0.124.0-win32-x64", "", { "os": "win32", "cpu": "x64" }, "sha512-t+lAwmCWwIWQKk6dKaYetRhQsmA+uDmQy1NLka1xAAv3PlNOH8iNz9Lsisr3qYkBR8Tf7tbQlt+pl9tw/A5mfA=="], + "@openai/codex-win32-x64": ["@openai/codex@0.128.0-win32-x64", "", { "os": "win32", "cpu": "x64" }, "sha512-k3jmUAFrzkUtvjGTXvSKjQqJLLlzjxp/VoHJDYedgmXUn6j70HxK38IwapzmnYfiBiTuzETvGwjXHzZgzKjhoQ=="], "@types/node": ["@types/node@22.19.15", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg=="], diff --git a/runners/codex-runner/package.json b/runners/codex-runner/package.json index 66788ca..4782b43 100644 --- a/runners/codex-runner/package.json +++ b/runners/codex-runner/package.json @@ -10,8 +10,8 @@ "start": "bun dist/index.js" }, "dependencies": { - "ejclaw-runners-shared": "file:../shared", - "@openai/codex": "^0.124.0" + "@openai/codex": "0.128.0", + "ejclaw-runners-shared": "file:../shared" }, "devDependencies": { "@types/node": "^22.10.7", diff --git a/runners/codex-runner/src/app-server-client.ts b/runners/codex-runner/src/app-server-client.ts index 333be05..a7f7aef 100644 --- a/runners/codex-runner/src/app-server-client.ts +++ b/runners/codex-runner/src/app-server-client.ts @@ -42,6 +42,53 @@ export interface CodexAppServerTurnResult { result: string | null; } +export interface CodexThreadGoal { + threadId: string; + objective: string; + status: string; + tokenBudget: number | null; + tokensUsed: number; + timeUsedSeconds: number; + createdAt: number; + updatedAt: number; +} + +export function buildCodexAppServerArgs(args: { + codexBin: string; + enableGoals?: boolean; +}): string[] { + return args.enableGoals === true + ? [args.codexBin, '--enable', 'goals', 'app-server'] + : [args.codexBin, 'app-server']; +} + +function isCodexThreadGoal(value: unknown): value is CodexThreadGoal { + if (!value || typeof value !== 'object') return false; + const goal = value as Partial; + return ( + typeof goal.threadId === 'string' && + typeof goal.objective === 'string' && + typeof goal.status === 'string' + ); +} + +function extractOptionalGoalResult(result: unknown): CodexThreadGoal | null { + const goal = (result as { goal?: unknown } | null)?.goal ?? null; + if (goal === null) return null; + if (!isCodexThreadGoal(goal)) { + throw new Error('Codex app-server returned an invalid goal payload.'); + } + return goal; +} + +function extractGoalResult(result: unknown, method: string): CodexThreadGoal { + const goal = extractOptionalGoalResult(result); + if (!goal) { + throw new Error(`Codex app-server ${method} did not return a goal.`); + } + return goal; +} + interface JsonRpcResponse { id: number; result?: unknown; @@ -79,12 +126,14 @@ export interface CodexAppServerClientOptions { cwd: string; env?: NodeJS.ProcessEnv; log: (message: string) => void; + enableGoals?: boolean; } export class CodexAppServerClient { private readonly cwd: string; private readonly env: NodeJS.ProcessEnv; private readonly log: (message: string) => void; + private readonly enableGoals: boolean; private readonly pending = new Map(); private readonly require = createRequire(import.meta.url); private nextId = 1; @@ -96,6 +145,7 @@ export class CodexAppServerClient { this.cwd = options.cwd; this.env = options.env || process.env; this.log = options.log; + this.enableGoals = options.enableGoals === true; } async start(): Promise { @@ -108,11 +158,18 @@ export class CodexAppServerClient { 'codex.js', ); - this.proc = spawn(process.execPath, [codexBin, 'app-server'], { - cwd: this.cwd, - env: this.env, - stdio: ['pipe', 'pipe', 'pipe'], - }); + this.proc = spawn( + process.execPath, + buildCodexAppServerArgs({ + codexBin, + enableGoals: this.enableGoals, + }), + { + cwd: this.cwd, + env: this.env, + stdio: ['pipe', 'pipe', 'pipe'], + }, + ); this.proc.stdout.setEncoding('utf8'); this.proc.stdout.on('data', (chunk: string) => { @@ -312,6 +369,27 @@ export class CodexAppServerClient { return turnPromise; } + async threadGoalSet( + threadId: string, + objective: string, + ): Promise { + const result = await this.request('thread/goal/set', { + threadId, + objective, + }); + return extractGoalResult(result, 'thread/goal/set'); + } + + async threadGoalGet(threadId: string): Promise { + const result = await this.request('thread/goal/get', { threadId }); + return extractOptionalGoalResult(result); + } + + async threadGoalClear(threadId: string): Promise { + const result = await this.request('thread/goal/clear', { threadId }); + return (result as { cleared?: boolean } | null)?.cleared === true; + } + private handleStdoutLine(line: string): void { let message: JsonRpcResponse | JsonRpcNotification | JsonRpcServerRequest; try { diff --git a/runners/codex-runner/src/index.ts b/runners/codex-runner/src/index.ts index de859ae..252e492 100644 --- a/runners/codex-runner/src/index.ts +++ b/runners/codex-runner/src/index.ts @@ -52,6 +52,7 @@ interface RunnerInput { isScheduledTask?: boolean; assistantName?: string; agentType?: string; + codexGoals?: boolean; roomRoleContext?: RoomRoleContext; } @@ -95,6 +96,10 @@ function log(message: string): void { console.error(`[codex-runner] ${message}`); } +function isCodexGoalsEnabled(runnerInput: RunnerInput): boolean { + return runnerInput.codexGoals === true || process.env.CODEX_GOALS === 'true'; +} + async function readStdin(): Promise { return new Promise((resolve, reject) => { let data = ''; @@ -343,6 +348,7 @@ async function runAppServerSession( cwd: EFFECTIVE_CWD, env: clientEnv, log, + enableGoals: isCodexGoalsEnabled(runnerInput), }); await client.start(); diff --git a/runners/codex-runner/test/app-server-client.test.ts b/runners/codex-runner/test/app-server-client.test.ts new file mode 100644 index 0000000..0e7e481 --- /dev/null +++ b/runners/codex-runner/test/app-server-client.test.ts @@ -0,0 +1,87 @@ +import { describe, expect, it, vi } from 'vitest'; + +import { + CodexAppServerClient, + buildCodexAppServerArgs, +} from '../src/app-server-client.js'; + +describe('codex app-server client goals', () => { + it('does not enable goals by default when spawning app-server', () => { + expect( + buildCodexAppServerArgs({ + codexBin: '/opt/codex/bin/codex.js', + }), + ).toEqual(['/opt/codex/bin/codex.js', 'app-server']); + }); + + it('adds the under-development goals feature only when explicitly enabled', () => { + expect( + buildCodexAppServerArgs({ + codexBin: '/opt/codex/bin/codex.js', + enableGoals: true, + }), + ).toEqual(['/opt/codex/bin/codex.js', '--enable', 'goals', 'app-server']); + }); + + it('wraps thread goal JSON-RPC methods with the upstream objective field', async () => { + const client = new CodexAppServerClient({ + cwd: '/repo', + log: () => undefined, + }); + const request = vi + .fn() + .mockResolvedValueOnce({ + goal: { + threadId: 'thread-1', + objective: 'ship the release', + status: 'active', + tokenBudget: null, + tokensUsed: 0, + timeUsedSeconds: 0, + createdAt: 1, + updatedAt: 1, + }, + }) + .mockResolvedValueOnce({ + goal: { + threadId: 'thread-1', + objective: 'ship the release', + status: 'active', + tokenBudget: null, + tokensUsed: 0, + timeUsedSeconds: 0, + createdAt: 1, + updatedAt: 1, + }, + }) + .mockResolvedValueOnce({ cleared: true }); + ( + client as unknown as { + request: (method: string, params?: unknown) => Promise; + } + ).request = request; + + await expect( + client.threadGoalSet('thread-1', 'ship the release'), + ).resolves.toMatchObject({ + threadId: 'thread-1', + objective: 'ship the release', + status: 'active', + }); + await expect(client.threadGoalGet('thread-1')).resolves.toMatchObject({ + objective: 'ship the release', + }); + await expect(client.threadGoalClear('thread-1')).resolves.toBe(true); + + expect(request).toHaveBeenNthCalledWith(1, 'thread/goal/set', { + threadId: 'thread-1', + objective: 'ship the release', + }); + expect(request).toHaveBeenNthCalledWith(2, 'thread/goal/get', { + threadId: 'thread-1', + }); + expect(request).toHaveBeenNthCalledWith(3, 'thread/goal/clear', { + threadId: 'thread-1', + }); + }); +}); diff --git a/src/agent-runner-environment.test.ts b/src/agent-runner-environment.test.ts index 85390ba..f972a02 100644 --- a/src/agent-runner-environment.test.ts +++ b/src/agent-runner-environment.test.ts @@ -370,6 +370,79 @@ describe('prepareGroupEnvironment codex auth handling', () => { }); }); +describe('prepareGroupEnvironment Codex goals handling', () => { + let tempRoot: string; + let previousCwd: string; + let previousCodexGoals: string | undefined; + + beforeEach(() => { + tempRoot = fs.mkdtempSync(path.join('/tmp', 'ejclaw-agent-env-goals-')); + previousCwd = process.cwd(); + previousCodexGoals = process.env.CODEX_GOALS; + process.chdir(tempRoot); + process.env.EJ_TEST_ROOT = tempRoot; + process.env.EJ_TEST_HOME = path.join(tempRoot, 'home'); + delete process.env.CODEX_GOALS; + + fs.mkdirSync(path.join(process.env.EJ_TEST_HOME, '.codex'), { + recursive: true, + }); + + mockReadEnvFile.mockReset(); + mockGetActiveCodexAuthPath.mockReset(); + vi.mocked(config.isReviewService).mockReturnValue(false); + vi.mocked(serviceRouting.hasReviewerLease).mockReturnValue(false); + vi.mocked(serviceRouting.getEffectiveChannelLease).mockReturnValue({ + chat_jid: 'dc:test', + owner_service_id: 'claude', + reviewer_service_id: 'codex-main', + arbiter_service_id: null, + owner_failover_active: false, + activated_at: null, + reason: null, + explicit: false, + }); + }); + + afterEach(() => { + process.chdir(previousCwd); + delete process.env.EJ_TEST_ROOT; + delete process.env.EJ_TEST_HOME; + if (previousCodexGoals) process.env.CODEX_GOALS = previousCodexGoals; + else delete process.env.CODEX_GOALS; + fs.rmSync(tempRoot, { recursive: true, force: true }); + }); + + it('keeps Codex goals disabled by default and enables them only via opt-in config', () => { + mockReadEnvFile.mockReturnValue({}); + + const defaultPrepared = prepareGroupEnvironment(group, false, 'dc:test'); + expect(defaultPrepared.env.CODEX_GOALS).toBeUndefined(); + + const enabledPrepared = prepareGroupEnvironment( + { + ...group, + agentConfig: { + codexGoals: true, + }, + }, + false, + 'dc:test', + ); + expect(enabledPrepared.env.CODEX_GOALS).toBe('true'); + }); + + it('allows CODEX_GOALS env opt-in for Codex runner sessions', () => { + mockReadEnvFile.mockReturnValue({ + CODEX_GOALS: 'true', + }); + + const prepared = prepareGroupEnvironment(group, false, 'dc:test'); + + expect(prepared.env.CODEX_GOALS).toBe('true'); + }); +}); + describe('prepareReadonlySessionEnvironment codex compatibility', () => { let tempRoot: string; let previousCwd: string; diff --git a/src/agent-runner-environment.ts b/src/agent-runner-environment.ts index 777b3a0..433b62f 100644 --- a/src/agent-runner-environment.ts +++ b/src/agent-runner-environment.ts @@ -319,6 +319,15 @@ function prepareCodexSessionEnvironment(args: { process.env.CODEX_EFFORT; if (codexEffort) args.env.CODEX_EFFORT = codexEffort; + const codexGoals = + args.group.agentConfig?.codexGoals ?? + (args.envVars.CODEX_GOALS === 'true' || process.env.CODEX_GOALS === 'true'); + if (codexGoals) { + args.env.CODEX_GOALS = 'true'; + } else { + delete args.env.CODEX_GOALS; + } + const sessionCodexDir = path.join(args.sessionRootDir, '.codex'); syncHostCodexSessionFiles(sessionCodexDir); @@ -527,6 +536,7 @@ export function prepareGroupEnvironment( 'CLAUDE_EFFORT', 'CODEX_MODEL', 'CODEX_EFFORT', + 'CODEX_GOALS', ]); const env = buildBaseRunnerEnv({ diff --git a/src/agent-runner.test.ts b/src/agent-runner.test.ts index 55320d7..962c85e 100644 --- a/src/agent-runner.test.ts +++ b/src/agent-runner.test.ts @@ -680,3 +680,59 @@ OUROBOROS_LLM_BACKEND = "codex" ); }); }); + +describe('agent-runner Codex goals', () => { + beforeEach(() => { + vi.useRealTimers(); + vi.clearAllMocks(); + fakeProc = createFakeProcess(); + vi.mocked(fs.existsSync).mockImplementation((p: fs.PathLike) => { + const str = String(p); + return ( + str.includes('dist/index.js') || + str.includes('dist/ipc-mcp-stdio.js') || + str.endsWith('/.codex/config.toml') + ); + }); + }); + + afterEach(() => { + vi.useRealTimers(); + }); + + it('passes Codex goal opt-in config through env and runner stdin payload', async () => { + let stdinPayload = ''; + fakeProc.stdin.on('data', (chunk) => { + stdinPayload += chunk.toString(); + }); + + const codexGroup: RegisteredGroup = { + ...testGroup, + agentType: 'codex', + agentConfig: { + codexGoals: true, + }, + }; + + const resultPromise = runAgentProcess( + codexGroup, + testInput, + () => {}, + async () => {}, + ); + + fakeProc.emit('close', 0); + const result = await resultPromise; + expect(result.status).toBe('success'); + + expect(JSON.parse(stdinPayload)).toMatchObject({ + prompt: 'Hello', + codexGoals: true, + }); + + const spawnEnv = vi.mocked(spawn).mock.calls.at(-1)?.[2]?.env as + | Record + | undefined; + expect(spawnEnv?.CODEX_GOALS).toBe('true'); + }); +}); diff --git a/src/agent-runner.ts b/src/agent-runner.ts index f885f87..e64c239 100644 --- a/src/agent-runner.ts +++ b/src/agent-runner.ts @@ -34,6 +34,7 @@ export interface AgentInput { useTaskScopedSession?: boolean; assistantName?: string; agentType?: 'claude-code' | 'codex'; + codexGoals?: boolean; roomRoleContext?: RoomRoleContext; } @@ -150,13 +151,17 @@ export async function runAgentProcess( onProcess(proc, processName, env.EJCLAW_IPC_DIR); - proc.stdin.write(JSON.stringify(input)); + const runnerInput: AgentInput = { + ...input, + ...(group.agentConfig?.codexGoals === true ? { codexGoals: true } : {}), + }; + proc.stdin.write(JSON.stringify(runnerInput)); proc.stdin.end(); runSpawnedAgentProcess({ proc, group, - input, + input: runnerInput, processName, logsDir, startTime, diff --git a/src/settings-store.test.ts b/src/settings-store.test.ts new file mode 100644 index 0000000..2f965e9 --- /dev/null +++ b/src/settings-store.test.ts @@ -0,0 +1,45 @@ +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; + +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; + +import { getCodexFeatures, updateCodexFeatures } from './settings-store.js'; + +describe('settings-store Codex features', () => { + let tempDir: string; + let previousCwd: string; + let previousCodexGoals: string | undefined; + + beforeEach(() => { + tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ejclaw-settings-')); + previousCwd = process.cwd(); + previousCodexGoals = process.env.CODEX_GOALS; + delete process.env.CODEX_GOALS; + process.chdir(tempDir); + }); + + afterEach(() => { + process.chdir(previousCwd); + if (previousCodexGoals === undefined) { + delete process.env.CODEX_GOALS; + } else { + process.env.CODEX_GOALS = previousCodexGoals; + } + fs.rmSync(tempDir, { recursive: true, force: true }); + }); + + it('stores the Codex goals opt-in in the EJClaw .env file', () => { + expect(getCodexFeatures()).toEqual({ goals: false }); + + expect(updateCodexFeatures({ goals: true })).toEqual({ goals: true }); + expect(fs.readFileSync(path.join(tempDir, '.env'), 'utf-8')).toContain( + 'CODEX_GOALS=true', + ); + + expect(updateCodexFeatures({ goals: false })).toEqual({ goals: false }); + expect(fs.readFileSync(path.join(tempDir, '.env'), 'utf-8')).toContain( + 'CODEX_GOALS=false', + ); + }); +}); diff --git a/src/settings-store.ts b/src/settings-store.ts index e861cfa..3ecd232 100644 --- a/src/settings-store.ts +++ b/src/settings-store.ts @@ -56,6 +56,10 @@ export interface FastModeSnapshot { claude: boolean; } +export interface CodexFeatureSnapshot { + goals: boolean; +} + const ROLE_KEYS = ['OWNER', 'REVIEWER', 'ARBITER'] as const; function envFilePath(): string { @@ -634,6 +638,36 @@ export function updateFastMode( return getFastMode(); } +function readCodexGoals(): boolean { + return readEnvOrProcess('CODEX_GOALS') === 'true'; +} + +function writeCodexGoals(value: boolean): void { + const file = envFilePath(); + const content = fs.existsSync(file) ? fs.readFileSync(file, 'utf-8') : ''; + const updated = setOrInsertEnvLine( + content, + 'CODEX_GOALS', + value ? 'true' : 'false', + ); + const tempPath = `${file}.tmp`; + fs.writeFileSync(tempPath, updated, { mode: 0o600 }); + fs.renameSync(tempPath, file); +} + +export function getCodexFeatures(): CodexFeatureSnapshot { + return { + goals: readCodexGoals(), + }; +} + +export function updateCodexFeatures( + input: Partial, +): CodexFeatureSnapshot { + if (typeof input.goals === 'boolean') writeCodexGoals(input.goals); + return getCodexFeatures(); +} + export function removeAccountDirectory( provider: 'claude' | 'codex', index: number, diff --git a/src/types.ts b/src/types.ts index 8233453..ce35bfe 100644 --- a/src/types.ts +++ b/src/types.ts @@ -5,6 +5,7 @@ export interface AgentConfig { // Per-group model/effort overrides (take precedence over global env vars) codexModel?: string; codexEffort?: string; + codexGoals?: boolean; claudeModel?: string; claudeEffort?: string; claudeThinking?: 'adaptive' | 'enabled' | 'disabled'; diff --git a/src/web-dashboard-settings-routes.test.ts b/src/web-dashboard-settings-routes.test.ts index 3b29fc2..84ee2af 100644 --- a/src/web-dashboard-settings-routes.test.ts +++ b/src/web-dashboard-settings-routes.test.ts @@ -7,6 +7,7 @@ import { import type { ClaudeAccountSummary, CodexAccountSummary, + CodexFeatureSnapshot, FastModeSnapshot, ModelConfigSnapshot, } from './settings-store.js'; @@ -42,6 +43,7 @@ const modelConfig: ModelConfigSnapshot = { }; const fastMode: FastModeSnapshot = { codex: true, claude: false }; +const codexFeatures: CodexFeatureSnapshot = { goals: false }; const moaSettings: MoaSettingsSnapshot = { enabled: true, @@ -107,6 +109,7 @@ function makeDeps( responseLength: 2, }), getActiveCodexSettingsIndex: () => 1, + getCodexFeatures: () => codexFeatures, getFastMode: () => fastMode, getModelConfig: () => modelConfig, getMoaSettings: () => moaSettings, @@ -116,6 +119,7 @@ function makeDeps( refreshCodexAccount: async () => makeCodexAccount(), removeAccountDirectory: () => undefined, setActiveCodexSettingsIndex: () => undefined, + updateCodexFeatures: () => codexFeatures, updateFastMode: () => fastMode, updateModelConfig: () => modelConfig, updateMoaSettings: () => moaSettings, @@ -155,6 +159,10 @@ describe('web dashboard settings routes', () => { expect(mode?.status).toBe(200); await expect(mode?.json()).resolves.toEqual(fastMode); + const features = await route('/api/settings/codex-features'); + expect(features?.status).toBe(200); + await expect(features?.json()).resolves.toEqual(codexFeatures); + const moa = await route('/api/settings/moa'); expect(moa?.status).toBe(200); const moaJson = (await moa?.json()) as MoaSettingsSnapshot; @@ -187,6 +195,10 @@ describe('web dashboard settings routes', () => { calls.push(`models:${JSON.stringify(input)}`); return modelConfig; }, + updateCodexFeatures: (input) => { + calls.push(`codex-features:${JSON.stringify(input)}`); + return { goals: input.goals === true }; + }, updateMoaSettings: (input) => { calls.push(`moa:${JSON.stringify(input)}`); return moaSettings; @@ -230,6 +242,15 @@ describe('web dashboard settings routes', () => { codexCurrentIndex: 1, }); + const goals = await route( + '/api/settings/codex-features', + 'PATCH', + { goals: true }, + deps, + ); + expect(goals?.status).toBe(200); + await expect(goals?.json()).resolves.toEqual({ goals: true }); + const moa = await route( '/api/settings/moa', 'PATCH', @@ -255,6 +276,7 @@ describe('web dashboard settings routes', () => { 'add:claude-token', 'delete:codex:4', 'current:4', + 'codex-features:{"goals":true}', 'moa:{"enabled":false,"models":[{"name":"kimi","enabled":false}]}', ]); }); diff --git a/src/web-dashboard-settings-routes.ts b/src/web-dashboard-settings-routes.ts index b0f268a..fcec7fe 100644 --- a/src/web-dashboard-settings-routes.ts +++ b/src/web-dashboard-settings-routes.ts @@ -1,6 +1,7 @@ import { addClaudeAccountFromToken, getActiveCodexSettingsIndex, + getCodexFeatures, getFastMode, getModelConfig, listClaudeAccounts, @@ -9,9 +10,11 @@ import { refreshCodexAccount, removeAccountDirectory, setActiveCodexSettingsIndex, + updateCodexFeatures, updateFastMode, updateModelConfig, type ClaudeAccountSummary, + type CodexFeatureSnapshot, type CodexAccountSummary, type FastModeSnapshot, type ModelConfigSnapshot, @@ -33,6 +36,7 @@ export interface SettingsRouteDependencies { addClaudeAccountFromToken: typeof addClaudeAccountFromToken; checkMoaModel: typeof checkMoaModel; getActiveCodexSettingsIndex: typeof getActiveCodexSettingsIndex; + getCodexFeatures: typeof getCodexFeatures; getFastMode: typeof getFastMode; getModelConfig: typeof getModelConfig; getMoaSettings: typeof getMoaSettings; @@ -42,6 +46,7 @@ export interface SettingsRouteDependencies { refreshCodexAccount: typeof refreshCodexAccount; removeAccountDirectory: typeof removeAccountDirectory; setActiveCodexSettingsIndex: typeof setActiveCodexSettingsIndex; + updateCodexFeatures: typeof updateCodexFeatures; updateFastMode: typeof updateFastMode; updateModelConfig: typeof updateModelConfig; updateMoaSettings: typeof updateMoaSettings; @@ -58,6 +63,7 @@ const defaultSettingsRouteDependencies: SettingsRouteDependencies = { addClaudeAccountFromToken, checkMoaModel, getActiveCodexSettingsIndex, + getCodexFeatures, getFastMode, getModelConfig, getMoaSettings, @@ -67,6 +73,7 @@ const defaultSettingsRouteDependencies: SettingsRouteDependencies = { refreshCodexAccount, removeAccountDirectory, setActiveCodexSettingsIndex, + updateCodexFeatures, updateFastMode, updateModelConfig, updateMoaSettings, @@ -133,6 +140,23 @@ async function handleFastModeRoute( } } +async function handleCodexFeaturesRoute( + request: Request, + jsonResponse: JsonResponse, + deps: SettingsRouteDependencies, +): Promise { + if (readMethod(request.method)) return jsonResponse(deps.getCodexFeatures()); + if (request.method !== 'PUT' && request.method !== 'PATCH') return null; + + const body = await readJsonObject(request, jsonResponse); + if (body instanceof Response) return body; + try { + return jsonResponse(deps.updateCodexFeatures(body)); + } catch (err) { + return jsonResponse({ error: errorMessage(err) }, { status: 500 }); + } +} + async function handleMoaSettingsRoute( request: Request, jsonResponse: JsonResponse, @@ -299,6 +323,9 @@ export async function handleSettingsRoute({ if (url.pathname === '/api/settings/fast-mode') { return handleFastModeRoute(request, jsonResponse, deps); } + if (url.pathname === '/api/settings/codex-features') { + return handleCodexFeaturesRoute(request, jsonResponse, deps); + } if (url.pathname === '/api/settings/moa') { return handleMoaSettingsRoute(request, jsonResponse, deps); } @@ -335,6 +362,7 @@ export async function handleSettingsRoute({ export type { ClaudeAccountSummary, CodexAccountSummary, + CodexFeatureSnapshot, FastModeSnapshot, ModelConfigSnapshot, MoaSettingsSnapshot,