Repository navigation
fix(cloud): a busy Mac chat moves to a new Mac before its deadline and keeps working - #160
Conversation
…ot paused Fails today: upkeep releases the busy chat's Mac and the host marks it paused, so its turn dies until a client wakes it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…d keeps working Before Namespace's five-hour deadline the host saves an instance-engine chat and releases its Mac. It marked every such chat paused, so a chat still working stayed down mid-turn until a client woke it. Upkeep now releases a Mac to sleep only when its chat reads idle. A chat still working at the forced-release threshold, or any awake chat found off its Mac, comes back as "reopen": the host keeps its lease awake and held, and brings it back through the same path a client's resume uses, where the guest's restart continuation picks the turn up. Because an awake lease with its chat off a Mac always reopens, a host restart mid-move finishes the move on the next pass. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Thread transfer impact✅ Thread transfer remains within every enforced ceiling.
Baseline: Scenario and decoded snapshot size10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.
Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed. |
|
Verdict: PASS+NOTES (head Independent review. Code and evidence only, no Macs started. Correctness. The decision logic holds. A host-driven move runs entirely under the per-box Notes (none blocking):
Tests. The tests assert outcomes and lease state. The test files are identical at 032faac and HEAD. Run against the pre-fix source, 3 of them fail ( Live proof ( Mergeability. |
… chat Upkeep treated a chat on a Mac it was never restored onto as running, so a host that died during a move's restore left the chat unready until the reaper paused it. Upkeep now returns `reopen` for that record, and the reopen restores onto the Mac the move already created. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Each host-driven move costs a Mac and prompts the agent to continue, so a chat that never settled could run unattended forever. The lease now counts moves in a row since a client last heartbeated or resumed it. The count rises only when a move lands, so a failed or redone move is counted once. At MAX_UNWATCHED_MOVES the next deadline releases the chat for sleep and marks it paused; opening it continues the turn. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
Re: review. Notes 1 and 2 are fixed in 559b2b0 and 77f04e8, after merging origin/main (55470cb). 1. Unbounded cost (77f04e8). A chat now moves at most 2. Host restart mid-restore (559b2b0). Tests. Each new test failed against the pre-fix source and passes now:
|
Problem
An instance-engine Mac chat could not run a turn longer than about 4h47m. Before Namespace's five-hour deadline, upkeep saves the chat and releases its Mac. The host then marked every released chat
paused, busy or not, and nothing reopened it. A turn still running at the deadline stopped and stayed stopped until a client woke the chat.Fix
NamespaceMacRuntime.upkeepnow releases a Mac for sleep only when the chat readsidle(that callback replacesbusy, so an unreadable chat no longer counts as idle). A chat still working at the forced-release threshold returns a new outcome,reopen. So does any awake chat found off its Mac.upkeepCloudChatshandlesreopenwhile it still holds the box. It keeps the lease awake and brings the chat back on a new Mac throughwake, which is the samedriver.resume+markActivepath a client's resume uses (now shared withresume). It does not wait for a client. On the new Mac, the guest's restart continuation (continueThreadsAfterServerUpdate, on for boxes) picks the interrupted turn back up.reopen. If the host dies at any point of a move, the next upkeep pass finishes it on the Mac the move already created. A failed reopen leaves the lease awake for the next pass. An expired snapshot marks the lease missing.MAX_UNWATCHED_MOVES) with no client heartbeat or resume since the first of them. On the next deadline it is released for sleep and marked paused, as before this PR, and opening it continues the turn. The count lives on the lease (unwatchedMoves), so it survives host restarts. It rises only when a move lands, so a failed or redone move counts once. A client heartbeat or resume clears it; the reaper keeping a busy box awake does not.All changes are in
apps/server/src/environmentControl/plus two sentences indocs/operations/cloud-provisioning.md.Tests
The failing tests land first (032faac).
NamespaceMacRuntime.test.tscovers four cases:reopenwith its final snapshot saved.reopen.reopen.released.A fifth case covers a host that died mid-restore: the next upkeep returns
reopen, and the reopen restores onto the existing Mac without creating a second one.EnvironmentControl.test.tscovers the host side, including the cap: three moves land (a failed one is not counted), the fourth deadline pauses the chat, and a client resume or heartbeat starts the count again.ProvisionedLeaseRegistry.test.tschecks that the reaper's keep-awake touch keeps the count and a client touch clears it.reopenkeeps the lease active and retries after a failed resume. Once the move lands, the lease records the new proxy. An expired snapshot marks the lease missing.Live proof
The run used a local manager built from this branch with a throwaway, uncommitted patch: Mac lifetime 40 min, idle window 10 min, template builds off so no builder Mac. The forced-move threshold stayed at 8 min. The repo was
andrewcai8/t3codeon the instance engine, with Claude running a 90-tick job (one tick every 30 s, about 45 min). Each tick logs the Mac's hostname and boot time. Only one test Mac existed at a time.Timeline (UTC, from the manager log and the report):
qdt1o044i1beocreated. The turn started at 00:58.namespace mac released before its deadline(leftMs 421818,asleep: false). Final save of 11.5 MB took 4.6 s. The Mac departed at 01:30:13.l5b738cbuqb7owith no client involved. It materialized at 01:31:25 from snapshot generation 7.activethroughout (move.neverPaused). The child reconnected at the same origin 51 s after the new Mac appeared, and the thread readrunning.tick 63ran onqdt1o044i1beoat 01:30:00.tick 64ran onl5b738cbuqb7oat 01:32:06. The job reachedtick 90and printedDONE host=l5b738cbuqb7oat 01:45:16. A follow-up turn on the same chat then answered in 15 s.Every check passed (
move.progress 90/90 on 2 Macs) excepthost.skills.claudeAgent, which tests local skill config and has nothing to do with this change. Both test leases were disposed through the manager's dispose path, and no test Mac is left running.Evidence lives on the machine that ran the test, under
/tmp/mac-move-proof/:report.json,run-final.log,manager-move-excerpt.log,progress-excerpt.txt, plus the harness and theshort-deadline.patch.Risks
markPaused. Each costs one Mac until the chat goes idle near the new deadline, or until the move cap puts it to sleep.renewbefore the first upkeep pass can still pause a chat that was mid-move. A client resume then continues it.🤖 Generated with Claude Code