Skip to content

engine+cli: wheel-on-wheel push batch — WHEEL_WORKSPACE, idle-parking, agent-sender test, ctx-clear - #23

Merged
Morgandri1 merged 3 commits into
mainfrom
sdk/query-function-denylist
Sep 8, 2026
Merged

engine+cli: wheel-on-wheel push batch — WHEEL_WORKSPACE, idle-parking, agent-sender test, ctx-clear#23
Morgandri1 merged 3 commits into
mainfrom
sdk/query-function-denylist

Conversation

@Morgandri1

Copy link
Copy Markdown
Owner

The wheel-on-wheel push batch — the prerequisites that let the cloud board self-develop safely, landed as one deploy.

What's in it

  • (a) WHEEL_WORKSPACE — the engine exports each agent's provisioned worktree path (/data/projects/<p>/ws/<agent>/wheel) into the child env. Provisioning (A8/A9 worktree off the shared object store) was already live and proven in production; this only names it. Paired with the doc/ctx change telling agents to work there instead of cloning.
  • (b) idle-parking (§3c#14) — the engine reads the idle-park config and runs an Idle→Parked timer: a woken agent releases its ~162MB process after its turn (process count → 0 in the container) and keeps session_id so the next message resumes the same session. idle_timeout_secs=0 opts out (stays hot).
  • (c) agent-sender transcript test — covers the multi-agent send producer path a broadened wake runs on.
  • (d) ctx-clear one-arm fix — the dispatch-arm invariant (every_verb_in_usage_has_a_dispatch_arm), mutation-verified.
  • swallowed-kill fix (d44f8fd) — a kill that fails on park is no longer silent (all three sites).

Review (all independent, run not inspected)

  • ADVERSARY: all 5 attack vectors closed (slot-lock serialization is the spine); swallowed-kill confirmed fixed; finding 051 → FIXED.
  • QA: coverage 95.24% on changed lines (8 uncovered are defensive, none on a park/resume path); behavior 10/10 (park→0 procs, session survives park, --resume <pre-park id> read from the child's argv, resumes same session, no message loss, timeout=0 hot); (d) mutation-checked (removed the arm, watched it fail naming the verb); cargo test --workspace exit 0, 58 suites.
  • H1 resolved: an earlier 2-test failure (session_id None before park) was attributed to a test stub not emitting system/init, not the engine.

Follow-up (not in this batch)

  • BUG-040 (premature park from uncancelled stale timers) — non-blocking; QA's re-arm-for-remainder fix + stale-resume land as a reviewed fast-follow before broadening the wake.

🤖 Generated with Claude Code

https://claude.ai/code/session_01BTHr6CTMA4NNuLKd2PS4h4

Morgandri1 and others added 3 commits September 7, 2026 17:29
…idle parking, (c) agent-sender coverage, (d) ctx clear

(a) The engine already provisions a worktree from `workspaces` — proven in
production this morning, a 4.1 MB shared store and a 7.1 MB worktree. What was
missing was a way to NAME it: cwd was always right, but a script that has cd-ed
cannot recover the path and there was no variable to ask. WHEEL_WORKSPACE is
exported only when a workspace was really materialised, so it never names the
fallback directory.

(b) Idle parking, §3c#14, the reason this project exists rather than YOKE.
idle_timeout_secs had a field, an accessor, a default, a Parked status and a
test named after the behaviour — and the engine never read it. Measured before
this: an agent an hour past its turn still held a 162 MB claude process; six
would hold a gigabyte. Park is `stop` that KEEPS session_id, so the next message
resumes the same session — the half already proven when the first wake came back
on the id it went to sleep with. The timer re-checks under the slot lock, so a
message arriving as it fires either restarts the agent or finds work queued and
leaves it alone.

(c) Agent→agent was covered at the CLI plane and envelope rendering in
wheel-core, but nothing asserted the two together: every test watching bytes
reach a child's stdin used a `user` sender. That is the producer a multi-agent
board runs on.

(d) `wheel ctx clear` had an engine route since M1 and a promise in every
agent's preamble, and no dispatch arm — so it answered "unknown command" to a
command we told agents to use. Adds the arm, the usage line, and an invariant
that every advertised verb has an arm.

Each mutation-checked. The (d) invariant needed two attempts: the first matched
the verb anywhere after `match cmd`, so it passed with the arm deliberately
removed — it was finding the word in an error message.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RqRiYvAcUKDYCHb4HDP8Jg
`let _ = child.kill()` discarded the result, so a failed kill left a live
process while the board reported Parked — the saving claimed and not made, and
invisible. kill_on_drop still reaps it, so the process does die; what was
missing was anyone hearing that the direct kill did not.

SCOPE NOTE, disclosed rather than folded in quietly: ADVERSARY found this in
`park`, and the identical swallow was in `stop` and `clear_context` too. All
three are fixed here. It is log-only with no behavioural change, and fixing one
instance of a class while leaving two is how the same finding gets rediscovered
as 050. Objecting to the extra two lines is reasonable and I will split them out
if either reviewer prefers.

Found because it was risk #2 of the three I gave the reviewers — which is the
argument for handing an adversary your own list of where you think it breaks.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RqRiYvAcUKDYCHb4HDP8Jg
…never initialises

BUG-040, found by QA reading the code before running anything: a timer is armed
per turn and none is cancelled, so an OLD timer outlives the turn that armed it.
Two turns 250s apart under a 300s timeout leaves the first firing 50s after the
second, parking an agent that was active moments ago — safe, since it resumes,
but it silently shortens the hot window the operator configured.

Fixed QA's way rather than mine: park compares last_activity to now and REPORTS
the remainder instead of parking, and the timer task loops on what it reports.
No cancellation state, self-correcting however many stale timers are in flight,
and it closes the timer-accumulation M2 in the same change. Reporting rather
than re-arming inside park also keeps park non-recursive — a recursive async fn
cannot be proven Send, which is how the first attempt failed to compile.

Elapsed time comes from sqlite's julianday, the same way every other age in this
engine is measured, rather than adding a date library to a crate that has done
without one.

Second fix, same area: a start that passed --resume and never reached `init` now
clears the session. Nothing cleared it before, so every later start re-passed the
same dead id and failed identically — an agent that could never recover without
a manual clear. Parking makes this matter, because resume used to be rare and is
now the normal path back from idle. Dropping a session we might have kept costs
context once; keeping a dead one costs every start from here on.

Both mutation-checked: parking anyway fails the first, not clearing fails the
second.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RqRiYvAcUKDYCHb4HDP8Jg
@Morgandri1
Morgandri1 merged commit 9a0dfd5 into main Sep 8, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant