Skip to content

Commit 330241c

Browse files
committed
Default task execution to Codex
1 parent e2e2ba9 commit 330241c

6 files changed

Lines changed: 73 additions & 44 deletions

File tree

channels/agent_utils.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
"claude": "Claude Code (claude CLI)",
2121
"codex": "Codex CLI (openai/codex)",
2222
}
23+
DEFAULT_AGENT = "codex"
2324

2425
# Regex: /agent <name>
2526
_AGENT_CMD_RE = re.compile(
@@ -62,6 +63,6 @@ def handle_agent_command(
6263
def resolve_agent(channel_key: str, db: "TaskDB") -> str:
6364
"""
6465
Return the current default agent from settings.
65-
Falls back to "claude" if not set.
66+
Falls back to the app default if not set.
6667
"""
67-
return db.get_setting("default_agent", "claude")
68+
return db.get_setting("default_agent", DEFAULT_AGENT)

taskboard-electron/src/renderer/App.jsx

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,8 @@ const AGENTS = {
9595
claude: { label: "Claude Code", icon: "⌘", color: "#7c6aff" },
9696
codex: { label: "Codex CLI", icon: "◈", color: "#10a37f" },
9797
};
98+
const DEFAULT_AGENT = "codex";
99+
const DEFAULT_TIMEOUT_SECONDS = 12000;
98100

99101
// ─── Formatted Output Component ───
100102
function FormattedOutput({ content, theme }) {
@@ -1388,7 +1390,7 @@ function HeartbeatModal({ onClose, onSubmit, initialData, defaultAgent, mode = "
13881390
cron_expr: initialData?.cron_expr || "",
13891391
check_prompt: initialData?.check_prompt || "",
13901392
action_prompt_template: initialData?.action_prompt_template || "",
1391-
default_agent: initialData?.default_agent || defaultAgent || "claude",
1393+
default_agent: initialData?.default_agent || defaultAgent || DEFAULT_AGENT,
13921394
cooldown_seconds: initialData?.cooldown_seconds || 1800,
13931395
enabled: initialData?.enabled ?? true,
13941396
}));
@@ -2118,7 +2120,7 @@ function NewTaskModal({ onClose, onSubmit, initialData, mode = "create" }) {
21182120
: "",
21192121
max_runs: initialData.max_runs || "",
21202122
tags: initialData.tags || "",
2121-
agent: initialData.agent || "claude",
2123+
agent: initialData.agent || DEFAULT_AGENT,
21222124
dag_id: initialData.dag_id || "",
21232125
};
21242126
}
@@ -2132,7 +2134,7 @@ function NewTaskModal({ onClose, onSubmit, initialData, mode = "create" }) {
21322134
scheduled_at: "",
21332135
max_runs: "",
21342136
tags: "",
2135-
agent: "claude",
2137+
agent: DEFAULT_AGENT,
21362138
dag_id: "",
21372139
};
21382140
});
@@ -2881,7 +2883,7 @@ function DetailPanel({ task, onClose, onResume }) {
28812883

28822884
<Section title="Configuration">
28832885
<InfoRow label="Working Dir" value={task.working_dir} />
2884-
<InfoRow label="Agent" value={task.agent || "claude"} />
2886+
<InfoRow label="Agent" value={task.agent || DEFAULT_AGENT} />
28852887
<InfoRow label="Schedule" value={task.schedule_type} />
28862888
{task.cron_expr && <InfoRow label="Cron" value={task.cron_expr} />}
28872889
{task.delay_seconds && <InfoRow label="Delay" value={`${task.delay_seconds}s`} />}
@@ -3369,10 +3371,10 @@ function SettingsModal({
33693371
onChannelsSave,
33703372
}) {
33713373
const [tab, setTab] = useState("general");
3372-
const [timeout, setTimeout] = useState(initialTimeout ?? 600);
3373-
const [defaultAgent, setDefaultAgent] = useState(initialDefaultAgent ?? "claude");
3374+
const [timeout, setTimeout] = useState(initialTimeout ?? DEFAULT_TIMEOUT_SECONDS);
3375+
const [defaultAgent, setDefaultAgent] = useState(initialDefaultAgent ?? DEFAULT_AGENT);
33743376
const [skillEnabled, setSkillEnabled] = useState(false);
3375-
const [skillSweepAgent, setSkillSweepAgent] = useState("claude");
3377+
const [skillSweepAgent, setSkillSweepAgent] = useState(DEFAULT_AGENT);
33763378
const [skillSweepCron, setSkillSweepCron] = useState("0 3 * * *");
33773379
const [feishu, setFeishu] = useState({
33783380
feishu_app_id: "",
@@ -3488,13 +3490,13 @@ function SettingsModal({
34883490

34893491
const handleSaveGeneral = async () => {
34903492
await updateSettings({
3491-
timeout: parseInt(timeout) || 600,
3493+
timeout: parseInt(timeout) || DEFAULT_TIMEOUT_SECONDS,
34923494
default_agent: defaultAgent,
34933495
skill_library_enabled: skillEnabled ? "1" : "0",
34943496
skill_sweep_agent: skillSweepAgent,
34953497
skill_sweep_cron: skillSweepCron,
34963498
});
3497-
onSave(parseInt(timeout) || 600, defaultAgent);
3499+
onSave(parseInt(timeout) || DEFAULT_TIMEOUT_SECONDS, defaultAgent);
34983500
onClose();
34993501
};
35003502

@@ -3644,7 +3646,7 @@ function SettingsModal({
36443646
style={fieldStyle}
36453647
/>
36463648
<div style={hintStyle}>
3647-
Default: 600s (10 min). Max time before a running task is killed.
3649+
Default: 12000s (200 min). Max time before a running task is killed.
36483650
</div>
36493651
</div>
36503652
<div style={{ marginBottom: 20 }}>
@@ -5406,8 +5408,8 @@ export default function App() {
54065408
const [heartbeatDetail, setHeartbeatDetail] = useState(null);
54075409
const [connected, setConnected] = useState(false);
54085410
const [filter, setFilter] = useState("");
5409-
const [taskTimeout, setTaskTimeout] = useState(600);
5410-
const [defaultAgent, setDefaultAgent] = useState("claude");
5411+
const [taskTimeout, setTaskTimeout] = useState(DEFAULT_TIMEOUT_SECONDS);
5412+
const [defaultAgent, setDefaultAgent] = useState(DEFAULT_AGENT);
54115413
const [feishuSettings, setFeishuSettings] = useState({});
54125414
const [channelsStatus, setChannelsStatus] = useState({});
54135415
const [backendReady, setBackendReady] = useState(false);

taskboard.py

Lines changed: 33 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@
4848
"--permission-mode",
4949
"bypassPermissions",
5050
]
51+
DEFAULT_AGENT = "codex"
52+
DEFAULT_TIMEOUT_SECONDS = 12000
5153

5254
LIVE_OUTPUT_EVENT_TYPES = {
5355
"assistant",
@@ -237,10 +239,10 @@ class Task:
237239
created_at: Optional[str] = None
238240
updated_at: Optional[str] = None
239241
tags: str = "" # comma-separated
240-
agent: str = "claude"
242+
agent: str = DEFAULT_AGENT
241243
question: Optional[str] = None # question the agent asked
242244
answer: Optional[str] = None # user's answer
243-
session_id: Optional[str] = None # claude session id for --resume
245+
session_id: Optional[str] = None # agent session/thread id for resume
244246
prompt_images: list = field(default_factory=list) # [{media_type, data, name}]
245247
image_paths: list = field(default_factory=list) # list of local image file paths
246248
dag_id: Optional[str] = None # optional DAG workflow group label
@@ -258,7 +260,7 @@ class Heartbeat:
258260
interval_seconds: Optional[int] = None
259261
check_prompt: str = ""
260262
action_prompt_template: str = ""
261-
default_agent: str = "claude"
263+
default_agent: str = DEFAULT_AGENT
262264
cooldown_seconds: int = 0
263265
next_run_at: Optional[str] = None
264266
last_tick_at: Optional[str] = None
@@ -302,14 +304,14 @@ def _init_db(self):
302304
created_at TEXT DEFAULT (datetime('now')),
303305
updated_at TEXT DEFAULT (datetime('now')),
304306
tags TEXT DEFAULT '',
305-
agent TEXT DEFAULT 'claude',
307+
agent TEXT DEFAULT 'codex',
306308
question TEXT,
307309
answer TEXT
308310
)
309311
""")
310312
# Migration: add agent column if it doesn't exist (for existing DBs)
311313
try:
312-
self.conn.execute("ALTER TABLE tasks ADD COLUMN agent TEXT DEFAULT 'claude'")
314+
self.conn.execute("ALTER TABLE tasks ADD COLUMN agent TEXT DEFAULT 'codex'")
313315
self.conn.commit()
314316
except sqlite3.OperationalError:
315317
pass # Column already exists
@@ -387,7 +389,7 @@ def _init_db(self):
387389
interval_seconds INTEGER,
388390
check_prompt TEXT NOT NULL,
389391
action_prompt_template TEXT DEFAULT '',
390-
default_agent TEXT DEFAULT 'claude',
392+
default_agent TEXT DEFAULT 'codex',
391393
cooldown_seconds INTEGER DEFAULT 0,
392394
next_run_at TEXT,
393395
last_tick_at TEXT,
@@ -1597,7 +1599,10 @@ class AgentExecutor:
15971599

15981600
@staticmethod
15991601
def run(
1600-
prompt: str, working_dir: str = ".", timeout: int = 600, image_paths: list[str] = None
1602+
prompt: str,
1603+
working_dir: str = ".",
1604+
timeout: int = DEFAULT_TIMEOUT_SECONDS,
1605+
image_paths: list[str] = None,
16011606
) -> tuple[bool, str]:
16021607
"""
16031608
Run a prompt through Claude Code CLI.
@@ -2021,7 +2026,7 @@ def run_skill_sweep(self, agent: Optional[str] = None, full: bool = False) -> di
20212026
agent = (
20222027
agent
20232028
or self.db.get_setting("skill_sweep_agent", None)
2024-
or self.db.get_setting("default_agent", "claude")
2029+
or self.db.get_setting("default_agent", DEFAULT_AGENT)
20252030
)
20262031
watermark = self.db.get_setting("skill_sweep_watermark", "") or ""
20272032
if full:
@@ -2196,7 +2201,7 @@ def distill_skill_draft(self, pattern_id: int, agent: Optional[str] = None) -> d
21962201
agent = (
21972202
agent
21982203
or self.db.get_setting("skill_sweep_agent", None)
2199-
or self.db.get_setting("default_agent", "claude")
2204+
or self.db.get_setting("default_agent", DEFAULT_AGENT)
22002205
)
22012206
try:
22022207
tids = json.loads(pattern["contributing_task_ids"]) or []
@@ -2426,7 +2431,7 @@ def _run_agent_command(
24262431
return False, f"{agent} CLI not found"
24272432
except OSError as e:
24282433
return False, str(e)
2429-
timeout_secs = int(self.db.get_setting("timeout", "600"))
2434+
timeout_secs = int(self.db.get_setting("timeout", str(DEFAULT_TIMEOUT_SECONDS)))
24302435
stdout_chunks = []
24312436
stderr_chunks = []
24322437

@@ -2549,7 +2554,7 @@ def _execute_heartbeat(self, heartbeat: dict):
25492554
interval_seconds=heartbeat.get("interval_seconds"),
25502555
check_prompt=heartbeat["check_prompt"],
25512556
action_prompt_template=heartbeat.get("action_prompt_template") or "",
2552-
default_agent=heartbeat.get("default_agent") or "claude",
2557+
default_agent=heartbeat.get("default_agent") or DEFAULT_AGENT,
25532558
cooldown_seconds=int(heartbeat.get("cooldown_seconds") or 0),
25542559
),
25552560
now,
@@ -2560,7 +2565,7 @@ def _append_tick_output(line: str):
25602565
output_chunks.append(line)
25612566
self._live_heartbeat_output[tick_id] = "".join(output_chunks)
25622567

2563-
agent = heartbeat.get("default_agent") or "claude"
2568+
agent = heartbeat.get("default_agent") or DEFAULT_AGENT
25642569
prompt = self._render_heartbeat_check_prompt(heartbeat)
25652570
working_dir_expanded = os.path.expanduser(heartbeat["working_dir"])
25662571
if agent == "codex":
@@ -2616,7 +2621,7 @@ def _append_tick_output(line: str):
26162621
prompt=task_prompt,
26172622
working_dir=heartbeat["working_dir"],
26182623
schedule_type=ScheduleType.IMMEDIATE,
2619-
agent=heartbeat.get("default_agent") or "claude",
2624+
agent=heartbeat.get("default_agent") or DEFAULT_AGENT,
26202625
tags="heartbeat",
26212626
)
26222627
task_id = self.submit_task(task)
@@ -3237,7 +3242,7 @@ def _is_safe_image_path(path: str) -> bool:
32373242
except Exception as e:
32383243
logger.error(f"Task {tid}: Failed to load image {img_path}: {e}")
32393244

3240-
agent = task.get("agent", "claude")
3245+
agent = task.get("agent") or DEFAULT_AGENT
32413246
use_stdin = bool(prompt_images) and agent == "claude"
32423247

32433248
if agent == "codex":
@@ -3285,7 +3290,7 @@ def _is_safe_image_path(path: str) -> bool:
32853290
# Popen itself raises (e.g. CLI not found) before the timer is armed.
32863291
timed_out = [False]
32873292
try:
3288-
timeout_secs = int(self.db.get_setting("timeout", "600"))
3293+
timeout_secs = int(self.db.get_setting("timeout", str(DEFAULT_TIMEOUT_SECONDS)))
32893294
start_time = time.time()
32903295
proc = subprocess.Popen(
32913296
cmd,
@@ -3771,7 +3776,7 @@ def resume_heartbeat(self, heartbeat_id: int):
37713776
interval_seconds=heartbeat.get("interval_seconds"),
37723777
check_prompt=heartbeat["check_prompt"],
37733778
action_prompt_template=heartbeat.get("action_prompt_template") or "",
3774-
default_agent=heartbeat.get("default_agent") or "claude",
3779+
default_agent=heartbeat.get("default_agent") or DEFAULT_AGENT,
37753780
cooldown_seconds=int(heartbeat.get("cooldown_seconds") or 0),
37763781
),
37773782
datetime.now(),
@@ -3936,7 +3941,7 @@ def _coerce_bool(value):
39363941
"default_agent",
39373942
existing.get("default_agent")
39383943
if existing
3939-
else self.db.get_setting("default_agent", "claude"),
3944+
else self.db.get_setting("default_agent", DEFAULT_AGENT),
39403945
)
39413946
),
39423947
cooldown_seconds=cooldown_seconds,
@@ -4112,11 +4117,11 @@ def do_GET(self):
41124117
elif path == "/api/settings":
41134118
self._json_response(
41144119
{
4115-
"default_agent": self.db.get_setting("default_agent", "claude"),
4116-
"timeout": int(self.db.get_setting("timeout", "600")),
4120+
"default_agent": self.db.get_setting("default_agent", DEFAULT_AGENT),
4121+
"timeout": int(self.db.get_setting("timeout", str(DEFAULT_TIMEOUT_SECONDS))),
41174122
"skill_library_enabled": self.db.get_setting("skill_library_enabled", "0")
41184123
== "1",
4119-
"skill_sweep_agent": self.db.get_setting("skill_sweep_agent", "claude"),
4124+
"skill_sweep_agent": self.db.get_setting("skill_sweep_agent", DEFAULT_AGENT),
41204125
"skill_sweep_cron": self.db.get_setting("skill_sweep_cron", "0 3 * * *"),
41214126
}
41224127
)
@@ -4361,7 +4366,7 @@ def do_POST(self):
43614366
next_run_at=body.get("next_run_at"), # Allow setting next_run_at directly
43624367
max_runs=body.get("max_runs"),
43634368
tags=body.get("tags", ""),
4364-
agent=body.get("agent") or self.db.get_setting("default_agent", "claude"),
4369+
agent=body.get("agent") or self.db.get_setting("default_agent", DEFAULT_AGENT),
43654370
prompt_images=prompt_images,
43664371
image_paths=image_paths,
43674372
dag_id=body.get("dag_id"),
@@ -4596,7 +4601,7 @@ def do_POST(self):
45964601
next_run_at=tdef.get("next_run_at"),
45974602
max_runs=tdef.get("max_runs"),
45984603
tags=tdef.get("tags", ""),
4599-
agent=tdef.get("agent") or self.db.get_setting("default_agent", "claude"),
4604+
agent=tdef.get("agent") or self.db.get_setting("default_agent", DEFAULT_AGENT),
46004605
prompt_images=prompt_images,
46014606
dag_id=dag_id,
46024607
)
@@ -4945,15 +4950,15 @@ def do_DELETE(self):
49454950

49464951
MAX_BODY_SIZE = 10 * 1024 * 1024 # 10 MB
49474952

4948-
def _drain_body(self):
4949-
"""Consume and discard the request body (capped) so an early rejection
4950-
doesn't desync the keep-alive connection and surface to the client as a
4953+
def _drain_body(self, length: Optional[int] = None):
4954+
"""Consume and discard the request body so an early rejection doesn't
4955+
desync the keep-alive connection and surface to the client as a
49514956
connection reset instead of the intended response."""
49524957
try:
4953-
length = int(self.headers.get("Content-Length", 0))
4958+
length = int(self.headers.get("Content-Length", 0)) if length is None else length
49544959
except (TypeError, ValueError):
49554960
return
4956-
remaining = min(length, self.MAX_BODY_SIZE)
4961+
remaining = max(length, 0)
49574962
while remaining > 0:
49584963
chunk = self.rfile.read(min(remaining, 65536))
49594964
if not chunk:
@@ -4973,6 +4978,7 @@ def _read_body(self):
49734978
except (TypeError, ValueError):
49744979
length = 0
49754980
if length > self.MAX_BODY_SIZE:
4981+
self._drain_body(length)
49764982
self._json_response({"error": "request body too large"}, 413)
49774983
return None
49784984
raw = self.rfile.read(length) if length > 0 else b""

tests/test_api_handler.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -207,8 +207,8 @@ def test_get_settings_defaults(api):
207207
client, _, _ = api
208208
status, data = client.get("/api/settings")
209209
assert status == 200
210-
assert data["default_agent"] == "claude"
211-
assert data["timeout"] == 600
210+
assert data["default_agent"] == "codex"
211+
assert data["timeout"] == 12000
212212
assert data["skill_library_enabled"] is False
213213

214214

@@ -871,7 +871,7 @@ def test_csrf_rejected_with_bad_token(api):
871871
)
872872
assert status == 403
873873
# State change must not have been applied.
874-
assert db.get_setting("default_agent", "claude") == "claude"
874+
assert db.get_setting("default_agent", "codex") == "codex"
875875

876876

877877
def test_csrf_accepted_with_valid_token(api):

tests/test_channel_utils.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,10 @@ def test_handle_agent_command_persists_default_agent():
8080
assert resolve_agent("telegram", db) == "codex"
8181

8282

83+
def test_resolve_agent_defaults_to_codex():
84+
assert resolve_agent("telegram", StubDB()) == "codex"
85+
86+
8387
def test_get_env_prepends_common_tool_paths(monkeypatch):
8488
monkeypatch.setenv("PATH", "/usr/bin:/bin")
8589
monkeypatch.setattr(os.path, "expanduser", lambda path: path.replace("~", "/Users/tester"))

tests/test_taskboard_gaps2.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
request-body-too-large 413.
2525
"""
2626

27+
import io
2728
import json
2829
import os
2930
import tempfile
@@ -774,3 +775,18 @@ def test_post_body_too_large_returns_413(api, monkeypatch):
774775
status, data = client.post("/api/tasks", {"prompt": "x" * 200, "schedule_type": "immediate"})
775776
assert status == 413
776777
assert "too large" in data["error"]
778+
779+
780+
def test_read_body_too_large_drains_declared_body(monkeypatch):
781+
handler = object.__new__(TaskAPIHandler)
782+
handler.headers = {"Content-Length": "32"}
783+
handler.rfile = io.BytesIO(b"x" * 32)
784+
handler.MAX_BODY_SIZE = 16
785+
responses = []
786+
monkeypatch.setattr(
787+
handler, "_json_response", lambda data, status=200: responses.append((data, status))
788+
)
789+
790+
assert handler._read_body() is None
791+
assert handler.rfile.read() == b""
792+
assert responses == [({"error": "request body too large"}, 413)]

0 commit comments

Comments
 (0)