Skip to content

fix(macapp): make the launch container sweep safe and complete (#284) - #374

Merged
joelmoss merged 5 commits into
masterfrom
fix/284-safer-sweep
Oct 8, 2026
Merged

joelmoss merged 5 commits into
masterfrom
fix/284-safer-sweep

Conversation

@joelmoss

@joelmoss joelmoss commented Oct 8, 2026

Copy link
Copy Markdown
Owner

Fixes #284. The launch sweep of leftover containers (Docker and Apple's runtime) is now safe for live workrooms, and it no longer misses leftovers.

Behaviour changes

  • A container goes only once two sweeps in a row found it unknown, and at most 3 go per sweep. Each sweep writes what it found unknown to hosts/unknown-containers-<runtime>[@<context>].json. A config that was lost, restored or half-written therefore costs nothing on the launch that reads it.
    • A failed listing keeps the last list.
    • A failed write deletes the last list, and that sweep removes nothing.
  • A sweep also runs when config records nothing, but then it takes only what a crashed create left. Before running a container, a driver writes its name to hosts/pending-containers-<runtime>[@<context>].json.
    • The name comes off once config records the host, or once a rollback, a delete or a sweep removes it.
    • With no container host recorded, a sweep removes only pending names, still under the two-launch rule and the cap, and no images. A boxd host in config doesn't count as a container host.
    • So a crash during a project's first base create is cleaned up.
    • A lost config, or a Dev launch with a throwaway HOME, cannot make a live container look disposable. (The CLI's config follows HOME; the app's Application Support doesn't.)
    • A Mac that never made a container workroom has nothing pending, and is still never asked about Docker.
  • A runtime that is down at launch is swept on a later reload. Before, it was skipped for the whole launch.
    • The sweep is tracked per runtime and context, and counts as done only once its container listing succeeded.
    • A runtime and context is never swept twice in one launch, since the two-launch rule would count the second sweep as the next launch.
  • Hosted unit tests and UI-test launches read no pending names, so they never sweep the developer's own runtime.

The boxd case (no sweep for boxd machines) is split out to #373.

Validation

  • make app-test on RemoteHostsTests, ContainerProvisioningIntegrationTests and AppleContainerIntegrationTests: 72 passed, 0 failed, 12 skipped.
  • make app-lint is clean.
  • Mutation checks: every new or changed sweep test was run against a deliberately broken version of the code it covers and went red, then green once restored. Covered:
    • the two-launch rule
    • the cap
    • the journal write
    • the pending-only filter
    • the onlyPending decision, including the boxd case
    • forgetting recorded hosts
    • a failed write removing nothing
    • a failed listing keeping the last list
    • the down-at-launch retry
  • Pre-landing /review:
    • Specialists: testing, maintainability, simplification and red-team.
    • Two Claude adversarial passes and a Codex adversarial challenge.
    • Its two critical findings (the empty-config sweep, and the stale list on a failed write) are fixed in the last commit.

Not checked

  • The 12 skipped tests are the Docker and Apple integration tests. They need a live runtime, and they were not run against the developer's own. ContainerProvisioningIntegrationTests was updated for the extra first sweep.
  • Codex's structured review returned no severity tags, so that outside check is missing.

Known and left as is

  • A pending name for a container removed by hand stays. It costs one listing a launch. For a deleted Docker context, it costs a failing listing on each reload. A name is not dropped for being missing from a listing: a key with no context lists whichever daemon is current, and the leftover may be on another daemon.
  • A relaunch within the 20-minute grace after a crash means the leftover goes on the third launch, not the second.

🤖 Generated with Claude Code

joelmoss and others added 4 commits October 7, 2026 23:40
…nknown, 3 at most (#284)

The launch sweep removed every labelled container older than its grace
that config did not name, with `docker rm --force --volumes` and no
prompt. A config that was lost, restored or half-written, or a
descriptor that failed lenient decoding, would destroy live remote
workrooms and any unpushed work on them.

Now a sweep writes down the containers it finds unknown
(hosts/unknown-containers-<runtime>-<context>.json) and removes only
those the previous sweep, a launch earlier, found unknown too, and at
most 3 of them; the rest go on later launches, 3 at a time, and the
held-back count is logged. Images are unchanged: one a container still
runs from is never removed. Both Docker and Apple's runtime.

Still open from #284: no sweep runs while config records nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… records nothing (#284)

`RemoteHosts.adopt` swept only runtimes and contexts that config recorded a host on, so a
crash during a project's first base create, before the base was recorded, left its container
running for good.

A container driver now writes `hosts/made-containers-<runtime>[@<context>]` before running a
container, and `adopt` also sweeps every runtime and context so marked. A sweep that finds
nothing of this build's left removes the marker once it is older than the grace, so a create
under way keeps it. A Mac that never made a container workroom has no marker and is still
never asked about Docker. Test processes (hosted unit runs and UI-test launches) read no
markers, so they never sweep the developer's own runtime.

The unknown-containers file now names no context as `<runtime>` and a context as
`<runtime>@<context>`, so no context is no longer confused with one named `default`.

Still open: the sweep is claimed before it runs, so a runtime that is down at launch is
swept only on the next launch.

Refs #284

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The launch's one sweep was claimed before it ran, so a runtime that could not list its
containers at launch (Docker not started yet, say) went unswept for the whole launch.

The sweep is now tracked per runtime and context. `claimSweep` hands out only keys whose sweep
neither ran this launch nor is running, and `finishSweep` marks a key done only when its
sweep listed its containers. A failed listing leaves the key owed, so the next reload sweeps
it. A key is never swept twice in one launch, since the two-launch rule would count the second
sweep as the next launch's. A sweep reports a failed listing with
`ContainerHostDriver.listingFailed`. Held sweeps (#296) behave as before.

Fixes #284

The boxd machine sweep moves to #373: comments that pointed at #284 for it now
point there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…left (#284)

Pre-landing review fixes.

The made-containers marker let a launch whose config records nothing sweep that runtime with
every container unknown. A lost config does that, and so does a Dev launch with a throwaway
HOME: the CLI's config follows HOME, but the app's Application Support (and so hosts/) does
not. From the second such launch, up to 3 live workroom containers would go per launch.

The marker is replaced by a pending-create journal,
hosts/pending-containers-<runtime>[@<context>].json. A driver writes each container's name
before running it, and the name comes off when config records the host (RemoteHosts.adopt),
or when a rollback, a destroy or a sweep removes it. A launch sweeps every runtime and context
with a name pending. With no container host recorded (a boxd host says nothing of the
containers), it removes only pending names, still under the two-launch rule and the cap, and
no images. A name is not dropped for missing from a listing: a key with no context lists
whichever daemon is current, and the leftover may be on another.

A sweep that cannot write its unknown list now deletes the old one and removes nothing.
Before, the list left behind was an older sweep's, and could approve removing a container
that was known in between.

Tests: the journal around a failed create, a pending-only sweep, adopt with nothing recorded,
a recorded host or only a boxd host, a failed write, and a failed listing keeping the last
unknown list. The down-at-launch retry test now waits on the running-sweep count rather than
a fixed sleep.

Refs #284

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 8, 2026 08:15
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 8, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-08T08:19:47.672224Z 2df5664 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2df5664f69

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Failed journal invalidation can permit unsafe container deletion, and the Docker integration test expects an incorrect result.

2 open findings
What changed in this PR

Makes launch-time container cleanup safer and recovers leftovers from interrupted creation, addressing #284 while leaving boxd cleanup to #373.

Changes:

  • Adds two-sweep confirmation, removal caps, and pending-container journals.
  • Retries unavailable runtimes on later reloads without repeating successful sweeps.
  • Updates regression tests and design documentation.
File Description
macapp/​WorkroomAppTests/​RemoteHostsTests.swift Tests sweep safety, pending journals, and retries.
macapp/​WorkroomAppTests/​ContainerProvisioningIntegrationTests.swift Adds first-sweep retention checks.
macapp/​WorkroomApp/​Core/​RemoteWorkrooms.swift Coordinates per-runtime/context sweeps and pending recovery.
macapp/​WorkroomApp/​Core/​ContainerHostDriver.swift Implements journals, confirmation, and removal caps.
docs/​designs/​remote-workrooms.md Documents cleanup behavior and deferred boxd work.

🧠 Review effort: Balanced


Give feedback about Copilot approvals in this survey to enter a drawing for a $150 gift card.


// The first sweep to find the orphans unknown only writes them down (#284).
let firstSweep = await driver.sweep(keeping: [keptID, instanceID], grace: 0)
XCTAssertEqual(firstSweep, [])

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Left by Claude Code.

Fixed in 2d79531. Confirmed against the code: sweep runs rmi without --force on an old, unrecorded image. On the first sweep the orphaned instance's container survives, so its image is still in use and Docker refuses it. The test now expects only image … failures from that sweep, and keeps the check that all four containers survive. It compiles. It was not run here, because it needs a live Docker fixture.

Copilot: the live-Docker sweep test's first sweep keeps the orphaned instance's container, so
its image is still in use and Docker refuses the `rmi`. Expect only image failures there,
instead of none.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@joelmoss
joelmoss merged commit b772b34 into master Oct 8, 2026
18 checks passed
@joelmoss
joelmoss deleted the fix/284-safer-sweep branch October 8, 2026 09:07
@joelmoss joelmoss mentioned this pull request Oct 9, 2026
20 of 21 tasks
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.

Make the remote host sweep safer and make it run when nothing is recorded

2 participants