Skip to content

Machine providers: durable lifecycle and shared plugin APIs - #3274

Merged
ymichael merged 21 commits into
mainfrom
bb/machine-provider-apis
Sep 11, 2026
Merged

Machine providers: durable lifecycle and shared plugin APIs#3274
ymichael merged 21 commits into
mainfrom
bb/machine-provider-apis

Conversation

@ymichael

@ymichael ymichael commented Sep 8, 2026

Copy link
Copy Markdown
Collaborator

Human comments

What was wrong

Cloud-backed workspaces need coordinated machine allocation, enrollment, server access, and durable lifecycle ownership. Without it, interrupted setup, pause/follow-up cycles, and project deletion can strand resources or interrupt other threads sharing a machine. Manual enrollment also read its command before preparation finished, and access readiness treated a registered provider as working access.

What changed

Adds experimental machine-provider and server-access APIs with UI, SDK, CLI, and Plugin Guide support. Core owns durable machine identity, encrypted bootstrap, enrollment, allocation checkpoints, checkout ownership, and coordinated lifecycle transitions. Plugins own vendor allocation, snapshots, and idle timing. Standalone machines have independent lifetimes; environment compositions pair machine and workspace providers through register().

Includes Manual enrollment, Connect server access, and Modal sandboxes. Modal supports configurable images and size presets, filesystem snapshots, same-host resume, and diagnostics. Its vendor implementation now lives under providers/modal/, with a shared adapter for machine lifecycle registration. Idle policy, settings, diagnostics, and UI remain Modal-specific.

Ephemeral machines account for live threads and launches across projects and environments, retiring after their final owner releases them. Durable resuming state holds queued follow-ups until restoration completes and drives settings, environment selection, and thread banners. Interrupted allocation, suspension, resume, and cleanup recover through persisted checkpoints. Checkout recovery retries when an earlier thread-start RPC settles, so a follow-up accepted after turn completion cannot remain stuck behind that RPC.

Plugin bootstrap requires an executor and uses the selected access provider; Manual setup uses internal enrollment operations. Cancellation reaches enrollment and access acquisition. Cleanup uses the creation key before a checkpoint and the resource afterward. Both UI and CLI wait for manual command readiness. Access settings and composer banners consume validated availability and refresh on recheck and plugin enable/disable; errors and timeouts become unavailable without exposing private diagnostics.

Provider-icon registrations require an agent, machine, or environment kind in the SDK. Environment providers and compositions require a description and icon. Existing untyped plugins can omit these fields at runtime; omitted icon kinds apply across kinds, while a specific kind takes precedence. Cross-plugin icon overrides remain allowed. Older compositions that omit an icon use FolderUnknownIcon, without inheriting the machine provider logo. Mobile includes the same icon and its native SF Symbol mapping.

Default machine choices, CLI enumeration, updates, and CLI-skill installation exclude ephemeral sandboxes; bb machine list --all and explicit machine targeting retain access to them. Active sandbox threads keep their current machine identity even with only one persistent machine. Project summaries count configured and total machines within the same scope; checkout counts follow the Show all machines toggle, and hidden online sandboxes do not mask offline persistent checkouts.

Plugin Guide, API map, audit entries, and CLI guides describe the implemented contracts. Removed obsolete wording/feature assertions and added compiled examples and boundary regressions. Artifact tests copy nested plugin source directories so the Modal refactor is covered while retaining SDK bundling and Dockerfile assertions.

Migration 0117_machine_providers.sql follows main's 0116. Host daemon protocol is 203, combining main's protocol changes with this branch's machine/bootstrap/shutdown changes. Plugin SDK version is 0.4.83. Modal participates in the bundled-plugin preparation graph.

How you verified

Rebased onto bc75f9dde7 and rebuilt the source app, server, daemon, bundled plugins, and matching CLI. Local checks passed 9,732 tests, with five skips, across app, server, daemon, CLI, database, domain, Plugin SDK, API map, server/daemon contracts, scripts, Connect, Modal, and plugin-build. Relevant typechecks and lint passed.

pnpm exec turbo run test typecheck lint --filter=@bb/server --filter=@bb/app --filter=@bb/host-daemon --filter=@bb/cli --filter=@bb/db --filter=@bb/domain --filter=@get-bb/plugin-sdk --filter=@bb/plugin-api-map --filter=@bb/server-contract --filter=@bb/host-daemon-contract --filter=@bb/scripts --filter=bb-plugin-environment-modal-sandbox --filter=bb-plugin-connect --concurrency=2
pnpm exec turbo run test typecheck lint --filter=@bb/plugin-build

The artifact regression reproduced locally before the fixture fix and passes afterward. The previous Linux package-smoke failure occurred during Ubuntu mirror installation; the rebase includes main's mirror fix.

Fresh live QA uses this worktree's isolated source server and built CLI, real Modal compute/snapshots, Connect access, and Pi agent turns executing shell tools:

  • Two projects, eight environments, and ten threads across one and two sandboxes, including shared worktrees. Read back distinct workspace markers from each actual host/path.
  • Ten concurrent queued follow-ups with both hosts paused restore the correct hosts and preserve all markers.
  • Delete one project across both hosts while preserving the other project's threads, environments, and files.
  • Archive shared owners one at a time; destroy the final owner's host automatically while work continues on the other host.
  • Pause with two active threads, resume through the surviving shared owner, then archive the final owner while paused. Both matrix machines are destroyed in one attempt without manual removal/retry.
  • Stop a thread after its shell command actually starts, verify idle, and complete a follow-up on the same workspace.
  • Browser Modal icon, selection persistence across reload, settings Suspend, and thread-banner Resume. The resuming banner appears and clears on completion.
  • Manual CLI enrollment command appears exactly once; Ctrl-C exits following with code 130; explicit removal clears the access grant.
  • Install and reload an untyped legacy plugin: missing environment presentation fields are accepted, and omitted-kind plus all three specific-kind icon registrations load together. Remove the fixture afterward.

The natural idle sweep suspended a third sandbox after 76 seconds with a temporary one-minute threshold. A follow-up restored its files and ran once; archive while paused automatically destroyed it in one attempt. The original ten-minute setting was restored. Independent Modal API checks confirm all three allocations have no compute and all seven captured snapshots were deleted. All six pre-existing machine phases and general/access/Modal settings match before and after.

All applicable CI checks passed on 30bf86c10b, including workspace build/typecheck/lint, every test shard, Linux/macOS package smoke, bundle budgets, and SDK/version guards. iOS and optional Node compatibility smoke are skipped by workflow conditions. The subsequent picker-description removal passes all 31 picker tests. Direct folder-browser loading is restored; omitted composition icons use FolderUnknownIcon. The affected app/server tests (18 tests), typechecks, lint, and bundle budget pass. The measured boot payload is 1,610.0 KB raw / 394.6 KB brotli, and the thread-route closure is 2,142.9 KB raw / 571.3 KB brotli. The latest CI fixes pass all 82 integration tests, all 334 mobile tests, 18 server provisioning tests, and relevant typechecks/lint. A controlled delayed start acknowledgment reproduces the checkout recovery failure before the fix and verifies successful follow-up output afterward. Integration tests ran locally with two workers after a fully parallel run encountered startup timeouts during concurrent builds. All applicable CI checks pass on 5c38bd6fb4, including the integration and packages shards, workspace checks, all app/server tests, and Linux/macOS package smoke; GitHub reports the PR mergeable.

Earlier passes covered stopped/interrupted setup, scheduled work and suspension recovery across server restart, access-provider errors/timeouts, and live access banner refresh. This pass does not claim desktop/mobile QA or installation of a separately managed Manual daemon. Removal of an independently managed unreachable machine can still require workspace cleanup; automatic abandonment is not implemented.

Pre-merge CLI/server rerun on 5c38bd6fb4 passed with real Modal, Connect, Pi and Codex: two projects/eight environments/ten threads across one and two sandboxes; ten simultaneous offline follow-ups with exact single writes; stop/follow-up; cross-project deletion; shared-owner cleanup while the other host runs; active-turn suspension; and separate Codex and natural-idle resume checks. Manual command readiness and idempotent creation also passed. Auxiliary fixture collisions were corrected and rerun. All five allocations created during the run were automatically destroyed in one attempt, and independent Modal queries confirmed no compute and all eight captured snapshots deleted. Settings and all seven pre-existing machine phases match the baseline. Cleanup emitted non-blocking Keep Awake/offline and Codex archive/machine-removing warnings; final archive and cleanup succeeded. No code changes were needed for that QA pass.

The display fixes in 2bd1ed9f36 add three regression tests for current sandbox identity, scoped project counts/offline status, and checkout counts across the Show all machines toggle. All three fail before the fixes and pass afterward. All 66 focused app tests pass; app typecheck passes and lint reports no errors. The real-provider QA above ran on 5c38bd6fb4, before the subsequent machine-enumeration and display changes; That push exposed the upstream Codex parity fixture failure described below.

pnpm exec turbo run test --filter=@bb/app -- environment-workspace-display.test ProjectsSettingsSection.test ProjectDetailSettingsView.test ThreadEnvironmentSummary.test PluginThreadChat.test PluginThreadChat.provider-context.test
pnpm exec turbo run typecheck lint --filter=@bb/app

Rebased onto 18b71d810c to include #3526's fix for Codex warning parity. The earlier CI packages failure came from #3478 changing warning classification while the recorded expected event still used the unhandled-event fallback. #3526 refreshes that single fixture and pins its unhandled count to zero; it also adds provider source files to the parity test's Turbo inputs. All 21 branch commits replayed without conflicts. On rebased head 92d25beb50, all 56 provider-parity tests, 281 Codex provider tests, and 66 focused app tests pass, along with app/parity/Codex typechecks. All applicable checks pass on rebased head 92d25beb50 in CI run 34643021790, including packages, server, all app shards, integration, workspace build/typecheck/lint, and Linux/macOS package smoke. iOS and optional Node compatibility jobs are skipped by workflow conditions.

AGENT GENERATED

ymichael and others added 17 commits September 11, 2026 13:12
Squash of bb/machine-provider-apis on top of 12e6e97. Machine
creation is now the environment's first provisioning step, the machine
migration is 0117 on top of the consolidation's 0116, and machine-phase
progress and failures are reported into the thread transcript.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
focusPromptBox was a plain function declaration, so it got a new
identity on every render. RootComposeView schedules it through
requestAnimationFrame in an effect that lists it as a dependency, so
calling it bumped focus-request state, re-rendered, produced a new
identity, re-ran the effect, and called it again. The loop ran at frame
cadence for as long as the composer was mounted.

That effect early-returns unless startedComposing is true, which is why
a cold load looked fine and returning to the new thread page from a
thread did not: the loop only starts once composing has begun.

Every frame it called focusEnd() on the prompt editor. Opening any menu
put focus inside that menu, the next frame pulled focus back out, and
Radix dismissed the layer -- the environment picker appeared to open and
close instantly. Traced from an open-to-dismiss window of five
milliseconds, with the dismissal stack running through our focusEnd into
DismissableLayer's onDismiss.

Memoize the callback so its identity is stable, and list it in the
plugin composer host memo that reads it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ymichael and others added 4 commits September 11, 2026 13:12
selectHosts silently returned every host, so ephemeral provider sandboxes
leaked into machine pickers, the projectless machine chip, usage limits and
the counts that gate multi-machine UI. It now takes a required scope and
callers state whether they want persistent machines or all of them.

The projectless chip keeps a selected ephemeral host in its list so filtering
the choices never hides the current selection.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
bb machine list, bb updates, and bb skill install-cli-skills enumerated every
host, so disposable provider sandboxes appeared as ordinary machines and
collected CLI installs and update runs they discard on recreation. The server
default for CLI-skill status did the same.

Each enumeration now selects persistent machines, matching the app's Show all
machines reveal; bb machine list --all opts back in and the table gained a Type
column so sandboxes are distinguishable. Lookups by id or name still resolve
sandboxes, so --machine can still target one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@ymichael
ymichael force-pushed the bb/machine-provider-apis branch from 2bd1ed9 to 92d25be Compare September 11, 2026 20:12
@ymichael
ymichael merged commit b3c5434 into main Sep 11, 2026
16 checks passed
@ymichael
ymichael deleted the bb/machine-provider-apis branch September 11, 2026 20:20
ymichael added a commit that referenced this pull request Sep 11, 2026
## Human comments

## What was wrong

Connect machine cleanup could become permanently stuck after the remote
revocation had already succeeded but its response was lost, or after
local grant cleanup failed. On retry, the owned machine row was already
revoked and no longer held a label claim, so `revokeMachine` returned
`not-found` instead of acknowledging the completed revocation. The
provider retained its local grant and every later retry failed the same
way.

## What changed

- Make revocation idempotent for an already-revoked machine owned by the
authenticated account.
- Preserve existing authentication and account-ownership checks: missing
devices, foreign devices, bogus credentials, and revoked server
credentials still fail.
- Preserve label-claim tunnel closure and its retry behavior; a
completed retry does not close the tunnel twice.
- No UI, public API shape, CLI, configuration, or server/daemon wire
contract changed. No protocol-version bump is needed.

## How you verified

- Added a regression that fails with the previous 404 behavior and
passes when a completed owned revocation is retried through both
dashboard and server-credential paths.
- `pnpm exec turbo run test --filter=@bb/web --force --
src/server/api.test.ts`: 31/31 passed.
- `pnpm exec turbo run test --filter=bb-plugin-connect --force --
src/server-access.test.ts src/tunnel-lifecycle.test.ts`: 20/20 passed.
- `pnpm exec turbo run typecheck build --filter=@bb/web --force`:
passed.
- `bb plugin build plugins/connect`: passed.
- Targeted formatting and `git diff --check` passed.
- Independently exercised response-loss, real SQLite deletion-failure,
pending reconciliation, authorization, and socket-cleanup recovery cases
with owned local fixtures; no cloud credentials or vendor resources were
used.

Follow-up to #3274.

> AGENT GENERATED
ymichael added a commit that referenced this pull request Sep 12, 2026
## Human comments

## What was wrong

Repository setup and teardown scripts were launched with piped stdin,
but the host daemon never wrote to or closed that pipe. Any
noninteractive hook that read stdin—for example, `cat >/dev/null` before
doing its work—waited until the lifecycle timeout. Setup could prevent
an environment from becoming ready, while teardown could skip cleanup
commands after the read.

## What changed

- Launch repository lifecycle scripts with the existing output-only
process helper, which supplies immediate stdin EOF while preserving
piped stdout and stderr.
- Apply the same behavior to setup and teardown through their shared
runner.
- Preserve contributed environment variables, streamed UTF-8 output,
timeouts, cancellation, and process-group cleanup.
- No public API, CLI, configuration, or server/daemon wire contract
changed. No protocol-version bump is needed.

## How you verified

- Added setup and teardown regressions that read stdin before writing a
marker, emit split UTF-8 output, and write stderr. Both fail before the
fix and pass after it.
- `pnpm exec turbo run test --filter=@bb/host-daemon --force --
src/environment-lifecycle-script.test.ts
src/operation-environment.test.ts
test/command/environment-hook.test.ts`: 20/20 passed.
- `pnpm exec turbo run build typecheck --filter=@bb/host-daemon
--force`: passed.
- Targeted formatting and `git diff --check` passed.
- On a real Modal sandbox, repository setup received EOF, a real Codex
agent completed its turn, and a separate disposable worktree's teardown
received EOF and wrote its cleanup marker. The sandbox was suspended
after verification.

Follow-up to #3274.

> AGENT GENERATED
ymichael added a commit that referenced this pull request Sep 12, 2026
## Human comments

## What was wrong

Variables saved in Machine environment were omitted from ordinary
repository setup. A script needing a configured token could fail even
though BB had saved the value correctly. The machine-environment support
in [PR #3274](#3274) supplied explicit
hooks, but normal setup ran through `environment.attach` without those
values.

## What changed

Resolve fresh machine variables on the server when dispatching setup,
then pass them transiently through `environment.attach` into the
existing setup runner. The retained provisioning request stays free of
plaintext values. Existing encryption, precedence, cancellation and
recovery behavior remain; unmanaged attached paths still skip setup.

Adds the validated attach contribution field and bumps host-daemon
protocol **204 → 205**, requiring older daemons to update. Updates the
configuration documentation, machine CLI guide and bb-cli reference. No
new user-facing setting or CLI command.

## How you verified

- Server-delivery and setup-shell regressions failed before the fix and
pass afterward.
- **146 tests pass**: 57 server, 30 daemon, 56 contract and 3
integration tests. Coverage includes fresh values on repeated dispatch,
local-host exclusion, encrypted storage, retained request/state privacy,
missing-key failure, cancellation with duplicate attaches, explicit
hooks, cloning and old-protocol rejection.
- Turbo builds and typechecks passed for `@bb/server`, `@bb/host-daemon`
and `@bb/host-daemon-contract`; formatting and diff checks passed.
- Started `pnpm start:worktree`, configured Modal, and created a test
thread through the BB CLI. A real Modal sandbox installed this server’s
daemon, cloned a disposable repository, and ran ordinary setup. Setup
required a configured test variable and wrote a receipt; the live Codex
thread read the receipt and returned **PASS**. The initial byte
comparison omitted the receipt’s trailing newline; the corrected check
passed without changing the file.

Focused commands:

```sh
pnpm exec turbo run test --filter=@bb/server -- test/hosts/live-command.test.ts test/internal/internal-session-protocol-version.test.ts test/services/environments/provider-orchestration.test.ts src/services/hosts/host-environment.test.ts
pnpm exec turbo run test --filter=@bb/host-daemon -- test/command/environment-dispatch.test.ts test/command/environment-hook.test.ts test/command/command-router.test.ts test/command/project-clone-private-env.test.ts src/operation-environment.test.ts
pnpm exec turbo run test --filter=@bb/host-daemon-contract
pnpm exec turbo run test --filter=@bb/integration-tests -- fake/environments/environment-attach-recovery.test.ts
pnpm exec turbo run build typecheck --filter=@bb/server --filter=@bb/host-daemon --filter=@bb/host-daemon-contract
pnpm exec turbo run build typecheck lint --cache-dir=.turbo/cache --output-logs=new-only --concurrency=4
```

> AGENT GENERATED
ymichael added a commit that referenced this pull request Sep 12, 2026
## Human comments

## What was wrong

The machine-environment CLI decoded every stdin chunk separately. A
multibyte UTF-8 character split across pipe chunks was therefore
replaced with invalid-character markers before the value was saved, even
though the command reported success. This follows up on the
machine-environment support added in [PR
#3274](#3274).

## What changed

Collect stdin as bounded byte buffers and decode UTF-8 once after EOF.
The existing 65,536-byte limit is still enforced on raw bytes before
removing one trailing newline. Empty input, CRLF handling, secret
masking, validation, and preservation of unrelated variables are
unchanged.

## How you verified

- Added regressions for deliberately split UTF-8, empty input, CRLF,
repeated newlines, exact ASCII and multibyte byte limits, and over-limit
rejection. The split-character and multibyte-boundary cases fail before
the fix and pass afterward.
- `pnpm exec turbo run test typecheck build --filter=@bb/cli --force`:
59 files and 607 tests passed; build and typecheck passed.
- Exact two-file formatting and `git diff --check origin/main...HEAD`
passed.
- Ran the built CLI against the worktree test server and its encrypted
disk SQLite database. Split `café`, `€`, and `🌍` values were stored
exactly; byte limits, masking, and unrelated-value preservation passed.
Temporary variables were removed.

> AGENT GENERATED
ymichael added a commit that referenced this pull request Sep 12, 2026
## Human comments

## What was wrong

The shared new-thread composer treated checkout-capable providers as
eligible on every connected machine, even when a machine had no source
for the selected project. That exposed Project checkout and Worktree on
an unconfigured machine; if that explicit choice later became invalid,
resolution could silently fall back to the primary machine. This
regressed in
[b3c5434](b3c5434)
([#3274](#3274)).

## What changed

- Derive existing-machine checkout eligibility from the project's
sources and the existing project-scoped provider query.
- Hide Project checkout and Worktree when that project is not set up on
a machine, while preserving valid local, remote, and hostless choices.
- Retain an explicit selected machine and block submission if its
checkout disappears or becomes unavailable instead of redirecting the
thread elsewhere.
- Add focused composer regressions for per-machine eligibility,
invalidation, availability, keyboard behavior, and valid fallback-free
selections.

| Before | After |
| --- | --- |
| ![Before: an unconfigured remote machine incorrectly offers Project
checkout](https://raw.githubusercontent.com/get-bb/bb/24a00316fd9954da1cc8e254e047889e10678007/before.png)
| ![After: the unconfigured remote machine says it is not set up and
offers no checkout
options](https://raw.githubusercontent.com/get-bb/bb/24a00316fd9954da1cc8e254e047889e10678007/after.png)
|

The paired 390×844 dark-theme captures use the actual source components
with synthetic SDK and machine data. The before image is from base
`1ebdc56a50`; the after image is from pushed head `63920f8faa`.

No server/daemon wire contract, CLI, public SDK, generated file, or
lockfile changes.

## How you verified

- New eligibility regressions failed before the fix and passed after it.
- `pnpm exec turbo run build typecheck test --filter=@bb/app --force` —
build and typecheck passed; 4,469 tests passed and 3 were intentionally
skipped.
- Extra templates validation — 44 tests passed.
- Browser target matrix — 20 submission/control cases plus four
rejection, keyboard, focus, root, and overflow cases passed at 1280×900
and 390×844 in light and dark themes.
- `git diff --check origin/main...HEAD` passed, and Git reports a
conflict-free merge with current `main`.

> AGENT GENERATED
ymichael added a commit that referenced this pull request Sep 12, 2026
## Human comments

## What was wrong

Plugin host calls share a process and its environment. After #3274, a
call needing changed machine variables waited for all active calls to
finish. That could block a stop/close call behind the long-running work
it was meant to stop. Cancelling a waiting call could also trigger the
manager's five-second force-kill timer and interrupt unrelated calls in
the same worker.

## What changed

Remove the environment wait queue. While any calls are active, incoming
calls run immediately using the worker's current environment, even if
they supply different values. Once all calls finish, the next incoming
call supplies fresh values. Values from overlapping calls are not saved
as pending updates. Continuous overlapping work can intentionally retain
old settings until the worker becomes idle.

The environment is restored when the last active call finishes. Existing
cancellation and force-kill protection for unresponsive handlers remain
intact. Acquisition stays inside the call's error cleanup.

Bump `HOST_DAEMON_PROTOCOL_VERSION` from 205 to 206 so enrolled machines
update for the changed environment semantics. Document when plugin calls
adopt settings in the machine CLI guide, configuration docs, and bb-cli
skill reference. No new CLI flags or public SDK members. This PR does
not touch UI code.

## How you verified

- Updated real-worker regression coverage for immediate admission with
different values, retaining the first values until every overlapping
call finishes, adopting the next idle call's values, variable removal,
cancellation/deadlines, disposal, and hung-handler termination. Four
tests failed with the previous waiting implementation; all 32 plugin
host manager tests pass after the change.
- Post-rebase `pnpm exec turbo run test build typecheck
--filter=@bb/host-daemon --filter=@bb/host-daemon-contract --force` —
617 daemon tests and 56 contract tests passed; all 11 Turbo tasks
passed.
- Actual browser-plugin host handlers running through the real manager,
worker process, and IPC, with an injected cooperative runtime:
previously close waited behind a two-second run after a variable change;
now it interrupts the run and returns in 1 ms with either unchanged or
changed values.
- Verified through the actual built `bb` CLI, isolated source server,
enrolled built daemon, and a diagnostic plugin installed with `bb plugin
install`. Two 20-second calls overlapped across a machine-variable
update and retained the old values in one worker PID; stop commands
returned in 268 ms and 259 ms; the first idle call adopted the new
values and later reflected removal.
- Targeted formatting and `git diff --check origin/main...HEAD` passed.

> AGENT GENERATED
ymichael added a commit that referenced this pull request Sep 12, 2026
## Human comments

## What was wrong

Automatic shared-machine cleanup counted live thread executions but
missed pending and starting threads whose workspace was still being
prepared or whose scheduled start had not dispatched. Archiving the
previous last owner could therefore remove an ephemeral machine
underneath new work already assigned to it. The gap came from
[b3c5434](b3c5434)
([#3274](#3274)).

## What changed

- Include persisted pending/starting environment intents and preparing
workspace ownership in the automatic-removal guard.
- Use targeted SQLite queries so retention survives database reopening
without adding a manager, timer, or schema migration.
- Recheck that a machine is still available before saving a startup
placement and before reserving a provider workspace, so removal winning
the race fails cleanly.
- Preserve cleanup for failed, cancelled, archived, deleted, and
orphaned work, as well as existing explicit machine-removal behavior.

No UI, host-daemon wire contract, CLI, public SDK, generated file, or
lockfile changes.

## How you verified

- Five new regressions failed before the fix and passed afterward:
archive/project deletion during workspace preparation or a scheduled
start, plus removal winning admission.
- 105 relevant tests passed: 99 server tests covering environment
providers, machine cleanup, and startup recovery; 6 database tests
covering persistence, terminal owners, orphan rows, and reuse.
- Post-rebase `pnpm exec turbo run test build typecheck
--filter=@bb/server --filter=@bb/db --force` passed all 13 Turbo tasks:
2,606 server tests and the full database suite passed, with one
intentional server skip.
- Isolated HTTP + migrated SQLite verification covered preparation and
scheduled-start cleanup through archive and project deletion.
- Real Modal verification retained a machine through preparing and
scheduled work, executed both surviving threads on that same machine,
and removed it after the final owner was archived.
- `git diff --check origin/main...HEAD` passed, and Git reports a
conflict-free merge with current `main`.

> AGENT GENERATED
ymichael added a commit that referenced this pull request Sep 12, 2026
## Human comments

## What was wrong

After Modal creates a sandbox but saving its resource checkpoint fails,
cleanup has only the allocation key. Reconciliation looked in the
currently configured app, so changing App Name could make cleanup report
success while leaving the original sandbox running. This affects the
cleanup path introduced in #3274.

## What changed

Find sandboxes across apps in the current Modal environment using the
existing `bbMachineKey` tag, terminate every match, and verify removal
by sandbox ID. Enumeration and termination failures remain retryable.
The change stays inside the Modal plugin and requires no new stored
fields, migration, core changes, or protocol changes.

## How you verified

- The cross-app regression failed before the fix and passes afterward.
Tests cover duplicate matching allocations, unrelated sandboxes,
repeated cleanup, and enumeration/termination failures followed by
retry.
- `pnpm exec turbo run test typecheck prepare:bundled lint
--filter=bb-plugin-environment-modal-sandbox` passed, including 52
tests.
- A local probe exercised the actual cleanup route with migrated SQLite
and an injected checkpoint failure. An installed-SDK probe verified tag
filtering without an app filter and pagination. Both used simulated
external compute; no live Modal API calls were made.

> AGENT GENERATED
ymichael added a commit that referenced this pull request Sep 12, 2026
## Human comments

## What was wrong

#3274 replaced plain machine names in the environment picker headers
with `MachineLabel`, adding a tiny laptop icon beside every machine
name, including servers and desktop machines.

## What changed

Restore the previous status-dot-and-name header markup. Preserve name
truncation and the conditional “this machine” badge. Update the existing
picker test to assert that headers contain no laptop icon.

## How you verified

- `pnpm exec turbo run test --filter=@bb/app --
EnvironmentPicker.test.tsx` — all 31 tests passed, including the “this
machine” badge assertion.
- Captured and visually inspected the actual picker in Ladle with
fixture machines, both with and without local-daemon detection. Headers
have no laptop icons; the local fixture shows the badge. Screenshot
animations were disabled for stable captures.
- `git diff --check` passed.

> AGENT GENERATED
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