Engineering archive
These notes describe work at the date shown. See the usage reference for current behavior.
Pool run record, 2026-09-10: polish tasks through the pool
Date: 2026-09-10, 00:25–01:30. Laptop CLI and worker helpers at main e8c1bb2 when the run started (newer builds were installed as fixes landed during the run). Three Mac minis: mini-1 (Cursor, env profile agents), mini-2 and mini-3 (Codex, gpt-5.6-luna, effort max). The project was a clean checkout of main. Task ids are shortened; prompts, paths and secrets are omitted, as the acceptance runbook requires.
What was run
One batch, worker task batch polish-2026-09-10.toml --name polish-2026-09-10 --max-parallel 3 --wait, with per-task worker, agent and env_profile overrides, then one retry submitted with --project from another directory.
| Task | Agent, worker | Outcome | Landed as |
|---|---|---|---|
4897403d render mac-worker's turn-framing events in task logs | Cursor, mini-1 | done in ~10 min; matched the prompt, including the privacy rule and tests | f309f96 (squash, message rewritten) |
e141be47 sync operator docs with the previous day's changes | Codex, mini-2 | done in ~12 min; docs only, links verified | ba32d33 (squash) |
a9287cd0 harden two timing-window lib tests | Codex, mini-3 | failed in 2 min: Codex auth (finding 3) | — |
af449fda the same task, retried | Codex, mini-2 | done in ~22 min; hardened one of the two tests; the runner then lost its queue row (finding 8) | 1c1c378 (squash) |
The batch finished in 718 s with exit 1 because of the failed task. task result gave the summary, the files and the branch for each finished task; task fetch brought each branch to the laptop as refs/remotes/mac-worker/<worker>/task/<id> whose tip is a synthetic commit "mac-worker: uncommitted changes after turn …" on top of the base. Every branch was gated on the laptop (fmt, clippy, the affected suites, the lib suite) and squash-merged with a proper message.
Findings
task list --state activefailed withTASK_VIEW_MISSING_TASK: task errorwhile the run was active.TaskClient::listfiltered task records by state before projecting runs, so a run referencing a filtered-out task broke the projection. Fixed the same night in308e52e: the projection runs over all records and the filter applies to the projected rows.--rundid not accept the batch name.task batch --name polish-2026-09-10printed a name, buttask list --run polish-2026-09-10andtask wait --run …demanded a UUID. Fixed in308e52e:--runresolves a UUID first, then an exact stored name.One ChatGPT login shared across minis burns Codex refresh tokens. The Codex turn on mini-3 died at once with
Your access token could not be refreshed because your refresh token was already used. Please log out and sign in again.The sameauth.jsonhad been copied to every mini; refresh tokens are single-use, so machines invalidate each other.codex login statusstill printedLogged in using ChatGPTon all three, so the facts kept advertising Codex on mini-3 — the same shape as the Cursor "login unverified" problem fixed the day before. Actions: a separatecodex loginper mini (operator), and a mac-worker follow-up, landed the next day asd564966,749c1b9and327710c: each adapter lists its fixed authentication-failure phrases, a failed turn whose stream tails match them records a private incident besidefacts.json(agent, profile name, a fixed reason, the time; never the log), the facts then showunknown (auth failed in a turn at <UTC minute>)for that agent, the capability disappears until a later turn of that agent succeeds, 24 hours pass, Codex'sauth.jsonis newer than the incident, or the operator runsworker workers --refresh --clear-auth-incidents; the failed turn's outcome readsagent authentication failed, and the cached host probe applies the overlay without waiting for a refresh.A pinned
submitto a busy worker fails fast withCAPACITY_BUSY: no eligible worker currently has an available heavy slot(the reason is public since the day before).submithas no wait-for-capacity flag; only batch runs park. By design, noted for the docs.Rendered logs were readable mid-turn (
event: tool_call/completed ×2,event: item.started, command lines); afterf309f96the framing lines readaccepted by mini-2and the terminal line is silent.Local
maindrifts. Pushingmainswept two commits another session had left unpushed on the shared local branch (915227f,dc5f937), for the third time in two days. Sessions should push their own commits or work on branches.Idle pool facts age out. Seen at the start of the run: after 15 minutes without a refresh the dashboard showed
herdr: unknownon two workers. Fixed in77bc222: the dashboard refreshes stale facts once per TTL in the background and renders stale facts with their age.A completed turn's queue row stayed
dispatchingand nothing could clear it. The retry turn (af449fda) endeddoneat 00:49: the worker's status has the turnsucceeded, the runner log's checkpoint records completion, and the runner cleared its identity. But its queue row was never removed. Consequences:task wait(which now waits for quiescence) ran to its 50-minute timeout,task closerefuses withTASK_BUSY: task turn is being dispatched, and everytask reconcileadopts the row to a fresh runner that dies within a second without a diagnostic, leaving the row under a dead pid. The runner writes its early-exit diagnostic only when the log is still empty, so the reason both runners failed after acceptance is lost. Most likely the original runner lost the row to await-driven reconcile (waitpollsreconcile_runnersevery 100 ms) that mis-judged it dead under load, then failedremove_task_turn_after_terminalwith an owner mismatch. Fixed the next day in057f1abande636ddb: a replacement runner resumes an accepted journal from its committed offsets instead of submitting again, a worker job that is gone after acceptance ends the journal as an explicitLOG_DRAIN_UNAVAILABLEcompletion (never a fabricatedturn_terminal), one finalizer shared by reconcile and the runner publishes the local failure, imports this turn's result and releases the base pin only after that import before retiring the row,task wait,task logs -fand the dashboard show that state instead of hanging, and the early-exit diagnostic is appended even when the log has content, prefixedexited after acceptance:. The first draft, which completed the journal from the task record, was withdrawn during review because the record is written before the result is fetched. Note for operators: the private state directories reject unexpected files — a backup copy placed next toqueue/state.jsonmade every task command fail withIO: I/O erroruntil it was moved out.Worker toolchains lag the laptop. The minis run rustc 1.94.1 (March 2026) while the laptop runs 1.98.0; the Codex turn hit a clippy lint on the old toolchain that the laptop's clippy does not raise. Keep
rustup updateon the minis in the setup checklist.The Codex sandbox on a worker cannot run the whole lib suite. One test that manipulates process groups failed inside the sandbox with a permission error; the agent reported it correctly and continued. Rust tasks that need the full lib suite should ask for a narrower gate or expect this.
The retry's hardening was partial. The rooted_fs cleanup-guard test now waits on its condition with a ten-second deadline (three lib runs alone: green). Two lib suites started at the same time on the laptop still fail one or two tests (
runner_logpending-crash windows among them), so the parallel-gate flakiness stays on the backlog.A named pipe in a job's
tmpblocks the worker's cleanup forever, and that blocked the laptop too. Found while closing finding 8 with the new reconcile: its post-acceptance diagnostic readexited after acceptance: HOST_IO. On the worker, job5bae1707wassucceededwithcleanup_error_code: MUTABLE_CLEANUP_FAILEDandsupervisor.logsaidI/O error: Invalid argument (os error 22): the agent's shell tool had left ~841.tmpXXXXXXdirectories under the job'stmp, one holdingcommand-ready.fifo, and the durable cleanup walk inrooted_fsrejected any entry that was not a directory, regular file or symlink. The lease was never released (slot: busyon that worker for fifteen hours), and every status or log read from the laptop re-ran the failing cleanup and answeredHOST_IO, so the accepted turn could not be drained. Fixed in14beaec: cleanup unlinks FIFOs, sockets and device nodes without opening them, sealing skips them like symlinks, and terminal status and log reads succeed even while a deferred cleanup still fails;cleanup_error_codeand the busy slot remain the operator's signal, andreconcile/resolution still report the error.
Second run, 17:34–18:00: unpinned batch on main f2843df
worker task batch live-2026-09-10b.toml --name live-2026-09-10b --max-parallel 3 --wait, three tasks without a worker pin: two Codex (a docs/usage.md update and a runner_log test hardening) and one Cursor with env_profile = "agents" told to ask one question before writing. The scheduler placed them on mini-3, mini-1 and mini-2 within seconds. All three finished done after one task say each, were fetched and landed as 7295743, e295a9d and c4c6408.
- A
needs_inputresult with a real question was recorded asunknown. The Cursor turn ended with prose followed by the JSON object;extract_last_json_objecttried a parse at every{and kept the innermost one, the question object, so any non-emptyquestionsarray was unrecognisable. Fixed in303472d: after a successful parse the scanner skips to that object's end, so only top-level objects are candidates. - A
task sayfollow-up inside a run at full parallelism parked itself.run_has_capacitycounted every Active task of the run, including the task that owns the follow-up row, so two siblings plus itself hitmax_parallel = 3and the row waited withRUN_MAX_PARALLELuntil a sibling finished. Fixed inc914e17: rows map back to their tasks, each task counts once, and the follow-up's own task is excluded. task batch --waitaborted withQUEUE_JOB_CONFLICT(exit 70) two minutes in while every task kept running.queue_entry_for_task_turnmaps only turn directories; a pending turn present in the status but without a directory looked row-less to both recovery phases ofreconcile_runners, so the second phase enqueued the row the first had just published. Fixed ind282692:enqueue_if_absentreturns the existing row, both phases share one queue snapshot, a turn that is already terminal is never re-enqueued, andwaitsurvives a transient conflict. Left for later:start_runnerstill uses the directory-only lookup.- The Codex sandbox cannot commit in the task workspace. Both Codex tasks did their work and then reported
blockedorneeds_inputbecause.git/index.lockis not writable; mac-worker imports the working tree as a synthetic commit at the end of the turn, so the work was not lost, but a prompt that demands a commit turns a done task into a question. Cursor's sandbox allowed the commit. Documented indocs/usage.mdin the same change as this record. - Replacement-runner churn from
task wait(seen while closing finding 8):waitpolled reconcile every 100 ms and each poll restarted a runner that died after acceptance with the same error. Fixed in00ce0d1: a per-row restart budget with backoff (2 s, 10 s, 30 s, 60 s), parking withRUNNER_REPEATED_FAILURE:<code>after five identical failures,WAIT_BLOCKEDfromwait, andworker task reconcileas the operator's reset. - Cosmetic residue of finding 12: the job's
status.jsonkeptcleanup_error_code: MUTABLE_CLEANUP_FAILEDafter the retried cleanup succeeded. Fixed in68b0810: a terminal status may clear the code once, through the same fenced write that records it.
Not exercised
- Automatic (unpinned) scheduling: every task was pinned so each worker got exactly one.
task sayfollow-ups: no task endedneeds_input.- Publication to an origin (
publish = ["fetch"]only).