Skip to content

feat: wire boxd into the app (#356) - #372

Merged
joelmoss merged 39 commits into
masterfrom
feat/boxd-in-app
Oct 7, 2026
Merged

joelmoss merged 39 commits into
masterfrom
feat/boxd-in-app

Conversation

@joelmoss

@joelmoss joelmoss commented Oct 7, 2026

Copy link
Copy Markdown
Owner

Fixes #356

Summary

You can now create, open and delete a remote workroom on boxd from the app, not only on a Docker or Apple container on this Mac. This is behind the remote-workrooms preview (Nightly and Dev builds).

  • New Workroom › Remote › boxd. The place appears in the context menu, the row's + and the File ▸ New Workroom… picker. When it can't be used it is disabled and says why: the CLI isn't installed, or Codaset isn't signed in. A boxd sign-in problem is reported at create and names boxd auth login. A create shows its steps (machine, setup, clone, enrol, checkout) in the sidebar's progress ring.
  • One driver per key. RemoteHosts holds drivers by DriverKey, which is either a container runtime and its context, or a boxd org plus account. The account is checked as well as the org, because every personal account's org is nil. Records carry driver: "boxd" with org and account. A relaunch reaches a boxd workroom by name, with no Docker needed. Delete goes through the boxd driver, and an unknown driver is refused rather than handed to Docker.
  • A background read never wakes a sleeping box. Before connecting, a status read asks boxd machine get. A box boxd calls asleep, or one it can't vouch for, is left alone. That covers a missing status, a CLI error and a timeout, and it is logged. Opening the workroom, selecting it or clicking it still connects.
  • An idle box is let go of so boxd can sleep it. When the agent reports IDLE for a connected boxd host that isn't selected and has no port forward or attached pane, the app drops its connection. Each pane holds only its own attach, and a pane attaching to a let-go box takes it back. A stalled or failing agent holds the box for one ceiling at most, never forever. A connected boxd host is polled even with nothing on screen.
  • Anything a failure leaves running is recorded. A derive or base create that can't remove its machine, or a base whose rollback fails, is recorded as failed, so the project shows it and delete can finish it. A record that also fails is logged with the machine named. destroy also removes a derive's snapshot, best-effort and logged.
  • Wakefulness agent (wr-agent): a resume after an unanswered prompt keeps the time the box was already awake, so the user is asked again at once rather than given a fresh ceiling. IDLE ticks inside the resume mask are ignored. Base connects send no ceiling settings (watched: false).
  • Sidebar and badge: asleep and idle states for remote rows. A warning when boxd's idle window is under 90 s, because the box could sleep under a running job. Host prompt cards are ordered by deadline and scroll past two.

Test Coverage

CODE PATHS (28)
[+] AgentWakefulness.swift
  ├── [★★★ TESTED] display(idleWindow)/isTooShort/help + Display.unknown
  ├── [★★  TESTED] apply(): report busy/idle to RemoteHosts.observed
  ├── [★★★ TESTED] stalled/stopped/failed-read hold for one ceiling; refusal classification; ceiling sanitising
  ├── [GAP] prefs-ceiling fallback + monotonic clock (intentionally uncovered)
  └── [★★★ TESTED] connection poll lifecycle
[+] RemoteWorkrooms.swift (RemoteHosts)
  ├── [★★★ TESTED] connect(): boxd idle window, connectedAt, poll
  ├── [★★★ TESTED] ensureConnected(): presence gate, revisit window, opened/wake/selected bypass
  ├── [GAP] unanswered presence caching (deferred #371)
  ├── [★★★ TESTED] observed(): grace, selected, container, port forward, attached pane
  ├── [GAP] end-of-let-go pane re-check; click racing in-flight disconnect; never-let-go box not taken back (intentionally uncovered)
  ├── [GAP] late BUSY/selection retracts a let-go in flight (intentionally uncovered, needs a seam)
  ├── [★★★ TESTED] select()/AppStore selection didSet/deinit, two windows
  ├── [GAP] AppStore restored-selection select (deferred #371)
  └── [★★★ TESTED] DriverKey/deletionKey/clonePath/base(in:for:)/environment/org+account
[+] PersistentSessionService + GhosttySurfaceView
  ├── [★★  TESTED] attach tracking, take-back on attach, pane exit/swap/teardown
  ├── [★   TESTED] applyPersistentSession passes attachToken (source-text match only)
  ├── [GAP] surface fails to start; pane freed with its window; attach cleared on session end (intentionally uncovered)
  └── [★★  TESTED] ensureConnected(wake:) call sites (Keep awake, close pane)
[+] BoxdHostDriver.swift
  ├── [★★★ TESTED] presence() (incl. status-less reply) + idleWindow()/Timers/Seconds
  ├── [★★★ TESTED] account/signedIn/checkOrg + destroy snapshot removal (best-effort)
  ├── [GAP] cancelled snapshot removal (deferred #371)
  └── [GAP] boxd derive steps; Codaset guard; watched:false wiring (intentionally uncovered)
[+] create / provisioning / AppStore
  ├── [★★★ TESTED] leftBehind recording (derive, base create, base rollback, record failure)
  ├── [★★★ TESTED] step reporting (driver, sequence, createStep)
  ├── [★★★ TESTED] AgentBootstrap.connect(watched:)
  └── [GAP] key(for: .boxd) and createRemoteWorkroom glue (intentionally uncovered, needs a seam)
[+] Views
  ├── [★★★ TESTED] resolver asleep/idle/gone to dot and aggregate weight
  ├── [★★  TESTED] ToastStack.ordered (incl. ties)
  └── [★★  TESTED] boxd in New Workroom: context menu, + menu, busy state, picker (NewWorkroomPlacesUITests); step ring view not tested
[+] wr-agent wakefulness.rs
  └── [★★★ TESTED] Ceiling.resumed(t) carry, step_unless_masked, ceiling_step, wake_masked

Opt-in, not counted: BoxdIntegrationTests (9 live cases on real boxd machines).

Coverage: 71% value-weighted (20/28 paths with a ★★ or ★★★ test), 75% including 1 weakly covered path.
Coverage gate: 71%. 7 paths are intentionally untested: each needs a test-only hook in the app, or is deferred to #371.
Test value: 0 tests written, 0 rejected by the authoring gate, 1 existing test extended (NewWorkroomPlacesUITests, boxd rows; renaming the boxd entry turned 2 of its 3 tests red, and the picker ID 1), 1 path weakly covered.

Pre-Landing Review

Three review passes ran in this /ship run, each with 5 specialists plus Red Team, and two earlier /ship runs did the same. The last pass made no edits.

  • Fixed in this run:
    • The pane-detach code now goes through one helper.
    • The create and RemoteHosts docs no longer say leftovers get swept, and adopt() no longer shadows keys.
    • Prompt-card ordering with tied deadlines is tested.
    • Two stale wakefulness comments are corrected.
  • Fixed in earlier runs:
    • A boxd reply with no status now counts as "can't say" instead of awake (7555c9d).
    • A snapshot that won't delete no longer fails destroy.
    • A negative or non-finite ceiling is dropped.
    • A base whose rollback fails is recorded.
    • A huge wire ceiling no longer crashes on Duration.
  • Follow-ups in boxd in the app: follow-ups from #356 (PR #372) #371: presence caching and single-flight, a cancelled snapshot removal, the AppStore restored-selection select, a let-go disconnect that can cut an in-flight request, a boxd record with no account at delete, out-of-order create steps, idle-timer string format, last-known dirty state while asleep, and stale doc comments.
  • Advisory simplifications were declined. They are: fold step_unless_masked; merge remaining and describing; keep the idle window in one place; ViewThatFits for prompt cards; merge the leftBehind and rollbackIncomplete catches.
  • Security, Debug builds only: the broker's reverse forward now also reaches a boxd VM, so processes there can reach the development Codaset's Puma. This is the exposure you accepted on 2026-10-01 (BrokerReverseForwards.swift) and is noted here for you. Release and Nightly are unaffected.
  • Coverage gaps:
    • The final pass's Red Team was stopped at your request, so that pass is incomplete.
    • The adversarial review this run was the native Claude pass only; the Codex passes were skipped at your request. In the previous run, the Codex structured review passed (no P0 or P1) and found the same P2 that 7555c9d fixed. The Codex adversarial challenge timed out in that run, so it gave no coverage.

Exploratory QA

Functional smoke probes ran after each fix round:

  • RemoteHostsTests, AgentWakefulnessTests, BoxdHostDriverTests and RemoteWorkroomRoutingTests, plus RemotePaneReconnectTests in round 2, captured with gstack-qa-evidence: 178/0, 182/0 and 178/0.
  • Live on real boxd (cbf4069):
    • testADeriveThatFailsAtEachStepLeavesNoMachineOrSnapshot passed (90 s).
    • testARemoteWorkroomIsMadeAndTakenDownOnBoxdAsTheAppDoesIt passed (38 s).
    • boxd machine list and snapshots list were empty afterwards.

Design Review

The boxd place reuses the existing New Workroom places UI. No new design review was run.

Eval Results

No prompt-related files changed, so evals were skipped.

Scope Drift

Scope check: drift was found, all of it approved during review. The plan's scope was create, open and delete on boxd. The branch also adds:

Plan Completion

The plan had 26 items: 20 done, 6 done another way. All were addressed.

  • Place enum with boxd; Remote section in the sidebar and the New Workroom picker (Nightly/Dev + preview gate)
  • Create via BoxdHostDriver: key(for: .boxd), driver and environment, create(key:driver:), HostDescriptor driver=boxd with org and account
  • [~] Changed: unavailability reasons. The menu shows "not installed" and "sign in to Codaset"; boxd sign-in is checked at create.
  • [~] Changed: the agent socket and clone path come from agentSocket(of: driver) and clonePath(for:on:).
  • [~] Changed: DriverKey is .boxd(org:account:).
  • Git on boxd goes through the Codaset broker only
  • Relaunch, status probe, delete and the no-sweep decision (Make the remote host sweep safer and make it run when nothing is recorded #284) on boxd
  • [~] Changed: a resume after an unanswered prompt keeps the time already awake and ignores masked IDLE ticks.
  • [~] Changed: base connects send no ceiling settings (watched: false).
  • A warning when the boxd idle window is under 90 s
  • [~] Changed: the TODOS items for this work were removed because it shipped, rather than repointed.
  • Unit tests with stub drivers, plus 9 gated live cases

Verification Results

Plan checks: 4 PASS, 0 FAIL, 0 BLOCKED, 0 NOT RUN.

  • Full suites: app 3267/0, wr-agent pass (31031eb).
  • Docker and Apple container tests pass unchanged (inside the full suite).
  • Live boxd: 2 affected cases rerun on cbf4069 (D2) and passed. The heartbeat and unanswered-prompt cases last passed on 1497e61.
  • Break-the-guard check on the resumed() carry: passed in an earlier run; wakefulness.rs is unchanged since.

Known and accepted:

TODOS

No TODO items completed in this PR. The branch removed the two it finished, the wake-on-connect P1 and the keep-awake acceptance P2. It adds a P0: AgentWakefulnessTests.testAReplyCrossingAKeepDoesNotResurrectTheCard flakes. That test came from #216 on master, and it failed once in a targeted QA run.

Documentation

Status: updated. One factual edit, no blockers.

Scope: base 8bb1d33 to HEAD 31031eb (33 commits, 34 files) plus uncommitted TODOS.md; branch feat/boxd-in-app (#356, boxd in the app). Steps 1, 1.5, 2-4 and 6 only. Docs discovered per the candidate list (README, AGENTS.md files, CONTRIBUTING, DESIGN, PRODUCT, docs/agents, docs/designs, docs/repository-notes.md) and the nested macapp/AGENTS.md.

Documentation health:

Coverage debt: common gaps, not filled. New Workroom > Remote > boxd and boxd idle let-go/sleep behaviour have reference and explanation in the design doc and macapp/AGENTS.md but no how-to or tutorial. No critical gaps. Diagram drift: none found. Suggest /document-generate if a boxd how-to is wanted.

Pre-existing debt: macapp/AGENTS.md 'Remote workrooms in config (#249)' still says the app's remote deletion is refused until #253; #253 shipped as #289 and #356 deletes boxd workrooms in the app. Left for the author because it is a narrative rewrite.

Test plan

  • make app-test: 3267 passed, 0 failed (31031eb; the later commits change only TODOS.md and macapp/AGENTS.md)
  • cargo test -p wr-agent (Rust 1.96.1): pass
  • make app-uitest APP_UITEST_FLAGS=-only-testing:WorkroomAppUITests/NewWorkroomPlacesUITests: 3/3
  • Live boxd (TEST_RUNNER_WR_BOXD_TESTS=1 WR_AGENT_LINUX=1, sandbox off): 2/2 affected cases, nothing left on boxd
  • make app-format && make app-lint: clean

🤖 Generated with Claude Code

joelmoss and others added 30 commits October 6, 2026 20:45
A resume cleared the awake ceiling outright. With the #257 heartbeat that
turned "ask, then let it sleep" into "awake in 4 h chunks": a connect
wakes a sleeping box, the ceiling started over, and a job still running
held the box for another whole ceiling.

Now a resume from Suppressed carries the awake time from before the
sleep, so the next BUSY tick is still past the ceiling and raises a fresh
prompt. A user who woke the box gets the card; with nobody there, the box
sleeps again once that prompt times out. Any other state still starts
over on a resume.

The resume mask reads a CPU-only job IDLE for its first few ticks, and an
IDLE step ends the ceiling, so the service now ignores IDLE from a masked
tick (Classifier::wake_masked, Ceiling::step_unless_masked). Without that
the carried time was lost before the job was seen again; the new sequence
test checks both halves.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
RemoteHosts only ever made ContainerHostDriver, keyed by container
runtime, so a boxd host had no key at all: a delete fell back to the
Docker driver (`DriverKey(host) ?? DriverKey()`), adopt dropped boxd
hosts, and a missing container CLI read as a boxd host being gone.

DriverKey is now an enum over a container runtime (with its Docker
context) and a boxd org and account, and RemoteHosts holds any
HostTerminalDriver. A boxd driver is made by name and never adopted or
swept (machine names carry no build; see #284). The agent socket comes
from the key, and a remote host never borrows the Mac's gh (OQ20). A
descriptor naming a driver this build doesn't know is refused, never
taken down with Docker.

RemoteWorkrooms.create takes the key instead of a runtime and records
`driver: "boxd"` with the org and the boxd account (`user_id`). The boxd
driver now refuses to act on another account too: every personal
account's org is nil, so after an account switch the org check passed
and a machine read as "not found" was taken for gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The sidebar's New Workroom menu gains a Remote section with boxd, and
the picker a boxd row. The entry says why it can't be used from what is
cheap to read in a menu body: the boxd CLI (~/.local/bin, Homebrew) and a
Codaset sign-in. A boxd workroom takes the broker's repo-scoped tokens
only (OQ20), so `gh auth login` is no way in there.

Whether boxd is signed in is a CLI call, so the create makes it:
RemoteHosts.key(for: .boxd) asks `boxd auth --json` for the active org
and account, which the new base is made in, and a signed-out CLI fails
naming `boxd auth login`. A create with no Codaset session stops before
anything is made.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
RemoteWorkrooms.clonePath always named /home/workroom, the container
user's home. A boxd machine logs in as boxd, so the path now follows the
driver key.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A boxd box wakes on an ssh login, and the status sweep connects to every
remote workroom, so every sweep would have woken every box and kept it
billing. A background read of a boxd host now asks boxd first
(`machine get --json`, status `standby` or `hibernated`) and, when it is
asleep, fails with RepositoryRoutingError.asleep instead of connecting.
boxd is asked at most once per host per retryAfter window.

The row shows a moon ("asleep") rather than the unknown dot, and Changes
says to open the workroom to wake it. Opening the workroom (`activate`)
connects as before, which wakes the box. When boxd can't say (no CLI,
signed out, offline), the read connects as it always did.

The status field is from boxd's CLI docs; the gated live run checks it
on a real machine.

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

Every connect handed the host's agent this Mac's ceiling settings, with
ask on wherever the box can sleep, including provisioning's base and
derive connects, whose prompts nothing watches: a long clone past the
ceiling would ask, nobody would answer, and the base would sleep under
it (TODOS, "A base gets ask mode with nobody watching").

Sending `ask: false` from those connects instead would race: the
settings are the whole agent's, so it could land after a watched
connection's `ask: true` (Debug provisioning connects one) and switch
prompting off under it. So an unwatched connect now sends none, and a
base keeps the agent's default, which never asks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…an hold it (#356)

The agent keeps a busy box awake with a heartbeat once a minute. A box
whose boxd auto-suspend or auto-hibernate timer is shorter than about 90
seconds can be slept between two sends, under a running job, with
nothing reporting it (TODOS, "An idle window under 60 s defeats the
heartbeat silently").

On every connect to a boxd host the app now reads the machine's timers
(`machine get --json`, the shorter of the two that are set) into the
host's wakefulness model. A busy box with a window under 90 s shows as
"busy but not kept awake", and the badge's help names the window and
says to raise the auto-suspend timeout; an idle box carries the same
note. The timers' encoding isn't documented, so numbers and unit
strings (`120s`, `2m`, `1h`) are read and anything else never warns.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Several hosts asking at once stacked their cards with no limit, so one
could be pushed off the window while its countdown ran, and a prompt
nobody answers lets that box sleep. The cards now go soonest deadline
first, and past two they scroll in a bounded area.

A stalled wakefulness service whose last reading was IDLE showed "idle"
on a host that sleeps; that reading may be stale while work runs, and
nothing is keeping the box awake either way. It now shows "status
unknown" with a warning. A host that never sleeps still shows idle.

The scroll area itself is a view change with no UI test; the ordering
and the display rule are unit-tested.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A container create shows its image download on the project row; a boxd
create (a new machine, its setup and the clone, then a snapshot, a
restore, a reboot, enrolment and checkout) showed a bare spinner for
20 s or more, which reads as a hang and invites a retry of a create that
was working.

The boxd driver and the provisioning sequence now report each step
through a task-local (`RemoteProvisioning.$reportStep`, the pattern
`ContainerHostDriver.$pullProgress` set), and a boxd create turns it
into a progress ring with the step's name: "Copying the base machine
(step 1 of 5)". Container creates report no steps.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Four BoxdIntegrationTests cases, gated like the others
(TEST_RUNNER_WR_BOXD_TESTS=1, WR_AGENT_LINUX=1, sandbox off):

- a remote workroom made and taken down through RemoteWorkrooms.create
  and delete on a boxd key: base in /home/boxd, records naming boxd with
  the org and account, nothing left after;
- #257's keep-awake acceptance, rerunnable (TODOS): a control box with
  no agent sleeps mid-job, the agent's box logs every tick and sleeps
  after the job;
- eng review D8: with the app attached (a watched connection, ssh
  keepalives, a badge-style status poll every 10 s), an idle box still
  sleeps, and a busy box whose prompt nobody answers still sleeps.

They make paid machines and take 5-10 minutes each, so they run only
when asked for.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds the design doc's "As built (#356)" entry: the driver-key enum,
the account check, broker-only credentials, leaving asleep boxes alone,
the ceiling across a wake, no boxd sweep, and the gated live runs that
decide whether the entry can ship as is.

Removes the two TODOS.md entries this branch completes (the P1 "Before
wiring boxd into the app" items and the keep-awake acceptance case) and
repoints the comments that cited them. The live sleep tests get more
time: a fresh boxd machine's start-up traffic holds its timer for about
290 s (#257's acceptance).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The live runs showed a boxd box never sleeps while the app is attached:
boxd counts inbound traffic as activity, and the service connection's
ssh keepalives (every 15 s) and the badge's status polls (every 10 s)
are inbound. Every open boxd workroom stayed awake and billing, and an
unanswered prompt could not let its box sleep (eng review D8).

When a connected boxd host's agent reports IDLE (its published verdict,
which an unanswered prompt also makes IDLE), and its workroom is not the
selected one and forwards no port, RemoteHosts now closes its connection
and marks it let go of. A background read never reconnects it (it reads
as asleep), and the badge, finding no connection, sends nothing.
Selecting or opening the workroom takes it back. A remote pane's own
ssh still keeps its box awake, which is the user working there.

The two app-attached live tests now attach through RemoteHosts, with the
badge's poll reporting to it, as the app does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…dle boxes (#356)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From /ship's pre-landing review (specialists, red team, core pass):

- A click wakes what a background read leaves alone: closing a pane and
  Keep awake call ensureConnected(_:wake:), so a boxd host let go of as
  idle, or asleep, is connected rather than refused. Before, closing a
  pane of such a workroom left its session running on the box.
- Selection is tracked per window (one AppStore per window), including a
  selection restored at launch; a closed window drops its entry. Before,
  one window's selection could un-protect another window's workroom.
- observed() re-checks the selection under the lock after its await, so
  a workroom selected meanwhile is never let go of.
- isAsleep treats a CLI that answered with an error (signed out, another
  account, not found) as asleep for background reads, so they leave the
  box alone; only a CLI that couldn't answer lets the read go ahead.
- Status reads time out after 10 s, not the CLI's 120 s.
- One agent-socket lookup and one place-name mapping; the asleep message
  is shared; a misplaced doc comment moved.

Tests: the click wake and per-window selection (both seen failing with
the fix removed), the signed-out message, help text combinations, an
anchored "unwatched connect sends nothing" check, and the coverage
audit's additions (let-go reporting, selection from AppStore, asleep
mapping, idle window on connect, step reports, port-forward exception).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A click (wake) is tried whatever the last attempt did: it no longer
  waits out the 30 s failure window, which a box failing once while it
  resumes would hit, and it clears that failure.
- observed() checks once more after its last await that the host is
  still let go of and unselected before dropping the connection, so a
  selection or click across that wait keeps it connected.
- A machine boxd answers "not found" for reads as gone (deleted, or in an
  org or account that isn't active), not asleep: background reads say so
  and leave it for the failure window. Other CLI errors still leave the
  box alone.
- One display(hostSleeps:idleWindow:), the idle-window sentence appended
  in one place, and the picker's remote places in one branch; a comment
  updated for the wake path; errorLine read once.

Tests: a second click inside the failure window, a gone machine, two
real AppStore windows and a closed window letting its box go, and the
unwatched-connect check now settles past the watched request. Each was
seen failing with its fix removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Red Team pass 3 of the pre-landing review found that a boxd host was let
go of only while a badge was polling it, and never looked at again once
it was:

- A connected boxd host is now polled for as long as it is connected
  (WakefulnessModel.pollWhileConnected), so an IDLE reading lets it go
  when its row is collapsed or scrolled away. The poll stops when the
  host is let go of or forgotten.
- A host let go of is asked about again once its idle window has passed.
  If boxd says it is still awake, work holds it, so it is reconnected and
  its ceiling prompts are heard.
- A stalled service's IDLE no longer lets a box go: it may be stale while
  work runs.
- A selected workroom's box is reached by background reads even when
  asleep or let go of, so it doesn't read as asleep after the Mac wakes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Letting go of idle boxes:
- A host with a pane open is never let go of: its pane's ssh holds the
  box awake anyway, so letting go only blanked its status.
- A host whose agent's wakefulness service has stopped is let go of, as
  nothing on it holds the box awake.
- IDLE readings within 10 s of a connect are ignored, so the read that
  connected the host is answered before its connection closes.
- A box let go of reads "idle, not connected" (RepositoryRoutingError.idle,
  VCSStatusFailure.idle), not "asleep", until boxd confirms it asleep.
- The let-go set and its timestamps are one map.

Fixes:
- An idle timer boxd reports past a week, or not finite, reads as unknown:
  as a Duration it trapped the revisit arithmetic.
- A failed idle-window read keeps the window last read.
- A new boxd host needs boxd auth's user_id, so a delete always checks the
  account.

Cleanups: DriverKey.isBoxd replaces runtime == nil, the boxd home paths
come from BoxdHostDriver.user, WorkroomPlace wraps RemoteWorkrooms.Place,
an unreachable asleep case goes, and the wr-agent service and its test
share one ceiling_step.

Tests cover each change (each mutation-checked), the 90 s boundary, the
stalled reading with a positive control, and the coverage audit's gaps.
The design doc records the selected workroom's ceiling limit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Red Team, review pass 2:
- A pane registers its session when it is made, but a restored one
  spawns nothing until it is shown, so counting registrations kept every
  relaunched boxd workroom with saved panes connected, and billed. Only a
  pane that asked for its attach command now counts
  (PersistentSessionService.hasAttachedPane), and closing it stops
  counting even when the host's kill fails.
- A service that stopped while its last reading was BUSY keeps its box:
  the connection is then all that keeps the job's box awake.
- The asleep message names the boxd auth case, which also reads asleep.
- The connect stamps its time before its poll starts, so the first
  reading always sees the grace.

Also: the let-go state is letGoAt/isLetGo, the grace is pinned to the
poll interval, a failed idle-window read is tested, the timer cap is
pinned at a week, the boxd agent socket default is inline, two stale
comments are fixed, and the design doc's limit says what holds a box.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Red Team, review pass 3:
- BoxdHostDriver.idleWindow throws when boxd can't say, so a connect
  keeps the last window only on a failure; an answer with no timer set
  now clears it, and the badge stops warning about a timer turned off.
- A stopped service's last BUSY holds its box for one ceiling at most:
  that reading never changes and nothing will ask about it.

Also: the observed() and asleep docs say what they now check,
BoxdHostDriver.stateDirectory builds the boxd state paths, the tests
share one boxd workroom helper, wait for the idle-window read rather
than sleeping, settle before their negative assertions, and cover a
forgotten or unreachable pane holding nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* origin/master:
  fix: address PR review findings
  docs(remote-workrooms): record the busy state surviving a reattach (#359)
  fix(wr-agent): replay the busy state to a reattaching client (#359)
A closed window or a dead link no longer keeps an idle box connected.
Failed and stalled readings hold a box for one ceiling at most, as a
stopped one already did. Also: the let-go grace is the poll interval,
a boxd driver made without its CLI is not cached, a presence read no
longer needs `source`, and tests for the click call sites, the hold
across polls and a keep on the resume tick.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A pane pointed at another session lets the old one's host go, and a
pane attaching to a host the app let go of takes it back, so its
prompts are heard. The untrusted hold counts only readings that reached
the agent, restarts after a trusted one, and falls back to the ceiling
in preferences.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A request the app never sent, like one refused for a let-go host, no
longer starts the untrusted hold, and a connect starts a new count. A
take-back runs only while its pane still holds the box, and a click
during a let-go's disconnect still takes the host back. A surface that
fails to start, or one freed with its window, marks its pane detached
at once.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Attaches are kept per pane, by a token the pane owns, so a pane freed
late cannot clear a live pane's mark for the same session, and a freed
pane's detach hops to the main actor safely instead of trapping off it.
Letting a box go now checks its mark last, after every wait, so a click
or a pane in between takes it back and keeps its connection. Tests pin
that failed reads which reached the agent start the hold's clock.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A let-go taken back resumes the host's poll without restarting the
untrusted hold's count, which only a connect does. The surface's attach
token is pinned by the wiring test, and comments and test notes match
the per-pane attaches.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…boxd cannot vouch for (#356)

A derive or base create whose rollback leaves its machine running now
keeps the entry, failed, naming the machine, so a delete can take it
down instead of a paid machine running on with nothing to find it.
leftBehind carries the host for that.

A background read of a boxd box that boxd cannot say about (no CLI, a
timeout, unexpected output, a cancelled read) now leaves it be and logs
why, rather than connecting and waking it. Letting a box go rechecks
port forwards and gives way to a newer BUSY reading at its last check,
and the untrusted hold runs on a monotonic clock.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…356)

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

A base build that fails after its machine exists, and whose rollback
cannot destroy it, now records the machine on the project as the
driver's own undo does. A record that fails is logged with the machine
named. A boxd delete also removes the derive snapshot a failed derive
can leave. The untrusted hold sanitises the wire ceiling before it
reaches Duration, which trapped on an out-of-range value.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d every left-behind record logs (#356)

- BoxdHostDriver.destroy removes the snapshot best-effort: the machine is
  gone, so a stuck snapshot is logged rather than thrown.
- Every record of something left live goes through recordLeftBehind, so a
  record that fails too names the machine and grant in the log.
- A negative or non-finite ceiling on the wire is dropped; zero still counts.
- Tests: snapshot removal names the machine, a stuck snapshot still destroys,
  a negative ceiling holds, and a failed base record keeps the original error.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…356)

presence() took a missing `status` as awake, so a background read would
connect and its ssh login would wake a box boxd had not vouched for. A
reply without a status is now "can't say" (nil), which RemoteHosts takes
as asleep and logs, as for any other answer it did not expect.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
joelmoss and others added 5 commits October 7, 2026 21:43
…#356)

The places loops listed only Docker and Apple Container, so a change that
dropped boxd from New Workroom, or enabled it while its title gave a
reason, passed every test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- GhosttySurfaceView: the three pane-detach sites share one helper, so a
  new way out of a pane can't miss the call that frees its host.
- RemoteWorkrooms: create() and RemoteHosts docs say a boxd machine is
  never swept (#284); adopt()'s local no longer shadows `keys`.
- Tests: prompt cards with no deadline or a tied one keep one order.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…here a failed read clears (#356)

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

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

chatgpt-codex-connector Bot commented Oct 7, 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-07T22:23:48.706852Z 838fbb1 New commits
ℹ️ 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: 1392ee5524

ℹ️ 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

Cancellation can be swallowed during boxd cleanup, and the idle-disconnect path still has an unresolved take-back race.

5 open findings
What changed in this PR

Adds preview-gated boxd workroom creation, connection, lifecycle management, and deletion to the macOS app.

Changes:

  • Introduces boxd-aware driver routing, identity, provisioning, and cleanup.
  • Adds sleep-safe connection handling, wakefulness updates, and pane/forward holds.
  • Extends UI status, progress, documentation, and automated coverage.
File Description
TODOS.md Records a wakefulness test flake.
docs/​designs/​remote-workrooms.md Documents the boxd architecture.
macapp/​AGENTS.md Updates app engineering guidance.
Core/​AgentPortForwarding.swift Exposes active-forward state.
Core/​AgentWakefulness.swift Adds idle-window and connection polling logic.
Core/​AppStore.swift Wires boxd creation and progress.
Core/​BoxdHostDriver.swift Adds account, presence, timers, and cleanup support.
Core/​ContainerHostDriver.swift Adapts leftover error handling.
Core/​GhosttySurfaceView.swift Tracks pane attachment lifetimes.
Core/​HostDriver.swift Extends leftover errors with host identity.
Core/​Models.swift Persists boxd org and account.
Core/​RemoteProvisioning.swift Reports provisioning steps.
Core/​RemoteWorkrooms.swift Implements boxd routing and idle release.
Core/​RepositoryLocation.swift Adds asleep, idle, and gone errors.
Core/​Session/​AgentBootstrap.swift Supports unwatched connections.
Core/​Session/​PersistentSessionService.swift Tracks attached remote panes.
Core/​WorkroomStatus.swift Presents asleep and idle states.
Core/​WorkroomStatusResolver.swift Maps new routing states.
Views/​ChangesPanel.swift Shows wakefulness warnings and unknown state.
Views/​NewWorkroomDialog.swift Adds boxd as a destination.
Views/​ProjectSidebar.swift Adds boxd actions and progress.
Views/​ToastStack.swift Orders and scrolls prompt cards.
AgentWakefulnessTests.swift Covers polling and idle-release behavior.
BoxdHostDriverTests.swift Covers boxd identity, presence, and cleanup.
BoxdIntegrationTests.swift Adds live boxd lifecycle scenarios.
ContainerProvisioningIntegrationTests.swift Updates error matching.
GhosttySurfaceViewTests.swift Tests pane attachment lifetime.
RemoteHostsTests.swift Covers routing, presence, and release rules.
RemoteProvisioningTests.swift Tests provisioning progress.
RemoteWorkroomRoutingTests.swift Tests status and user-action routing.
Session/​AgentBootstrapTests.swift Tests unwatched connection settings.
Session/​PersistentSessionAttachGateTests.swift Verifies pane token wiring.
NewWorkroomPlacesUITests.swift Verifies boxd UI availability.
wakefulness.rs Preserves ceiling state across resumes.
wakefulness/​tests.rs Tests resumed ceiling behavior.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

joelmoss and others added 2 commits October 7, 2026 22:46
…ac TODO (#348)

#348 (a second Mac finds a remote workroom and reaches its host) is closed
as not needed, so the Phase 4 gate (#260) no longer asks for the
different-Mac test, and its P1 TODO goes with it. Open question 8 stays
built (#255) and proven on the ssh fixture.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Codex:
- A let-go's disconnect now asks, on the connection manager, whether the
  host was taken back meanwhile (`disconnect(_:unless:)`), so a click or
  selection that lands across the hop keeps the connection it found up.

Copilot:
- A cancelled delete no longer reads as done when the cancel lands during
  the snapshot removal: cancellation is rethrown before the best-effort catch.
- A presence read boxd couldn't answer is remembered for `retryAfter`, as
  asleep is, so an outage asks boxd once a window, not on every read.
- The failed-record log names "the workroom as destroyed" when nothing is
  left live, instead of an empty list.
- Test comments: the `eventually` doc is back on `eventually`, and a timed-out
  presence read is documented as nil taken as asleep, not "the read goes ahead".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@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: fd955b02ed

ℹ️ 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".

Comment thread macapp/WorkroomApp/Core/AppStore.swift Resolved
…l back (#356)

Codex: connect's idle-window read could land after a delete had forgotten
the host, and its `model(forHost:)` lookup made the model anew, watching
for prompts for good. The read now updates the model the connect made, and
only while it is still the host's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@joelmoss joelmoss mentioned this pull request Oct 7, 2026
12 of 19 tasks
…age before fixing

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@joelmoss
joelmoss merged commit 1a63fd5 into master Oct 7, 2026
14 checks passed
@joelmoss
joelmoss deleted the feat/boxd-in-app branch October 7, 2026 22:24
@joelmoss joelmoss mentioned this pull request Oct 8, 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.

Wire boxd into the app: create, open and delete remote workrooms on boxd

2 participants