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
This commit is contained in:
@@ -1,8 +1,8 @@
|
|||||||
# EJClaw
|
# EJClaw
|
||||||
|
|
||||||

|

|
||||||

|

|
||||||

|

|
||||||

|

|
||||||

|

|
||||||
|
|
||||||
@@ -115,6 +115,11 @@ Discord ──► SQLite (WAL) ──► GroupQueue ──┬──► Owner (ho
|
|||||||
- Codex CLI
|
- Codex CLI
|
||||||
- Discord 봇 토큰 3개(owner / reviewer / arbiter)
|
- 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
|
```bash
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ describe('SettingsPanel', () => {
|
|||||||
expect(html).toContain('모델');
|
expect(html).toContain('모델');
|
||||||
expect(html).toContain('MoA 참조 모델');
|
expect(html).toContain('MoA 참조 모델');
|
||||||
expect(html).toContain('패스트 모드');
|
expect(html).toContain('패스트 모드');
|
||||||
|
expect(html).toContain('Codex 실험 기능');
|
||||||
expect(html).toContain('불러오는 중');
|
expect(html).toContain('불러오는 중');
|
||||||
expect(html).toContain('Claude');
|
expect(html).toContain('Claude');
|
||||||
expect(html).toContain('계정');
|
expect(html).toContain('계정');
|
||||||
|
|||||||
@@ -3,17 +3,20 @@ import { useEffect, useState } from 'react';
|
|||||||
import {
|
import {
|
||||||
type ClaudeAccountSummary,
|
type ClaudeAccountSummary,
|
||||||
type CodexAccountSummary,
|
type CodexAccountSummary,
|
||||||
|
type CodexFeatureSnapshot,
|
||||||
type FastModeSnapshot,
|
type FastModeSnapshot,
|
||||||
type ModelConfigSnapshot,
|
type ModelConfigSnapshot,
|
||||||
type ModelRoleConfig,
|
type ModelRoleConfig,
|
||||||
addClaudeAccount,
|
addClaudeAccount,
|
||||||
deleteAccount,
|
deleteAccount,
|
||||||
fetchAccounts,
|
fetchAccounts,
|
||||||
|
fetchCodexFeatures,
|
||||||
fetchFastMode,
|
fetchFastMode,
|
||||||
fetchModelConfig,
|
fetchModelConfig,
|
||||||
refreshAllCodexAccounts as refreshAllCodexAccountsApi,
|
refreshAllCodexAccounts as refreshAllCodexAccountsApi,
|
||||||
refreshCodexAccount as refreshCodexAccountApi,
|
refreshCodexAccount as refreshCodexAccountApi,
|
||||||
setCurrentCodexAccount as setCurrentCodexAccountApi,
|
setCurrentCodexAccount as setCurrentCodexAccountApi,
|
||||||
|
updateCodexFeatures,
|
||||||
updateFastMode,
|
updateFastMode,
|
||||||
updateModels,
|
updateModels,
|
||||||
} from './api';
|
} from './api';
|
||||||
@@ -82,6 +85,8 @@ export function SettingsPanel({
|
|||||||
|
|
||||||
<FastModeSettings />
|
<FastModeSettings />
|
||||||
|
|
||||||
|
<CodexFeatureSettings onRestartStack={onRestartStack} />
|
||||||
|
|
||||||
<AccountSettings onRestartStack={onRestartStack} />
|
<AccountSettings onRestartStack={onRestartStack} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -302,6 +307,104 @@ function FastModeSettings() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function CodexFeatureSettings({
|
||||||
|
onRestartStack,
|
||||||
|
}: {
|
||||||
|
onRestartStack: () => void;
|
||||||
|
}) {
|
||||||
|
const [state, setState] = useState<CodexFeatureSnapshot | null>(null);
|
||||||
|
const [busy, setBusy] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [savedAt, setSavedAt] = useState<number | null>(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 (
|
||||||
|
<section className="settings-section">
|
||||||
|
<h3>Codex 실험 기능</h3>
|
||||||
|
{error ? <p className="settings-error">{error}</p> : null}
|
||||||
|
{!state ? (
|
||||||
|
<p className="settings-hint">불러오는 중…</p>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<label className="settings-toggle-row">
|
||||||
|
<span className="settings-toggle-label">
|
||||||
|
<span className="settings-toggle-title">/goal</span>
|
||||||
|
<small className="settings-hint">
|
||||||
|
CODEX_GOALS=true — Codex 0.128의 under-development goals
|
||||||
|
기능입니다. 기본 OFF이며, 저장 후 스택 재시작이 필요합니다.
|
||||||
|
</small>
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
checked={state.goals}
|
||||||
|
disabled={busy}
|
||||||
|
onChange={() => void toggleGoals()}
|
||||||
|
type="checkbox"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<div className="settings-actions">
|
||||||
|
{savedAt ? (
|
||||||
|
<small className="settings-hint">
|
||||||
|
저장됨. 적용하려면 스택 재시작 필요.
|
||||||
|
</small>
|
||||||
|
) : null}
|
||||||
|
<button
|
||||||
|
className="settings-restart"
|
||||||
|
disabled={busy}
|
||||||
|
onClick={() => {
|
||||||
|
if (
|
||||||
|
window.confirm(
|
||||||
|
'스택을 재시작하면 진행 중인 모든 에이전트 작업이 중단됩니다. 진행할까요?',
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
onRestartStack();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
스택 재시작
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function formatExpiry(
|
function formatExpiry(
|
||||||
iso: string | null,
|
iso: string | null,
|
||||||
): { label: string; cls: string } | null {
|
): { label: string; cls: string } | null {
|
||||||
|
|||||||
@@ -388,6 +388,10 @@ export interface FastModeSnapshot {
|
|||||||
claude: boolean;
|
claude: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CodexFeatureSnapshot {
|
||||||
|
goals: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export interface MoaReferenceStatus {
|
export interface MoaReferenceStatus {
|
||||||
model: string;
|
model: string;
|
||||||
checkedAt: string;
|
checkedAt: string;
|
||||||
@@ -538,6 +542,34 @@ export async function updateFastMode(
|
|||||||
return (await response.json()) as FastModeSnapshot;
|
return (await response.json()) as FastModeSnapshot;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function fetchCodexFeatures(): Promise<CodexFeatureSnapshot> {
|
||||||
|
return fetchJson('/api/settings/codex-features');
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateCodexFeatures(
|
||||||
|
input: Partial<CodexFeatureSnapshot>,
|
||||||
|
): Promise<CodexFeatureSnapshot> {
|
||||||
|
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<MoaSettingsSnapshot> {
|
export async function fetchMoaSettings(): Promise<MoaSettingsSnapshot> {
|
||||||
return fetchJson('/api/settings/moa');
|
return fetchJson('/api/settings/moa');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
"": {
|
"": {
|
||||||
"name": "ejclaw-agent-runner",
|
"name": "ejclaw-agent-runner",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@anthropic-ai/claude-agent-sdk": "0.2.114",
|
"@anthropic-ai/claude-agent-sdk": "0.2.126",
|
||||||
"@modelcontextprotocol/sdk": "^1.12.1",
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
||||||
"cron-parser": "^5.0.0",
|
"cron-parser": "^5.0.0",
|
||||||
"ejclaw-runners-shared": "file:../shared",
|
"ejclaw-runners-shared": "file:../shared",
|
||||||
@@ -18,23 +18,23 @@
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
"packages": {
|
"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=="],
|
"@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=="],
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
"start": "bun dist/index.js"
|
"start": "bun dist/index.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@anthropic-ai/claude-agent-sdk": "0.2.114",
|
"@anthropic-ai/claude-agent-sdk": "0.2.126",
|
||||||
"@modelcontextprotocol/sdk": "^1.12.1",
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
||||||
"cron-parser": "^5.0.0",
|
"cron-parser": "^5.0.0",
|
||||||
"ejclaw-runners-shared": "file:../shared",
|
"ejclaw-runners-shared": "file:../shared",
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
"": {
|
"": {
|
||||||
"name": "ejclaw-codex-runner",
|
"name": "ejclaw-codex-runner",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@openai/codex": "^0.124.0",
|
"@openai/codex": "0.128.0",
|
||||||
"ejclaw-runners-shared": "file:../shared",
|
"ejclaw-runners-shared": "file:../shared",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -15,19 +15,19 @@
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
"packages": {
|
"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=="],
|
"@types/node": ["@types/node@22.19.15", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg=="],
|
||||||
|
|
||||||
|
|||||||
@@ -10,8 +10,8 @@
|
|||||||
"start": "bun dist/index.js"
|
"start": "bun dist/index.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ejclaw-runners-shared": "file:../shared",
|
"@openai/codex": "0.128.0",
|
||||||
"@openai/codex": "^0.124.0"
|
"ejclaw-runners-shared": "file:../shared"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^22.10.7",
|
"@types/node": "^22.10.7",
|
||||||
|
|||||||
@@ -42,6 +42,53 @@ export interface CodexAppServerTurnResult {
|
|||||||
result: string | null;
|
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<CodexThreadGoal>;
|
||||||
|
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 {
|
interface JsonRpcResponse {
|
||||||
id: number;
|
id: number;
|
||||||
result?: unknown;
|
result?: unknown;
|
||||||
@@ -79,12 +126,14 @@ export interface CodexAppServerClientOptions {
|
|||||||
cwd: string;
|
cwd: string;
|
||||||
env?: NodeJS.ProcessEnv;
|
env?: NodeJS.ProcessEnv;
|
||||||
log: (message: string) => void;
|
log: (message: string) => void;
|
||||||
|
enableGoals?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CodexAppServerClient {
|
export class CodexAppServerClient {
|
||||||
private readonly cwd: string;
|
private readonly cwd: string;
|
||||||
private readonly env: NodeJS.ProcessEnv;
|
private readonly env: NodeJS.ProcessEnv;
|
||||||
private readonly log: (message: string) => void;
|
private readonly log: (message: string) => void;
|
||||||
|
private readonly enableGoals: boolean;
|
||||||
private readonly pending = new Map<number, PendingRequest>();
|
private readonly pending = new Map<number, PendingRequest>();
|
||||||
private readonly require = createRequire(import.meta.url);
|
private readonly require = createRequire(import.meta.url);
|
||||||
private nextId = 1;
|
private nextId = 1;
|
||||||
@@ -96,6 +145,7 @@ export class CodexAppServerClient {
|
|||||||
this.cwd = options.cwd;
|
this.cwd = options.cwd;
|
||||||
this.env = options.env || process.env;
|
this.env = options.env || process.env;
|
||||||
this.log = options.log;
|
this.log = options.log;
|
||||||
|
this.enableGoals = options.enableGoals === true;
|
||||||
}
|
}
|
||||||
|
|
||||||
async start(): Promise<void> {
|
async start(): Promise<void> {
|
||||||
@@ -108,11 +158,18 @@ export class CodexAppServerClient {
|
|||||||
'codex.js',
|
'codex.js',
|
||||||
);
|
);
|
||||||
|
|
||||||
this.proc = spawn(process.execPath, [codexBin, 'app-server'], {
|
this.proc = spawn(
|
||||||
|
process.execPath,
|
||||||
|
buildCodexAppServerArgs({
|
||||||
|
codexBin,
|
||||||
|
enableGoals: this.enableGoals,
|
||||||
|
}),
|
||||||
|
{
|
||||||
cwd: this.cwd,
|
cwd: this.cwd,
|
||||||
env: this.env,
|
env: this.env,
|
||||||
stdio: ['pipe', 'pipe', 'pipe'],
|
stdio: ['pipe', 'pipe', 'pipe'],
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
this.proc.stdout.setEncoding('utf8');
|
this.proc.stdout.setEncoding('utf8');
|
||||||
this.proc.stdout.on('data', (chunk: string) => {
|
this.proc.stdout.on('data', (chunk: string) => {
|
||||||
@@ -312,6 +369,27 @@ export class CodexAppServerClient {
|
|||||||
return turnPromise;
|
return turnPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async threadGoalSet(
|
||||||
|
threadId: string,
|
||||||
|
objective: string,
|
||||||
|
): Promise<CodexThreadGoal> {
|
||||||
|
const result = await this.request('thread/goal/set', {
|
||||||
|
threadId,
|
||||||
|
objective,
|
||||||
|
});
|
||||||
|
return extractGoalResult(result, 'thread/goal/set');
|
||||||
|
}
|
||||||
|
|
||||||
|
async threadGoalGet(threadId: string): Promise<CodexThreadGoal | null> {
|
||||||
|
const result = await this.request('thread/goal/get', { threadId });
|
||||||
|
return extractOptionalGoalResult(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
async threadGoalClear(threadId: string): Promise<boolean> {
|
||||||
|
const result = await this.request('thread/goal/clear', { threadId });
|
||||||
|
return (result as { cleared?: boolean } | null)?.cleared === true;
|
||||||
|
}
|
||||||
|
|
||||||
private handleStdoutLine(line: string): void {
|
private handleStdoutLine(line: string): void {
|
||||||
let message: JsonRpcResponse | JsonRpcNotification | JsonRpcServerRequest;
|
let message: JsonRpcResponse | JsonRpcNotification | JsonRpcServerRequest;
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ interface RunnerInput {
|
|||||||
isScheduledTask?: boolean;
|
isScheduledTask?: boolean;
|
||||||
assistantName?: string;
|
assistantName?: string;
|
||||||
agentType?: string;
|
agentType?: string;
|
||||||
|
codexGoals?: boolean;
|
||||||
roomRoleContext?: RoomRoleContext;
|
roomRoleContext?: RoomRoleContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,6 +96,10 @@ function log(message: string): void {
|
|||||||
console.error(`[codex-runner] ${message}`);
|
console.error(`[codex-runner] ${message}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isCodexGoalsEnabled(runnerInput: RunnerInput): boolean {
|
||||||
|
return runnerInput.codexGoals === true || process.env.CODEX_GOALS === 'true';
|
||||||
|
}
|
||||||
|
|
||||||
async function readStdin(): Promise<string> {
|
async function readStdin(): Promise<string> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
let data = '';
|
let data = '';
|
||||||
@@ -343,6 +348,7 @@ async function runAppServerSession(
|
|||||||
cwd: EFFECTIVE_CWD,
|
cwd: EFFECTIVE_CWD,
|
||||||
env: clientEnv,
|
env: clientEnv,
|
||||||
log,
|
log,
|
||||||
|
enableGoals: isCodexGoalsEnabled(runnerInput),
|
||||||
});
|
});
|
||||||
|
|
||||||
await client.start();
|
await client.start();
|
||||||
|
|||||||
87
runners/codex-runner/test/app-server-client.test.ts
Normal file
87
runners/codex-runner/test/app-server-client.test.ts
Normal file
@@ -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<unknown>;
|
||||||
|
}
|
||||||
|
).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',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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', () => {
|
describe('prepareReadonlySessionEnvironment codex compatibility', () => {
|
||||||
let tempRoot: string;
|
let tempRoot: string;
|
||||||
let previousCwd: string;
|
let previousCwd: string;
|
||||||
|
|||||||
@@ -319,6 +319,15 @@ function prepareCodexSessionEnvironment(args: {
|
|||||||
process.env.CODEX_EFFORT;
|
process.env.CODEX_EFFORT;
|
||||||
if (codexEffort) args.env.CODEX_EFFORT = codexEffort;
|
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');
|
const sessionCodexDir = path.join(args.sessionRootDir, '.codex');
|
||||||
syncHostCodexSessionFiles(sessionCodexDir);
|
syncHostCodexSessionFiles(sessionCodexDir);
|
||||||
|
|
||||||
@@ -527,6 +536,7 @@ export function prepareGroupEnvironment(
|
|||||||
'CLAUDE_EFFORT',
|
'CLAUDE_EFFORT',
|
||||||
'CODEX_MODEL',
|
'CODEX_MODEL',
|
||||||
'CODEX_EFFORT',
|
'CODEX_EFFORT',
|
||||||
|
'CODEX_GOALS',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const env = buildBaseRunnerEnv({
|
const env = buildBaseRunnerEnv({
|
||||||
|
|||||||
@@ -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<string, string>
|
||||||
|
| undefined;
|
||||||
|
expect(spawnEnv?.CODEX_GOALS).toBe('true');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ export interface AgentInput {
|
|||||||
useTaskScopedSession?: boolean;
|
useTaskScopedSession?: boolean;
|
||||||
assistantName?: string;
|
assistantName?: string;
|
||||||
agentType?: 'claude-code' | 'codex';
|
agentType?: 'claude-code' | 'codex';
|
||||||
|
codexGoals?: boolean;
|
||||||
roomRoleContext?: RoomRoleContext;
|
roomRoleContext?: RoomRoleContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,13 +151,17 @@ export async function runAgentProcess(
|
|||||||
|
|
||||||
onProcess(proc, processName, env.EJCLAW_IPC_DIR);
|
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();
|
proc.stdin.end();
|
||||||
|
|
||||||
runSpawnedAgentProcess({
|
runSpawnedAgentProcess({
|
||||||
proc,
|
proc,
|
||||||
group,
|
group,
|
||||||
input,
|
input: runnerInput,
|
||||||
processName,
|
processName,
|
||||||
logsDir,
|
logsDir,
|
||||||
startTime,
|
startTime,
|
||||||
|
|||||||
45
src/settings-store.test.ts
Normal file
45
src/settings-store.test.ts
Normal file
@@ -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',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -56,6 +56,10 @@ export interface FastModeSnapshot {
|
|||||||
claude: boolean;
|
claude: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CodexFeatureSnapshot {
|
||||||
|
goals: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
const ROLE_KEYS = ['OWNER', 'REVIEWER', 'ARBITER'] as const;
|
const ROLE_KEYS = ['OWNER', 'REVIEWER', 'ARBITER'] as const;
|
||||||
|
|
||||||
function envFilePath(): string {
|
function envFilePath(): string {
|
||||||
@@ -634,6 +638,36 @@ export function updateFastMode(
|
|||||||
return getFastMode();
|
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>,
|
||||||
|
): CodexFeatureSnapshot {
|
||||||
|
if (typeof input.goals === 'boolean') writeCodexGoals(input.goals);
|
||||||
|
return getCodexFeatures();
|
||||||
|
}
|
||||||
|
|
||||||
export function removeAccountDirectory(
|
export function removeAccountDirectory(
|
||||||
provider: 'claude' | 'codex',
|
provider: 'claude' | 'codex',
|
||||||
index: number,
|
index: number,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ export interface AgentConfig {
|
|||||||
// Per-group model/effort overrides (take precedence over global env vars)
|
// Per-group model/effort overrides (take precedence over global env vars)
|
||||||
codexModel?: string;
|
codexModel?: string;
|
||||||
codexEffort?: string;
|
codexEffort?: string;
|
||||||
|
codexGoals?: boolean;
|
||||||
claudeModel?: string;
|
claudeModel?: string;
|
||||||
claudeEffort?: string;
|
claudeEffort?: string;
|
||||||
claudeThinking?: 'adaptive' | 'enabled' | 'disabled';
|
claudeThinking?: 'adaptive' | 'enabled' | 'disabled';
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
import type {
|
import type {
|
||||||
ClaudeAccountSummary,
|
ClaudeAccountSummary,
|
||||||
CodexAccountSummary,
|
CodexAccountSummary,
|
||||||
|
CodexFeatureSnapshot,
|
||||||
FastModeSnapshot,
|
FastModeSnapshot,
|
||||||
ModelConfigSnapshot,
|
ModelConfigSnapshot,
|
||||||
} from './settings-store.js';
|
} from './settings-store.js';
|
||||||
@@ -42,6 +43,7 @@ const modelConfig: ModelConfigSnapshot = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const fastMode: FastModeSnapshot = { codex: true, claude: false };
|
const fastMode: FastModeSnapshot = { codex: true, claude: false };
|
||||||
|
const codexFeatures: CodexFeatureSnapshot = { goals: false };
|
||||||
|
|
||||||
const moaSettings: MoaSettingsSnapshot = {
|
const moaSettings: MoaSettingsSnapshot = {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
@@ -107,6 +109,7 @@ function makeDeps(
|
|||||||
responseLength: 2,
|
responseLength: 2,
|
||||||
}),
|
}),
|
||||||
getActiveCodexSettingsIndex: () => 1,
|
getActiveCodexSettingsIndex: () => 1,
|
||||||
|
getCodexFeatures: () => codexFeatures,
|
||||||
getFastMode: () => fastMode,
|
getFastMode: () => fastMode,
|
||||||
getModelConfig: () => modelConfig,
|
getModelConfig: () => modelConfig,
|
||||||
getMoaSettings: () => moaSettings,
|
getMoaSettings: () => moaSettings,
|
||||||
@@ -116,6 +119,7 @@ function makeDeps(
|
|||||||
refreshCodexAccount: async () => makeCodexAccount(),
|
refreshCodexAccount: async () => makeCodexAccount(),
|
||||||
removeAccountDirectory: () => undefined,
|
removeAccountDirectory: () => undefined,
|
||||||
setActiveCodexSettingsIndex: () => undefined,
|
setActiveCodexSettingsIndex: () => undefined,
|
||||||
|
updateCodexFeatures: () => codexFeatures,
|
||||||
updateFastMode: () => fastMode,
|
updateFastMode: () => fastMode,
|
||||||
updateModelConfig: () => modelConfig,
|
updateModelConfig: () => modelConfig,
|
||||||
updateMoaSettings: () => moaSettings,
|
updateMoaSettings: () => moaSettings,
|
||||||
@@ -155,6 +159,10 @@ describe('web dashboard settings routes', () => {
|
|||||||
expect(mode?.status).toBe(200);
|
expect(mode?.status).toBe(200);
|
||||||
await expect(mode?.json()).resolves.toEqual(fastMode);
|
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');
|
const moa = await route('/api/settings/moa');
|
||||||
expect(moa?.status).toBe(200);
|
expect(moa?.status).toBe(200);
|
||||||
const moaJson = (await moa?.json()) as MoaSettingsSnapshot;
|
const moaJson = (await moa?.json()) as MoaSettingsSnapshot;
|
||||||
@@ -187,6 +195,10 @@ describe('web dashboard settings routes', () => {
|
|||||||
calls.push(`models:${JSON.stringify(input)}`);
|
calls.push(`models:${JSON.stringify(input)}`);
|
||||||
return modelConfig;
|
return modelConfig;
|
||||||
},
|
},
|
||||||
|
updateCodexFeatures: (input) => {
|
||||||
|
calls.push(`codex-features:${JSON.stringify(input)}`);
|
||||||
|
return { goals: input.goals === true };
|
||||||
|
},
|
||||||
updateMoaSettings: (input) => {
|
updateMoaSettings: (input) => {
|
||||||
calls.push(`moa:${JSON.stringify(input)}`);
|
calls.push(`moa:${JSON.stringify(input)}`);
|
||||||
return moaSettings;
|
return moaSettings;
|
||||||
@@ -230,6 +242,15 @@ describe('web dashboard settings routes', () => {
|
|||||||
codexCurrentIndex: 1,
|
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(
|
const moa = await route(
|
||||||
'/api/settings/moa',
|
'/api/settings/moa',
|
||||||
'PATCH',
|
'PATCH',
|
||||||
@@ -255,6 +276,7 @@ describe('web dashboard settings routes', () => {
|
|||||||
'add:claude-token',
|
'add:claude-token',
|
||||||
'delete:codex:4',
|
'delete:codex:4',
|
||||||
'current:4',
|
'current:4',
|
||||||
|
'codex-features:{"goals":true}',
|
||||||
'moa:{"enabled":false,"models":[{"name":"kimi","enabled":false}]}',
|
'moa:{"enabled":false,"models":[{"name":"kimi","enabled":false}]}',
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
addClaudeAccountFromToken,
|
addClaudeAccountFromToken,
|
||||||
getActiveCodexSettingsIndex,
|
getActiveCodexSettingsIndex,
|
||||||
|
getCodexFeatures,
|
||||||
getFastMode,
|
getFastMode,
|
||||||
getModelConfig,
|
getModelConfig,
|
||||||
listClaudeAccounts,
|
listClaudeAccounts,
|
||||||
@@ -9,9 +10,11 @@ import {
|
|||||||
refreshCodexAccount,
|
refreshCodexAccount,
|
||||||
removeAccountDirectory,
|
removeAccountDirectory,
|
||||||
setActiveCodexSettingsIndex,
|
setActiveCodexSettingsIndex,
|
||||||
|
updateCodexFeatures,
|
||||||
updateFastMode,
|
updateFastMode,
|
||||||
updateModelConfig,
|
updateModelConfig,
|
||||||
type ClaudeAccountSummary,
|
type ClaudeAccountSummary,
|
||||||
|
type CodexFeatureSnapshot,
|
||||||
type CodexAccountSummary,
|
type CodexAccountSummary,
|
||||||
type FastModeSnapshot,
|
type FastModeSnapshot,
|
||||||
type ModelConfigSnapshot,
|
type ModelConfigSnapshot,
|
||||||
@@ -33,6 +36,7 @@ export interface SettingsRouteDependencies {
|
|||||||
addClaudeAccountFromToken: typeof addClaudeAccountFromToken;
|
addClaudeAccountFromToken: typeof addClaudeAccountFromToken;
|
||||||
checkMoaModel: typeof checkMoaModel;
|
checkMoaModel: typeof checkMoaModel;
|
||||||
getActiveCodexSettingsIndex: typeof getActiveCodexSettingsIndex;
|
getActiveCodexSettingsIndex: typeof getActiveCodexSettingsIndex;
|
||||||
|
getCodexFeatures: typeof getCodexFeatures;
|
||||||
getFastMode: typeof getFastMode;
|
getFastMode: typeof getFastMode;
|
||||||
getModelConfig: typeof getModelConfig;
|
getModelConfig: typeof getModelConfig;
|
||||||
getMoaSettings: typeof getMoaSettings;
|
getMoaSettings: typeof getMoaSettings;
|
||||||
@@ -42,6 +46,7 @@ export interface SettingsRouteDependencies {
|
|||||||
refreshCodexAccount: typeof refreshCodexAccount;
|
refreshCodexAccount: typeof refreshCodexAccount;
|
||||||
removeAccountDirectory: typeof removeAccountDirectory;
|
removeAccountDirectory: typeof removeAccountDirectory;
|
||||||
setActiveCodexSettingsIndex: typeof setActiveCodexSettingsIndex;
|
setActiveCodexSettingsIndex: typeof setActiveCodexSettingsIndex;
|
||||||
|
updateCodexFeatures: typeof updateCodexFeatures;
|
||||||
updateFastMode: typeof updateFastMode;
|
updateFastMode: typeof updateFastMode;
|
||||||
updateModelConfig: typeof updateModelConfig;
|
updateModelConfig: typeof updateModelConfig;
|
||||||
updateMoaSettings: typeof updateMoaSettings;
|
updateMoaSettings: typeof updateMoaSettings;
|
||||||
@@ -58,6 +63,7 @@ const defaultSettingsRouteDependencies: SettingsRouteDependencies = {
|
|||||||
addClaudeAccountFromToken,
|
addClaudeAccountFromToken,
|
||||||
checkMoaModel,
|
checkMoaModel,
|
||||||
getActiveCodexSettingsIndex,
|
getActiveCodexSettingsIndex,
|
||||||
|
getCodexFeatures,
|
||||||
getFastMode,
|
getFastMode,
|
||||||
getModelConfig,
|
getModelConfig,
|
||||||
getMoaSettings,
|
getMoaSettings,
|
||||||
@@ -67,6 +73,7 @@ const defaultSettingsRouteDependencies: SettingsRouteDependencies = {
|
|||||||
refreshCodexAccount,
|
refreshCodexAccount,
|
||||||
removeAccountDirectory,
|
removeAccountDirectory,
|
||||||
setActiveCodexSettingsIndex,
|
setActiveCodexSettingsIndex,
|
||||||
|
updateCodexFeatures,
|
||||||
updateFastMode,
|
updateFastMode,
|
||||||
updateModelConfig,
|
updateModelConfig,
|
||||||
updateMoaSettings,
|
updateMoaSettings,
|
||||||
@@ -133,6 +140,23 @@ async function handleFastModeRoute(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleCodexFeaturesRoute(
|
||||||
|
request: Request,
|
||||||
|
jsonResponse: JsonResponse,
|
||||||
|
deps: SettingsRouteDependencies,
|
||||||
|
): Promise<Response | null> {
|
||||||
|
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(
|
async function handleMoaSettingsRoute(
|
||||||
request: Request,
|
request: Request,
|
||||||
jsonResponse: JsonResponse,
|
jsonResponse: JsonResponse,
|
||||||
@@ -299,6 +323,9 @@ export async function handleSettingsRoute({
|
|||||||
if (url.pathname === '/api/settings/fast-mode') {
|
if (url.pathname === '/api/settings/fast-mode') {
|
||||||
return handleFastModeRoute(request, jsonResponse, deps);
|
return handleFastModeRoute(request, jsonResponse, deps);
|
||||||
}
|
}
|
||||||
|
if (url.pathname === '/api/settings/codex-features') {
|
||||||
|
return handleCodexFeaturesRoute(request, jsonResponse, deps);
|
||||||
|
}
|
||||||
if (url.pathname === '/api/settings/moa') {
|
if (url.pathname === '/api/settings/moa') {
|
||||||
return handleMoaSettingsRoute(request, jsonResponse, deps);
|
return handleMoaSettingsRoute(request, jsonResponse, deps);
|
||||||
}
|
}
|
||||||
@@ -335,6 +362,7 @@ export async function handleSettingsRoute({
|
|||||||
export type {
|
export type {
|
||||||
ClaudeAccountSummary,
|
ClaudeAccountSummary,
|
||||||
CodexAccountSummary,
|
CodexAccountSummary,
|
||||||
|
CodexFeatureSnapshot,
|
||||||
FastModeSnapshot,
|
FastModeSnapshot,
|
||||||
ModelConfigSnapshot,
|
ModelConfigSnapshot,
|
||||||
MoaSettingsSnapshot,
|
MoaSettingsSnapshot,
|
||||||
|
|||||||
Reference in New Issue
Block a user