Repository navigation
Job records never capture the resolved model/effort/sandbox a job ran with #481
Description
Summary
jobs/<id>.json never records what model, reasoning effort, or sandbox a job actually executed with. Background task jobs persist the raw CLI flags, which are null whenever the caller relied on ~/.codex/config.toml defaults; foreground task jobs and all review jobs record nothing at all. External tooling that wants to display "this job ran gpt-5.4 / medium / read-only" has to reverse-engineer it from Codex CLI rollout files.
Evidence (at HEAD db52e28, plugin v1.0.6)
- Base job records carry no model/effort fields:
createCompanionJob(plugins/codex/scripts/codex-companion.mjs:567-578) pluscreateJobRecord(plugins/codex/scripts/lib/tracked-jobs.mjs:60-68) produceid,kind,kindLabel,title,workspaceRoot,jobClass,summary,write,createdAt,sessionId. - Background tasks additionally persist the raw request —
enqueueBackgroundTaskstoresrequest(codex-companion.mjs:689-698), built from the CLI flags as passed (buildTaskRequest,codex-companion.mjs:604-614). If--model/--effortwere omitted,request.model/request.effortarenull, while the job really runs with the config.toml defaults resolved by the app-server. runTrackedJobnever adds resolved values to the running or completed record (tracked-jobs.mjs:142-204).- Review jobs and foreground tasks go through
runForegroundCommandwith norequestpersisted at all (codex-companion.mjs:731-752,807-822). - The resolved values are available to the plugin: the
thread/start/thread/resumeresponse already carriesmodel,reasoningEffort,sandbox, andapprovalPolicy— the repo's own fake app-server mirrors this shape (tests/fake-codex-fixture.mjs:316).
Repro
node scripts/codex-companion.mjs task --background "any prompt"(no--model, no--effort).- Inspect
$CLAUDE_PLUGIN_DATA/state/<slug>-<hash>/jobs/<id>.json:request.modelandrequest.effortarenull; no other field names the model/effort/sandbox. - Run
node scripts/codex-companion.mjs review: the stored job has no model/effort trace at all. - The actual values only exist in the Codex CLI rollout
turn_contextentries under$CODEX_HOME/sessions/YYYY/MM/DD/rollout-<ts>-<threadId>.jsonl.
Proposed fix
At thread/turn start, write the resolved model, effort, and sandbox (as reported by the app-server response) into the job record on all paths — background tasks, foreground tasks, and reviews — e.g. alongside threadId/turnId in the running record written by runTrackedJob.
Happy to follow up with a PR if this direction sounds right.
Why it matters
The downstream log watcher @cubicj/codex-watch displays a per-job session-info line. To get it, it joins the job's threadId against rollout filenames and parses turn_context payloads. That workaround is fragile in ways the plugin can't see: rollout date directories use local time while job timestamps are UTC, resumed threads live under the thread's original date directory, and rollouts are pruned independently of job records. One field written at turn start removes the entire join.
Related
- review / adversarial-review silently ignore reasoning effort — --effort unparsed, and turn/start effort omitted on the adversarial path #476 — review paths ignore
--effort; same underlying theme (effort is invisible/unthreaded), different bug. - Feature: forward extra args (custom model_provider / profile) to every codex launch #418 — forwarding extra args to the Codex CLI; adjacent, about inputs rather than records.
- Add observer and worktree features with multi-root support #359 —
/codex:observestructured event stream; complementary observability interest.
Environment
- Plugin
codex@openai-codexv1.0.6 (upstream HEAD db52e28) - Codex CLI 0.145.0-alpha.2
- Claude Code on WSL2 Ubuntu 26.04, Node v24.18.0
Activity
axisrow commentedon Jul 12, 2026
Seconding this from the reviewer's side — I just hit every trap you described, with concrete numbers, trying to answer "did
--effort highactually reach the model in this review?".I ran a
codex adversarial-review --effort high(via the plugin's companion, codex-cli 0.144.1) and then tried to confirm the resolved effort. What I found maps 1:1 onto your write-up:jobs/review-<id>.jsonpersistedstatus,threadId,turnId,startedAt/completedAt— but nothing about the model orreasoning_effortit ran with. Exactly your point aboutcreateJobRecord/runTrackedJob.turn_context, and it's hard to join:createdAtwas2026-07-12T05:30Z(UTC), but the rollout landed in the2026/07/11/date directory — the thread's original-date dir, not today's. Same local-vs-UTC drift you called out.019f50ff-…), not thethreadId(019f54ce-…) the job record stores. Joining jobthreadId→ rollout filename fails outright.ccusage(great tool, btw) didn't surface this session at all — presumably because its date-dir scan skipped the07/11placement for a job that "ran" on07/12. A clean illustration of why external tooling can't rely on the join.For the record, the resolved effort was correct once I finally found the rollout: the main
turn_contextentries carriedreasoning_effort: "high"(thelowentries were Codex's internal helperevent_msgs, not the override leaking). So the override works — it's purely the observability that's missing.Your proposed fix (write resolved
model/effort/sandboxfrom thethread/startresponse into the job record on all paths) is exactly what would have saved me ~30 minutes of grepping rollout dirs. +1 to that direction. Happy to test/review a PR, or to contribute the job-record write against your design if you'd rather hand it off.(Side note: this is also the same invisibility theme as #476 — review silently ignoring
--effort. Once the override is wired through, #481 is what lets anyone prove it took effect.)cc @hiirott (#408, spawn-
-cmodel fix — knowing the resolved model up front at spawn would make writing it into the job record trivial), @klmklmnb (#418, forwarding extra args to the Codex CLI — adjacent, about inputs the job never records), @jianjettfu-oss (#309, the unsupported-default-model boot failure on review) — your takes on where the resolved model/effort should be captured would be useful here.cubicj commentedon Jul 12, 2026
Thanks for the independent reproduction — the join pain you hit (UTC job timestamps against the thread's original date dir, session-id filenames against the job's
threadId) is exactly the cost this issue was meant to capture, and confirming the override itself worked makes the gap cleaner to see.I originally filed this as an issue rather than a PR because I expected "which values count as resolved" to depend on which override mechanism lands, whether that's #471's
thread/start.configroute, spawn-time-cfrom #408, or today's request fields. I went through the code again before replying, and I no longer think it does.ThreadStartResponseandThreadResumeResponsealready carry resolvedmodel,modelProvider,reasoningEffort, andsandboxas a fullSandboxPolicyobject, with the config.toml fallback applied — I probed this over JSON-RPC earlier: omitted params come back resolved from config, explicit params come back echoed. The plugin receives all of it and throws it away.startThreadreturns the full response (plugins/codex/scripts/lib/codex.mjs:733), but both callers read onlyresponse.thread.id(codex.mjs:1010for native review,codex.mjs:1114for the turn path). Since it's the server reporting what it resolved, the capture point stays truthful under any of the pending mechanism decisions, including a config-driven sandbox from #482. The one gap is thatTurnStartResponseis{ turn }only while the task path passeseffortatturn/start(codex-companion.mjs:490), so the thread-level echo needs anoptions.effort ?? response.reasoningEffortoverlay to reflect an explicit per-turn override.The change I'd propose is observability-only and reuses the plumbing
threadIdalready rides. Inlib/codex.mjs, pickresolved = { model, modelProvider, reasoningEffort, sandbox }from thethread/start/thread/resumeresponse inrunAppServerTurnandrunAppServerReview, attach it to the existing "Thread ready" progress event, and include it in the returned result. Inlib/tracked-jobs.mjs, let the progress updater patchresolvedinto the job file mid-run the same way it patchesthreadId, and addresolved: execution.resolvedto the completion write —tracked-jobs.mjs:158spreads the in-memory record, so a mid-run-only patch would be clobbered there. Incodex-companion.mjs, liftresult.resolvedinto the execution objects next tothreadId/turnId; every job kind sharesrunTrackedJob, so foreground and background paths get it without per-path changes. Tests would extend the fake-codex fixture'sthread/startresponse and assertresolvedlands in the job JSON across task, background worker, native review, and adversarial review, plus a failure case keeping the mid-run value.The mid-run patch is what addresses your scenario directly: a failed or cancelled job still records what it actually ran with, so proving an override took effect never requires the rollout join again. Keeping
resolvedas a nested object next to the existing raw request fields also leaves both layers visible, requested versus server-confirmed, which is the comparison #476 debugging needs. The only call I'd still want from a maintainer is the field shape — nestedresolved: {...}versus flat fields on the record. Happy to send the PR on this design, and the rollout cross-checking you've already done here is exactly the validation it needs.