Skip to content

CmuxFleet: core engine — seams, app actuator/bridge, hook supervision, persistence - #7418

Closed
austinywang wants to merge 11 commits into
feat-fleet-engine-skeletonfrom
feat-fleet-core-engine
Closed

austinywang wants to merge 11 commits into
feat-fleet-engine-skeletonfrom
feat-fleet-core-engine

Conversation

@austinywang

@austinywang austinywang commented Jul 6, 2026 •

Copy link
Copy Markdown
Contributor

Scope

Part 3 of #7361 — the Fleet core engine. Stacked on #7374 (PR 1, base branch of this PR) and #7403 (PR 2, merged into this branch); the last commit is this PR's own diff.

  • FleetEngine (Packages/macOS/CmuxFleet): imperative engine driving the pure FleetSupervisor/FleetScheduler through seam protocols (FleetActuating, FleetWorldReading, FleetTimerScheduling, FleetProcessWatching, FleetPersisting). Handles dispatch ticks, provisioning bookkeeping with per-task generations (cancel-during-provision closes orphan workspaces), attempt-scoped backoff + stall timers, hook-signal mapping (claude Stop = per-turn activity; SessionEnd/pid-exit are end-of-run), reconcile pass (workspace-gone → cancelled, PR badge → prChanged → awaiting_review/done, prompt-idle fallback with grace window), and JSON persistence with restore.
  • App seams (Sources/Fleet/): actuator provisions a git worktree (git worktree add -b fleet/<task>) + an unfocused workspace grouped under the fleet's name (addWorkspace(select: false) + group APIs), types the agent command via the queued sendInputResult path, kills via pid/ETX, posts localized notifications (EN/JA); world reader consumes the sidebar PR badge and shell-activity state; timers/pid watchers follow the FeedCoordinator.armPidWatcher DispatchSourceProcess pattern with identity-guarded, cycle-free fire paths; persistence mirrors the session-snapshot conventions (atomic, sortedKeys, identical-content skip).
  • Workstream hook tap: publishWorkstreamEvent moved to a new Sources/CmuxWorkstreamEventPublishing.swift (shrinking at-cap CmuxEventPublishing.swift 502 → 434) and now forwards phase-"received" hooks to the engine when it is live.
  • The PR 2 fleet.* socket stubs are replaced by the engine bridge; fleet.task.open focuses the task workspace through the same body workspace.focus uses — the single focus-changing fleet path.
  • Frozen: CmuxControlSocket package untouched; both budget TSVs untouched; no new package dependencies.

Tests

  • cd Packages/macOS/CmuxFleet && swift test — 56 tests (engine happy path, needsInput round-trip, turn-vs-end semantics, retry/backoff to failed, pid-exit + stale-attempt drops, PR handoff to awaiting_review/done, scheduler caps, cancel/retry, reconcile workspace-gone + prompt-idle grace (positive and negative), stall, persistence round-trip/restore, open targets, unknown-workspace + terminal-state hook ignores, cancel-during-provision stale-outcome drop).
  • cd Packages/macOS/CmuxControlSocket && swift test — 187 tests unchanged.
  • pbxproj normalized (scripts/normalize-pbxproj.py idempotent, check-pbxproj.sh green); all new files < 500 lines.
  • Live e2e (fleet on a manaflow-ai/cmux clone; agent = claude -p; two tasks provisioned into worktree workspaces, supervised to PR handoff) — transcript in the PR discussion after dogfood.

Plan/code/judge loop: plan + 3 coder rounds + 2 judge passes; final judge issue (timer retain cycle) fixed verbatim.


View with Codesmith Autofix with Codesmith
Need help on this PR? Tag /codesmith with what you need. Autofix is disabled.


Summary by cubic

Adds the Fleet core engine with app seams, control-socket bridge, hook supervision, and persistence so fleets can be created, started, and run tasks end-to-end, with PR rescue for failed tasks and smarter retries. Also fixes SSH failures on hosts with a configured RemoteCommand by overriding it with -o RemoteCommand=none across cmux-controlled SSH invocations.

  • New Features

    • Introduced CmuxFleet engine: provisions git worktrees, launches agents, supervises via workstream hooks, runs backoff/stall timers and pid watchers, reconciles state (now rescues failed tasks on PR open/merge and only cancels on workspace-gone for non-terminals), reuses live workspaces on retry and drops stale workspace mappings, and persists/restores JSON snapshots.
    • App integration: actuator/world reader/timers/process watcher/persistence in Sources/Fleet/, workstream hook tap (CmuxWorkstreamEventPublishing.swift) forwarding to the engine, and localized Fleet notifications (EN/JA).
    • Control socket: implemented fleet.* in @CmuxControlSocket and bridged to the engine; fleet.task.open focuses via the existing workspace focus path.
    • Packaging: FleetPromptTemplate is now an instantiable struct to comply with package conventions.
  • Bug Fixes

    • Resolved SSH RemoteCommand conflicts (issue cmux ssh fails when SSH Host config sets RemoteCommand/RequestTTY #7246) by inserting -o RemoteCommand=none ahead of destinations for all cmux-supplied commands across CLI, @CmuxCore daemon transport, tmux control, file explorer, git status, and @CmuxRemoteSession; for batch SSH execs, also force -o RequestTTY=no.
    • Quoted FleetPromptTemplate placeholders for {{DIR}} and {{BRANCH}} so paths with spaces cannot split rendered commands.
    • Fixed an init ambiguity for FleetID in the fleet task-list control-bridge to ensure correct ID parsing.

Written for commit fdb31ea. Summary will update on new commits.

Review in cubic

austinywang and others added 4 commits July 4, 2026 18:25
…7359)

* Add failing regression tests for ssh hosts configured with RemoteCommand/RequestTTY

cmux ssh against a host alias whose ssh_config sets `RequestTTY yes` and
`RemoteCommand sudo su -` exits 255 with OpenSSH's "Cannot execute
command-line and remote command." and loops the reconnect banner
(issue #7246): every cmux-controlled invocation that supplies its own
remote command (foreground auth `true`, bootstrap installer hop, daemon
stdio transport, coordinator batch plumbing, ssh-tmux control commands)
inherits the host RemoteCommand instead of overriding it.

Covers, all red without the fix:
- cmuxTests/SSHConfiguredRemoteCommandHostTests: end-to-end `cmux ssh`
  startup scripts (persistent-PTY foreground-auth flow and bootstrap
  install flow) against a fake ssh that mirrors OpenSSH's rule, plus the
  app-side SSHPTYAttachStartupCommandBuilder foreground auth argv.
- cmuxTests/RemoteTmuxHostRemoteCommandOverrideTests: shared ssh-tmux
  control args, interactive auth, and tmux -CC control-mode argv.
- CmuxCoreTests: daemonTransportArguments (cmuxd stdio transport).
- CmuxRemoteSessionTests: coordinator batch exec argv (port scan) and
  override/RequestTTY ordering ahead of caller-configured options.

Part 1 of 2 (test-only, expected red); the fix lands separately so CI
proves these tests catch the bug.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Override host-configured RemoteCommand in cmux-controlled ssh invocations

Fixes `cmux ssh` (and every other cmux-built ssh exec) against host
aliases whose ssh_config sets `RemoteCommand` (typically with
`RequestTTY yes`): OpenSSH refuses a command-line remote command while a
configured RemoteCommand is in effect ("Cannot execute command-line and
remote command.", exit 255), so the foreground auth hop died before the
session ever started and the pane looped reconnect attempts
(issue #7246).

New shared CmuxFoundation constant `SSHHostConfiguredRemoteCommand`
(`-o RemoteCommand=none`, OpenSSH >= 7.6 — macOS has shipped newer
clients since 10.13.2) applied at every builder that appends its own
remote command:

- CLI `cmux ssh`: foreground-auth hop, bootstrap installer hop, and the
  `cmux ssh <dest> -- <command>` passthrough branch (inserted right
  after `ssh`, so it also wins over caller-supplied options under
  OpenSSH's first-value-per-option rule). The interactive session hop
  keeps carrying cmux's own `-o RemoteCommand=<bootstrap>`, which
  already overrides the host config; bare interactive invocations (VM
  attach) are untouched.
- App restore/reattach: SSHPTYAttachStartupCommandBuilder foreground
  auth.
- Coordinator batch plumbing (bootstrap probes/install, BootstrapTTY,
  port scans, upload cleanup, relay metadata, stale-listener cleanup):
  sshCommonArguments(batchMode:) now also pins `-o RequestTTY=no` so a
  host `RequestTTY force` cannot CRLF-corrupt parsed pipes.
- CmuxCore daemonTransportArguments (cmuxd stdio transport).
- ssh-tmux stack via RemoteTmuxHost.sshControlArguments (interactive
  auth, `tmux -CC` control mode — which keeps its forced `-tt` — and
  one-shot discovery/mutation commands).
- File explorer listing, remote git status, and drag-drop upload
  cleanup argv builders.

Invocations with no remote command (`-N` forwards, `-O` control ops,
`-G` config dumps, plain interactive shells) are unchanged, and hosts
without a configured RemoteCommand see identical behavior — the
override is inert there.

The CLIRemoteShellStartupPerformanceTests fake ssh now mirrors
OpenSSH's real RemoteCommand semantics (first value wins, `none`
clears) so the installer hop's new override falls through to the
positional command exactly like real ssh.

Fixes #7246

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Make SSHHostConfiguredRemoteCommand an instantiable struct per package conventions

The package-conventions lint forbids all-static public namespace types in
packages; follow the SSHAgentSocketResolver pattern (public struct with a
public initializer) and access the override via an instance at every call
site.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Part 2 of the Fleet chain (#7361). Adds the fleet.* command domain to
CmuxControlSocket following the coordinator seam pattern: typed wire
values, ControlFleetContext seam protocol, handleFleet dispatch for
fleet.list/create/start/stop/status and
fleet.task.add/list/retry/cancel/open, plus coordinator tests. The app
target conforms with pre-engine stubs (empty reads, unavailable
mutations) until PR 3 wires the FleetEngine bridge.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ence

Part 3 of the Fleet chain (#7361). FleetEngine (CmuxFleet package) drives
the pure supervisor/scheduler through seam protocols: provisioning git
worktrees + unfocused grouped workspaces per task, typing the agent
command into the real terminal, supervision via workstream hook events,
kqueue pid-exit watchers, stall/backoff timers and a reconcile tick
(workspace-gone, PR badge changes, prompt-idle fallback), with JSON
persistence across relaunches. App-side seams live in Sources/Fleet/;
the workstream hook tap rides a new CmuxWorkstreamEventPublishing
extraction that shrinks the at-cap CmuxEventPublishing.swift. The PR 2
fleet.* socket stubs are replaced by the engine bridge, so fleet
create/start/task.add/list/retry/cancel/open work end-to-end over
cmux rpc. Fleet notification strings are localized (EN/JA).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 6, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cmux Canceled Canceled Jul 6, 2026 3:51am
cmux-staging Building Building Preview, Comment Jul 6, 2026 3:51am

@coderabbitai

coderabbitai Bot commented Jul 6, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: f1fe4730-40d3-44a7-a40c-44fae95a925e

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat-fleet-core-engine

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@greptile-apps

greptile-apps Bot commented Jul 6, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

Adds the Fleet core engine with app seams (FleetAppActuator, FleetAppWorldReader, FleetAppTimers, FleetAppProcessWatcher, FleetAppPersistence), implements all fleet.* control-socket methods via a new @MainActor ControlFleetContext bridge on TerminalController, and wires workstream hook events into the engine through CmuxWorkstreamEventPublishing. Also fixes SSH RemoteCommand conflicts (issue #7246) across CLI, daemon transport, tmux, git status, and remote session invocations.

  • Fleet engine (CmuxFleet package): FleetEngine drives pure FleetSupervisor/FleetScheduler reducers; per-task provision generations close orphan workspaces on cancel; attempt-scoped backoff and stall timers use identity-guarded [weak self] fire paths; reconcilePass probes workspace existence, PR badge changes, and prompt-idle grace; JSON persistence with atomic write and byte-identical skip.
  • App seams (Sources/Fleet/): Provisioning offloads filesystem/git work to Task.detached(priority: .utility); timer and PID-watcher implementations guard stale fires with object-identity checks; all ten Fleet notification strings have EN and JA translations in Localizable.xcstrings.
  • SSH fix: New SSHHostConfiguredRemoteCommand struct centralises -o RemoteCommand=none insertion; applied consistently across the affected call sites with a companion test suite.

Confidence Score: 4/5

The engine logic, timer/process-watcher patterns, actor isolation, and SSH fix are all solid; the main outstanding concern noted in a prior review thread — blocking disk I/O running on the main actor inside FleetAppPersistence — has not been addressed in this PR and affects every task state transition.

The fleet engine architecture is well-structured and the new app seams are correct. The unresolved blocking I/O on the main actor (raised in the previous review thread) remains in FleetAppPersistence.save() and FleetEngine.restore(), and the FleetAppHost.shared singleton introduced here holds live runtime state in global scope.

Sources/Fleet/FleetAppPersistence.swift (blocking I/O on main actor, unaddressed from prior thread) and Sources/Fleet/FleetAppHost.swift (new singleton for runtime engine state).

Important Files Changed

Filename Overview
Sources/Fleet/FleetAppHost.swift New singleton composition host (static let shared) holding the runtime FleetEngine — violates the no-ambient-global-state rule; engine should be owned and injected from the app seam.
Sources/Fleet/FleetAppPersistence.swift Synchronous Data(contentsOf:) in load() and data.write(to:options:.atomic) in save() run on @MainActor; flagged in prior review thread; byte-identical skip guard is a useful optimisation but doesn't remove the blocking write.
Packages/macOS/CmuxFleet/Sources/CmuxFleet/Engine/FleetEngine.swift Core engine driving FleetSupervisor/FleetScheduler; @MainActor throughout; restore() called synchronously from init loads persisted state via persistence.load() which is blocking disk I/O on the main actor (flagged in prior thread).
Packages/macOS/CmuxFleet/Sources/CmuxFleet/Engine/FleetEngine+Commands.swift Command dispatch and provisioning; per-task generation guard correctly closes orphan workspaces on cancel-during-provision; runGit is nonisolated and uses Task.detached correctly.
Packages/macOS/CmuxFleet/Sources/CmuxFleet/Engine/FleetEngine+Signals.swift Signal application, reconcile pass, stall timer management; [weak self] captures used consistently; stall timer cancel/re-arm logic is correct on state transitions.
Sources/Fleet/FleetAppActuator.swift App-side actuation; filesystem work correctly offloaded to Task.detached(priority: .utility); localized notification strings matched with %@ format in xcstrings; killAgent falls back from SIGTERM to ETX correctly.
Sources/Fleet/FleetAppTimers.swift Timer and process-watcher implementations; identity guard (self.timers[key] === timer) prevents stale fire; [weak self] avoids retain cycle; FleetAppProcessWatcher uses a separate utility queue correctly.
Sources/TerminalController+ControlFleetContext.swift Bridges @MainActor ControlFleetContext protocol to FleetAppHost.shared.engine; ControlCommandCoordinator is @MainActor so all context calls are correctly isolated; fleet.task.open focus path matches workspace.focus semantics.
Sources/CmuxWorkstreamEventPublishing.swift Extracts publishWorkstreamEvent from CmuxEventPublishing.swift and forwards received-phase events to FleetAppHost.shared; Task { @MainActor in } hop is consistent with existing codebase pattern.
Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Fleet/ControlCommandCoordinator+Fleet.swift Implements all fleet.* socket methods on the @MainActor coordinator; routing is clean and delegates to @MainActor ControlFleetContext; no WebKit/main-actor wait concerns.
Sources/Fleet/FleetControlSocketMapping.swift Wire-shape mapping implemented as extensions on existing types — not a caseless-enum namespace; clean bidirectional mapping.
Resources/Localizable.xcstrings Adds 10 Fleet notification strings with EN and JA translations; %@ format specifiers correctly match Swift String.LocalizationValue interpolations in FleetAppActuator.
Packages/macOS/CmuxFoundation/Sources/CmuxFoundation/SSHHostConfiguredRemoteCommand.swift New SSHHostConfiguredRemoteCommand struct provides -o RemoteCommand=none argv fragment to fix issue #7246; correct Sendable struct pattern, no ambient state.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    CS[Control Socket fleet commands] -->|MainActor| CC[ControlCommandCoordinator]
    CC -->|ControlFleetContext| TC[TerminalController]
    TC --> FAH[FleetAppHost.shared]
    FAH --> FE[FleetEngine]

    WS[Workstream Hook publishWorkstreamEvent] -->|Task MainActor| FAH

    FE -->|FleetActuating| FAA[FleetAppActuator]
    FE -->|FleetWorldReading| FAWR[FleetAppWorldReader]
    FE -->|FleetTimerScheduling| FAT[FleetAppTimers]
    FE -->|FleetProcessWatching| FAPW[FleetAppProcessWatcher]
    FE -->|FleetPersisting| FAP[FleetAppPersistence]

    FAA -->|Task.detached| GIT[git worktree add]
    FAA --> TM[TabManager addWorkspace]
    FAP -->|atomic write| DISK[(fleet-state.json)]
    FE -->|restore on init| DISK
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    CS[Control Socket fleet commands] -->|MainActor| CC[ControlCommandCoordinator]
    CC -->|ControlFleetContext| TC[TerminalController]
    TC --> FAH[FleetAppHost.shared]
    FAH --> FE[FleetEngine]

    WS[Workstream Hook publishWorkstreamEvent] -->|Task MainActor| FAH

    FE -->|FleetActuating| FAA[FleetAppActuator]
    FE -->|FleetWorldReading| FAWR[FleetAppWorldReader]
    FE -->|FleetTimerScheduling| FAT[FleetAppTimers]
    FE -->|FleetProcessWatching| FAPW[FleetAppProcessWatcher]
    FE -->|FleetPersisting| FAP[FleetAppPersistence]

    FAA -->|Task.detached| GIT[git worktree add]
    FAA --> TM[TabManager addWorkspace]
    FAP -->|atomic write| DISK[(fleet-state.json)]
    FE -->|restore on init| DISK
Loading

Reviews (3): Last reviewed commit: "Reuse live workspaces on fleet retry; dr..." | Re-trigger Greptile

Comment on lines +18 to +25
"{{PROMPT}}": shellQuoted(prompt),
"{{TITLE}}": shellQuoted(task.title),
"{{BODY}}": shellQuoted(task.body),
"{{TASK_ID}}": task.id.rawValue,
"{{DIR}}": directory,
"{{BRANCH}}": branch ?? "",
]
var rendered = template

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Unquoted {{DIR}} breaks commands on paths with spaces

{{PROMPT}}, {{TITLE}}, and {{BODY}} are correctly wrapped by shellQuoted(), but {{DIR}} and {{BRANCH}} are inserted verbatim into the shell command. If the user's repo lives in a path with spaces — /Users/Jane Smith/code → worktree at /Users/Jane Smith/code-fleet/task — any template that expands {{DIR}} (e.g. cd {{DIR}} && claude {{PROMPT}}) will split on the space and fail silently. {{BRANCH}} is sanitized by FleetPathSanitizer so the risk is lower there, but {{DIR}} includes the user-supplied repoRoot and is directly unsafe.

Suggested change
"{{PROMPT}}": shellQuoted(prompt),
"{{TITLE}}": shellQuoted(task.title),
"{{BODY}}": shellQuoted(task.body),
"{{TASK_ID}}": task.id.rawValue,
"{{DIR}}": directory,
"{{BRANCH}}": branch ?? "",
]
var rendered = template
let replacements = [
"{{PROMPT}}": shellQuoted(prompt),
"{{TITLE}}": shellQuoted(task.title),
"{{BODY}}": shellQuoted(task.body),
"{{TASK_ID}}": task.id.rawValue,
"{{DIR}}": shellQuoted(directory),
"{{BRANCH}}": branch.map(shellQuoted) ?? "",
]

Comment thread Sources/Fleet/FleetWorkstreamTap.swift Outdated
Comment on lines +7 to +8
enum FleetWorkstreamTap {
/// Forwards one hook event when Fleet already owns the event workspace.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Caseless enum used as a static-function namespace

FleetWorkstreamTap, FleetControlSocketMapping (Sources/Fleet/FleetControlSocketMapping.swift), and FleetPromptTemplate (Packages/macOS/CmuxFleet/Sources/CmuxFleet/Engine/FleetPromptTemplate.swift) are all caseless enums whose entire public surface is static functions. The cmux-no-ambient-global-state rule flags this pattern. The canonical fix is to represent the mapping as free functions (for nonisolated utilities) or as methods on the owning type — e.g. FleetWorkstreamTap.handle could be a method on FleetAppHost or FleetEngine, and the pure mapping utilities in FleetControlSocketMapping could be extensions on FleetTaskState/ControlFleetTaskStateName. The pattern appears in all three files.

Rule Used: Flag new ambient global state in production Swift:... (source)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

fleetID.map(FleetID.init) is ambiguous across the three String
initializers; name init(rawValue:) explicitly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@blacksmith-sh

This comment has been minimized.

The package-conventions-lint namespace-type rule rejects all-static
public types. Convert the caseless enum to a Sendable struct with a
public init and instance render(...), matching FleetPathSanitizer and
FleetBackoff in this package.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment on lines +55 to +70
func load() -> FleetPersistedState? {
guard let fileURL,
let data = try? Data(contentsOf: fileURL)
else { return nil }
do {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
let state = try decoder.decode(FleetPersistedState.self, from: data)
lastData = data
return state
} catch {
#if DEBUG
cmuxDebugLog("fleet.persistence.load.failed \(error.localizedDescription)")
#endif
return nil
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Blocking disk I/O on @MainActor in both save() and load()

load() calls Data(contentsOf: fileURL) + JSONDecoder.decode synchronously on the main actor. The first call to any fleet control-socket command (e.g. fleet.create, fleet.list) reaches FleetAppHost.shared.engine, which triggers FleetEngine.init → restore() → persistence.load(). That entire chain runs synchronously on the main actor, so a cold-start fleet command blocks the main thread for a full file read + JSON decode while the socket worker is waiting.

save() has the same problem in the hot direction: FileManager.createDirectory + data.write(to:options:.atomic) run synchronously on @MainActor on every task state transition. With several concurrent tasks, this can be called multiple times per second, each time blocking the main thread for an atomic rename.

The FleetPersisting protocol is marked @MainActor, which forces all conformers to block the main thread for I/O. Making load() async (or @concurrent) and moving the write inside a Task.detached(priority: .utility) would keep disk work off the main actor; the main-actor portion only needs to apply the decoded state.

Rule Used: Flag production Swift that reads, decodes, or scan... (source)

austinywang and others added 5 commits July 5, 2026 20:27
Regression tests first (red) per repo policy:
- reconcile must deliver prChanged to .failed tasks so an open PR
  rescues them to .awaitingReview and a merged PR to .done with
  workspace cleanup; today reconcile skips terminal tasks entirely.
- FleetPromptTemplate must shell-quote {{DIR}} and {{BRANCH}} so
  paths with spaces cannot split the rendered agent command.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The reconcile pass skipped every terminal task, so the supervisor's
prChanged rescue transitions (failed -> awaitingReview on an open PR,
failed -> done on a merged PR) were unreachable: a task that failed
before its PR badge populated stayed failed forever. Keep probing
pull-request status for .failed tasks that still have a workspace while
still skipping .done/.cancelled, and keep the workspace-gone
cancellation for non-terminal tasks only.

FleetPromptTemplate now shell-quotes {{DIR}} and {{BRANCH}} like the
other string placeholders so a repository path with spaces cannot split
the rendered agent command.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
FleetControlSocketMapping and FleetWorkstreamTap were caseless enums
whose whole surface was static functions (static-as-namespace policy).
Express the socket wire mapping as extensions on the mapped types
(FleetTaskState/ControlFleetTaskStateName properties and
ControlFleetSnapshot/ControlFleetTaskSnapshot inits) and move the
workstream tap onto FleetAppHost as an instance method, dropping the
now-redundant liveHost/hasLiveEngine statics. No behavior change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Red per repo regression policy:
- retrying a failed task whose workspace still exists must not
  provision a duplicate workspace; the relaunch reuses the original
  workspaceID and only resends the agent command.
- when the old workspace is gone and a replacement is provisioned,
  the old taskIDByWorkspaceID mapping must be dropped so stale hooks
  for the replaced workspace no longer bind to the task.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Green for the preceding red tests: re-provisioning a task whose
workspace still exists reuses it (no duplicate workspace, command
resent in place), and when a replacement workspace is created the old
taskIDByWorkspaceID entry is removed so stale hooks cannot bind.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

This branch was successfully deployed

1 active deployment
Preview – cmux — fdb31ea9 Deployed Jul 6, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

stale-revisit Closed after 30+ days without activity; preserved for possible revisit or reopening.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants