A patch release. Codex now actually sees the unread line at the top of its turn, the runtime work that had lived only in five uncommitted desktop worktrees is on master, and the README says out loud that the OS runs on Codex. Tests 3,285 -> 3,391; psutil becomes a declared dependency.
Fixed
- Codex could not see the unread line (
c6c4cdb, merged in14bfeba) - the hook ran, its audit recorded 376 characters written, and the model answered that it had no unread context. Codex's host does not inject a hook's plain stdout; it takes the standardhookSpecificOutput.additionalContextJSON. The entry script now emits that form for non-empty output; the no-unread path stays silent and the diagnostic path stays on stderr, so nothing changes for the cases that already worked, and the audit records the wire length. Written by the peer, reviewed here, and measured after merge on both the Codex CLI (a blind marker with a random first line appeared in the model's context before any tool call, and the model returned it verbatim after its tool call was denied) and the desktop app (the user typed one word). What is measured is prompt-time delivery; an idle Codex session is still not woken by mail. psutilwas load-bearing but undeclared (5d528fec) - the runtime reconciliation merged in this release importedpsutilinside a try/except and then relied on it. With the module absent, a stale startup lock could never be reclaimed (the previous code reclaimed by age), the API never auto-started after a 20 s wait, the PID ledger always read empty, and the recovery branch for a stuck API was dead code, so a second uvicorn could start on a random port against the same SQLite file. Every test passed on the development machine becausepsutilarrived transitively through an unrelated package. Found by the review of the consolidated branch (one reviewer per dimension, one refuter per finding; two dimensions hit this independently).psutilis now declared inpyproject.toml,plugin/requirements.txtand the installer's fallback list, and absence degrades instead of paralysing: liveness falls back tokill(0)plusps, a stale lock is reclaimed only once its owner is confirmed dead and the file has not changed underneath, and withoutpsutilthe code never authorises a kill and never starts a second instance on an occupied port.- The health check read the port from a constant (
5d528fec) -os_health_checkderived the port from the import-timeAPI_URLconstant (always 8000) while the API records its real port in a file, so on the documented fallback path where 8000 is taken and the API lands on a random port, PID reconciliation always reportednot_managedand the reported URL was wrong. The two new tests patched the constant and hid it. It now uses the dynamic URL. - One hung request froze a Dashboard refresh prefix for good (
5d528fec) - the coalesced refresh keeps a key in flight until its request settles, andapiFetchhas no timeout, so one request that never returned stopped every later refresh of that prefix; the previous behaviour (cancelRefetch: true) had self-healed by cancelling and re-issuing. A 30 s deadline now settles the key and cancels only the request captured at that moment; a newer request on the same key is left alone. - Legacy WebSocket close swallowed cancellation (
ad11886) - on thelegacywebsockets backend,close()catchesCancelledErrorand shields the TCP teardown, so a timed-out send could still hold the HTTP acknowledgement hostage: 0.704 s measured against a 0.10 s budget on a real loopback handshake. Close now runs as a tracked task that is cancelled on deadline without being awaited, and late results are consumed by a done callback. 0.704 s -> 0.103 s.
Added
- Runtime consolidation from five desktop worktrees (
ad11886,5d528fec, merged ina85b7f7) - WebSocket broadcast isolates slow clients with bounded concurrent sends instead of awaiting them in series (that serial await is what pushed hook posts past their 1.5 s budget); the HTTP admission middleware keeps one of the five SQLite slots for hook events, so ordinary API traffic cannot starve them; the PID ledger reconciles on demand, adopting only a listener on the managed port whose process identity is verified, andos_restart_api(source_root=..., dry_run=true)checks imports without stopping the service; the Dashboard coalesces query refreshes over 200 ms without cancelling requests in flight. 1,178 lines of new tests, and the realtime hook tests now have a runner (npm test) wired intopreflight.shand CI, which also installs frompyproject.tomlinstead of a hand-kept list. The consolidation itself was done by the peer harness over the OS channel: it merged the five trees into one branch, this side reviewed (five dimensions, nine confirmed findings, one blocking), it fixed, this side re-verified and merged. The twelve desktop worktrees are gone; the six that held uncommitted work were first archived as local commits so the pre-consolidation state can be rebuilt. channel_waitreports how the message arrived (c360c9e, merged in8ab3dd8) - successful responses carrydelivery_source:replayfor the initial inbox read,eventfor a read triggered by a WebSocket event,timeout_readfor the final read after the wait expires. Until now a message returned by a wait could not be told apart from one the final read would have found anyway, which left the push path unverifiable. Error responses claim no source.- The README says the OS runs on Codex - a standing note under the tagline, separate from the per-release line, states what a Codex session gets (the same task wall, memory, reports, Dashboard and MCP tools, fed by its own native hooks; cross-harness messaging with a prompt-time unread line), what is still Claude Code only (one-command install, the session-start briefing, direction-layer memory for new subagents, waking an idle session), and that Codex setup is manual today. The
os-channelskill's per-harness capability table was rewritten to match what is now measured.
Changed
- Worktrees live inside the repository (
1b68077) -.worktrees/is ignored and the multi-session discipline inCLAUDE.mdnow requires it. Twelve sibling directories had accumulated on the Desktop, and sibling paths also fall outside the OS's subdirectory-based project attribution. - Release checklist step 11 restarts the local API first (
c55b696) -/api/healthreports the version constant held by the running process, so a long-lived API keeps answering with the previous version after the files change. The same root cause tripped both the peer harness and the Dashboard on the same day.
Upgrade notes
psutilis installed bypython install.py --update, which import-checks every declared dependency. Without it, on POSIX, an existing API is still recognised and a dead lock owner still reclaimed; uncertain identities never authorise a kill or a duplicate service.- Codex users: the unread hook changed script content only.
hooks.jsonand the trust lock are unchanged, so no re-trust is needed; the installed script takes effect on the next prompt. - Deferred, on the task wall: a recorded PID reused by an unrelated live process blocks auto-start with a warning until cleared, because the identity check does not yet distinguish "not our API" from "cannot tell"; and the guardrail middleware still rejects hook payloads that happen to contain path-traversal or code-injection lookalikes, a pre-existing loss path now recorded against the silent-drop task.