Skip to content

fix(daemon): a full disk under cmux-tui offers Restart with the reason, keeps starting it, and a refused New Workspace says why (restore fuzz h2, h3) - #30496

Merged
lawrencecchen merged 10 commits into
feat-cmux-nextfrom
fuzz-red/restore-h2h3
Nov 1, 2026
Merged

lawrencecchen merged 10 commits into
feat-cmux-nextfrom
fuzz-red/restore-h2h3

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Nov 1, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Restore fuzz (chaos diskfull) found two ways a full disk under cmux-tui's state leaves the person with nothing to do and no reason:

  • h2 (STATUS 3149). The local cmux-tui died while its disk was full. Its reconnect's server ensure failed. After the disk had space again, the app started no new daemon by itself (none within 60 s), and in 2 of 3 runs it did not offer Restart cmux-tui. A window that showed content had no Restart control at all. Pressing Restart (debug.daemon {action: restart}) recovered in about 1 s.
  • h3 (STATUS 3213). On a full disk the daemon refused the person's New Workspace (operation.failed, "database or disk is full"). The app correctly kept no row, but showed no refusal, toast or dialog.

After this change:

  • The app keeps starting its local daemon. The local reconnect and first connect (RetryPolicy.localReconnect, .localFirstConnect: keepsTrying) keep one attempt per 30 s backoff cap past their 10 timed retries, on the injected clock, instead of waiting for an event only. A disk that has space again sends no event. App activation, Retry Now and Restart still wake the loop at once and start the spacing over. Remote machines and every other loop keep event-only waits.
  • Every failed start offers Restart cmux-tui, with the reason. EnsureOutcome records each failed ensure as a DaemonStartFailure (disk_full when the error says so or the state volume has under 64 MB free, else failed). DaemonRecovery.offersRestart is true for any failed start, not only a stuck owner. The automatic restart still needs stuck-owner failures (UnresponsiveOwnerWatch is unchanged). debug.daemon reports start_failure and start_failure_detail.
  • A window that shows content gets the way out. DaemonRecoveryNotice shows one until-closed toast (the standard CmuxToastCenter, so the toast's own motion gate and keys apply) in the workspace window the key or main window acts for. It says why cmux-tui is not running ("cmux-tui could not start: the disk is full. Free some space and cmux starts it again by itself.", or a short generic reason), and its button is Restart cmux-tui, which asks the same CmuxDialogSpec confirm as the launch window's button. It ends when the daemon is back, a restart starts or the offer ends. A cancelled confirm shows it again. Closed by the person, it stays closed until the daemon connects again.
  • A refused person's New Workspace shows one refusal. It reads "New Workspace did not apply: the disk is full" through the existing action.effect.failed path. A scripted run (CLI, socket, MCP) still gets the typed daemon error and shows no HUD (ActionRegistry.trackOrReport(_:as:)).

No daemon state is deleted or reset.

Cause (on the base, 88dcb89)

  • h2, no automatic restart: RetryPacer.swift:92 returns nil after RetryPolicy.reconnect's 10 timed retries (RetryPacer.swift:32). The reconnect loop then waits for a socket change or an explicit event only (DaemonConnection.swift:400).
  • h2, no Restart offer: DaemonRecovery.swift:116 offers Restart only when the last ensure looked stuck or the daemon does not answer. DaemonLauncher+Replace.swift:84 counts only restore_timeout, timedOut and server.unavailable as stuck, so a spawn that failed on a full disk offered nothing.
  • h2, no control over content: WindowController.swift:240 shows the connecting view, which holds the only Retry and Restart controls, only when the window has no content.
  • h3: AppActions+Workspaces.swift:163 handed the create's failure (:175) to registry.track, which keeps it only for a capturing caller (ActionRegistry+Work.swift:75-76). A keyboard, menu or palette run captures nothing, so the failure was dropped. The red below proves it: the action ran (perform true), the stand-in daemon received create-workspace and refused it, and the refusal HUD count stayed 0.

Changed assertion in an existing test

DaemonRestartTests.restartIsOfferedOnlyAfterAPlainEnsureFailedAndReplacesThenConnectsAgain asserted at :176 that Restart is hidden after a start failure that is not a stuck owner (server.spawn_failed). That assertion encoded this bug: a full disk fails exactly that way and left nothing to press. It now expects Restart shown. Its wait after Retry Now waits for Retry Now's own ensure, because Restart already shows before the press. The rest of the test (the replace runs the same ensure plus --replace-unresponsive, then connects again with a plain ensure, one replace only) is unchanged and passes.

Testing

Reds (each fails on its own assertion on feat with the tests only):

  • h2: CmuxNextAppTests.DaemonRecoveryAfterFailedStartsTests at 4d96f95, job ebaff5a8a29162f4c7189400: offersRestart false after a disk-full ensure (:103), debug.daemon offers_restart false (:115), no reason exposed (:117); the reconnect parked for an event after 10 failed starts, did not reconnect, and stopped at 11 starts (:208, :212, :213).
  • h3: CmuxNextAppTests.NewWorkspaceDiskFullRefusalTests at 71665d8 (branch fuzz-red/restore-h3-red), job b0cc51eb433543f2a3f59099: the person's create reached the daemon and was refused, and no refusal showed (:91, :93, :95, :96). The scripted test passes on the base, as it should: it guards that a caller keeps the typed error.

Greens: all 8 suites below passed (8 passed, 0 failed), 5 of 5, at da8798b (head): 4eb957f0ce1924f5d3579bef, 1e64070fbc06abd921ce7893, c97b5b57e4f1019e01d73392, ba699c66be77e730685205db, cd94fc92fe25605088cf1ed5. Suites: DaemonRecoveryAfterFailedStartsTests, DaemonRecoveryNoticeTests (new: a window with content shows the notice with the disk-full reason and Restart; Restart asks first; a cancelled confirm shows it again), NewWorkspaceDiskFullRefusalTests, DaemonRestartTests, DaemonAutoRestartTests, BusyRefusalReconnectTests, RetryPacerSocketBudgetTests, UnresponsiveOwnerWatchTests. The same 8 suites also passed 5 of 5 at 4c8a6c2 (1ec96cb8, 28b51b2e, bde4ea06, 7d87de79, 5fdd58f2), before the last commit replaced English-text assertions with the localized string and the typed start_failure.

DaemonRestartTests alone on the base 88dcb89 passed (job 7f107cbf400e92fe232f0f64): the one red it had at fff3cc7 came from this branch's offer change and is fixed.

Compile: fleet compile-check CmuxNext with tests at da8798b passed (940c168b2ab8f1e126d18a9c); CmuxNext + app at 4c8a6c2 passed (10b6bc8ad7719763d8f0b86d); Release app compile at 4c8a6c2 passed (1c5b9112ecd1b3d74f2464ec). prepush-gate passes on every commit.

Localization audit: three keys, each with all 21 locales its catalog carries, every one translated (no copied English): refusal.daemon.diskFull ("the disk is full", CmuxNextDaemonService/Resources/Refusals.xcstrings), the one disk-full reason shared by h2 and h3; daemon.recovery.diskFull (a format string around it) and daemon.recovery.stopped (CmuxNextDaemonRecovery/Resources/Localizable.xcstrings). scripts/localization_catalog.py check reports no error for these keys. ./scripts/localize-changes cannot prepare CmuxNextDaemonRecovery (the package has several string catalogs), so I wrote those entries by hand. The tool's other reports (debt over the baseline in CmuxNextActions, CmuxNextSettings and other catalogs) are base debt in files this PR does not touch.

Observed / Not run

Observed: the reds and greens above, run on the fleet; the compile-checks; prepush-gate.

Display proof at the head (the real app on a display worker, through the mini scheduler, exclusive host):

  • Display-worker suite job 6bc9adc8699e6e31d2c11401 at da8798b (gui-session-v1, cmuxs-Mac-mini-3, console session, cmux-ci run --display, CMUX_TEST_REQUIRE_GUI=1), exit 0. All 8 PR suites passed, 31 tests, none skipped: DaemonRecoveryAfterFailedStartsTests 2, DaemonRecoveryNoticeTests 1, NewWorkspaceDiskFullRefusalTests 2, DaemonRestartTests 3, DaemonAutoRestartTests 2, BusyRefusalReconnectTests 6, RetryPacerSocketBudgetTests 3, UnresponsiveOwnerWatchTests 12.
  • Build job 74bf492462d9ec4492560f1c (head da8798b). Bundle h2h3fix-da8798ba, commit da8798b.
  • h2: round fgh-h2-head-4202, capture-slot lease slot-cmux19-2ee9e0291d4a on cmux19-perf, restore fuzz chaos diskfull with daemon kills, --visible, harness 855dff1 (fuzz(restore): diskfull app create probe runs New Workspace as a person #30531). 23 judged, 13 daemon kills, 0 violations, app_daemon_differ 0. After a daemon kill on the full disk, the window showed "disconnected" with its content (66 samples, no blank pane). When the space came back, the daemon came back by itself in 18.2 s with no Restart press. On the base it never did.
  • h3: same build, round fgh-h3-head-4203, lease slot-cmux22-0c6402a26e98 on cmux22-perf. The person's New Workspace (debug.toast perform newTab, origin user) on the full disk showed exactly one refusal, "New Workspace did not apply: the disk is full". No ghost workspace stayed, now or after a relaunch. The 4202 probe gave the same result.
  • STATUS 3393.

Not run:

  • A real capture of the new notice.
  • DaemonCrashLoopTests (needs the real cmux-tui binary; keepsTrying keeps its crash-loop socket limit, but I did not run it).
  • The tests of every other ActionRegistry user.

Read only: the h2 fuzz runs that failed with server.unavailable (1 of 3) also offered Restart before this change; the cause of that one ensure failure is not known.

Changelog

Fixed: When cmux-tui stops and cannot start again (for example because the disk is full), cmux says why, offers Restart cmux-tui in the open window, and starts it again by itself once it can; a New Workspace refused on a full disk now says "did not apply: the disk is full"

Proof

No capture: the new notice is a standard CmuxToastCenter toast (no new view or style). It shows only after the local cmux-tui died on a full disk, and the capture minis cannot reach that state yet. DaemonRecoveryNoticeTests checks its text, action and confirm. The lead's diskfull fuzz round on a fleet build of this head checks it in the real app.


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.


Summary by cubic

Fixes two restore-fuzz findings where a full disk under cmux-tui's state left the person with no visible state and no way out: the app now keeps starting its local daemon, offers Restart cmux-tui with the reason in any open window, and shows a refusal when a person's New Workspace is refused.

Daemon recovery (h2)

  • The local reconnect and first connect keep one attempt per 30 s backoff cap past their timed retries, so the daemon restarts by itself once the disk has space again.
  • Every failed start offers Restart cmux-tui; a window that shows content gets an until-closed toast naming the reason (a full disk is told apart) with the same Restart confirm as the launch window.
  • debug.daemon reports start_failure and start_failure_detail.

New Workspace refusal (h3)

  • A person's New Workspace refused on a full disk shows one "did not apply: the disk is full" refusal; a scripted run still gets the typed daemon error and no HUD.

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

View guided diff Turn on auto-fix

…-tui offered with the reason after a failed ensure, and the reconnect keeps starting the daemon with backoff

Restore fuzz finding h2 (STATUS 3149, chaos diskfull, feat d1f0e63): the daemon dies on a full
disk, its reconnect's server ensure fails, and when the disk has space again the app neither starts a
new daemon by itself nor (2 of 3) offers Restart cmux-tui.

- restartIsOfferedWithTheReasonWhenTheReconnectsEnsureFailsOnAFullDisk: a stand-in ensure fails with
  server.spawn_failed (no space left on device); offersRestart stays false because it is set only for
  a stuck-owner failure (DaemonLauncher.isStuckOwnerFailure), and no reason reaches debug.daemon.
- theAppKeepsStartingTheDaemonWithBackoffAfterAFullDiskAndReconnectsOnceAStartWorks: the app's local
  connection on a ManualClock; 30 failed starts then one that works. RetryPolicy.reconnect spends its
  10 timed retries and parks for an event only, which a disk with space again never sends.

Measurer commit: tests only, no product change.
…ed New Workspace shows one 'did not apply: the disk is full' refusal; a scripted one gets the typed error and no HUD

Restore fuzz finding h3 (STATUS 3213): on a full daemon state disk the daemon refuses the person's
New Workspace (pin-workspace or create-workspace: operation.failed, database or disk is full); the
app keeps no row but the person sees nothing. A stand-in daemon refuses every create and pin that way.

Measurer commit: tests only, no product change.
…Restart with the reason, and the app keeps starting it until it runs

Restore fuzz h2 (STATUS 3149, chaos diskfull): the daemon died on a full disk, its reconnect's
server ensure failed, and when the disk had space again the app neither started a new daemon by
itself nor, in 2 of 3 runs, offered Restart cmux-tui. A window that showed content had no
Restart control at all.

- RetryPolicy.keepsTrying (localReconnect, localFirstConnect): a loop that starts its own peer keeps
  one attempt per backoff cap (30 s, on the injected clock) past its timed budget instead of
  waiting for an event only; an explicit event (activation, Retry Now, Restart) starts the spacing
  over. Remote machines and other loops keep event-only waits.
- EnsureOutcome records every failed ensure (DaemonStartFailure: disk_full when the error says so
  or the state volume has under 64 MB free, else failed). DaemonRecovery offers Restart for any
  failed start, not only a stuck owner; the automatic restart still needs stuck-owner failures.
- The reason reaches the person: the launch window's status line names a full disk, and a window
  that shows content gets DaemonRecoveryNotice, an until-closed toast with the reason and Restart
  cmux-tui (same confirm as the launch window's button). debug.daemon reports start_failure and
  start_failure_detail.
- Two new strings in CmuxNextDaemonRecovery's catalog, all 21 locales it carries.
- DaemonRestartTests asserted Restart hidden after a non-stuck start failure: that assertion
  encoded this bug and now expects Restart shown.

No daemon state is deleted or reset.
…did not apply: the disk is full"; a scripted run keeps the typed error

Restore fuzz h3 (STATUS 3213): the daemon refused the person's create (pin-workspace or
create-workspace: operation.failed, database or disk is full) and the person saw nothing, because
newWorkspace handed its failure to registry.track, which keeps it only for a capturing caller.

- ActionRegistry.trackOrReport(_:as:): a capturing caller (CLI, socket, MCP) gets the typed failure;
  a run nobody waits on shows it once where a failed change shows ("<action> did not apply:
  <reason>"), unless the person already saw it. newWorkspace uses it.
- DaemonError.isDiskFull (SQLite "database or disk is full", ENOSPC; in the message, the details'
  reason or a failed server ensure), shared with DaemonStartFailure.
- ActionWorkFailure.shownReason: the person's words where the message is the daemon's own error. A
  daemon failure on a full disk gets RefusalStrings.diskFull ("the disk is full"), the one disk-full
  reason; cmux-tui's start notice (h2) now formats the same reason.
- refusal.daemon.diskFull in Refusals.xcstrings (21 locales); daemon.recovery.diskFull becomes a
  format string around it (21 locales).
… the notice is internal and holds its daemon

Review of the h2 fix before its first build: no all-static public type (namespace convention), no
weak capture inside the notice's Sendable observation (the app's local daemon lives as long as the
app; a test stops the notice), DaemonStartFailure needs no nonisolated in a module that is not
main-actor by default.
…he daemon, and names the commands the stand-in saw

The first fleet run (job 17626e37) failed the person test on its own assertion, but the scripted
test saw no failure at all in 0.1 s: these checks tell a create that never reached the daemon from a
failure the app dropped.
…n the main actor (fleet compile, job b7b1b48e)
…the app does at launch

Job 271c01eb showed perform("newTab") returned false in both tests: AppServices alone binds no
action, so the earlier red (17626e37) failed without the action running at all.
…start already shows before it (h2: every failed start offers it)

Job d6feab63: the wait for Restart shown returned at once (it shows since the first failed ensure),
so the count was read before Retry Now's ensure ran.
…_failure, not English text (REVIEW.md rule 1)

@cubic-dev-ai cubic-dev-ai 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.

10 issues found across 23 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="Packages/macOS/CmuxNext/Sources/CmuxNextDaemonRecovery/DaemonRecoveryNotice.swift">

<violation number="1" location="Packages/macOS/CmuxNext/Sources/CmuxNextDaemonRecovery/DaemonRecoveryNotice.swift:123">
P2: If no visible workspace window exists when the offer starts, `show` returns without a toast and nothing retries it. The notice then never appears, even after a window opens while the daemon is still down. Retry `show` when a workspace window becomes available, for example on window key or visibility changes or on app activation.</violation>

<violation number="2" location="Packages/macOS/CmuxNext/Sources/CmuxNextDaemonRecovery/DaemonRecoveryNotice.swift:136">
P2: `CmuxToastCenter` reports `.closed` for both the toast's close button and a closing host window. This sets the app-wide `closedByPerson` flag when a workspace window closes, suppressing Restart in later windows until the daemon reconnects; distinguish a person closing the toast from window teardown.</violation>

<violation number="3" location="Packages/macOS/CmuxNext/Sources/CmuxNextDaemonRecovery/DaemonRecoveryNotice.swift:138">
P2: When the recovery toast is oldest and a fourth toast arrives, `CmuxToastCenter` removes it as `.replaced`; this callback only clears `handle`, while the watcher observes daemon state only. The Restart action then stays absent until daemon state changes; reapply the current offer after `.replaced`.</violation>
</file>

<file name="Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/DaemonRestartTests.swift">

<violation number="1" location="Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/DaemonRestartTests.swift:176">
P3: The struct doc at the top of this file still says Restart is offered only after a stuck-owner ensure failure. This change makes Restart appear after any failed start, so update that doc to match.</violation>
</file>

<file name="Packages/macOS/CmuxNext/Sources/CmuxNextDaemon/Launch/DaemonStartFailure.swift">

<violation number="1" location="Packages/macOS/CmuxNext/Sources/CmuxNextDaemon/Launch/DaemonStartFailure.swift:32">
P2: The disk-full markers omit EDQUOT. cmux-tui's `is_disk_full` also matches "disk quota exceeded" and "disc quota exceeded", so a quota-full start is reported as a generic failure. Add those markers.</violation>

<violation number="2" location="Packages/macOS/CmuxNext/Sources/CmuxNextDaemon/Launch/DaemonStartFailure.swift:43">
P2: Any start failure becomes `.diskFull` when free space is under 64 MB, even when the error has nothing to do with space. Consider applying the free-space check only to ambiguous failures, such as the "owner did not become ready" case, so a missing binary or a stuck owner keeps the generic reason.</violation>
</file>

<file name="Packages/macOS/CmuxNext/Sources/CmuxNextDaemon/Launch/DaemonLauncher+Replace.swift">

<violation number="1" location="Packages/macOS/CmuxNext/Sources/CmuxNextDaemon/Launch/DaemonLauncher+Replace.swift:62">
P2: `replaceUnresponsive()` records a success but not a failed start. A failure from Restart's own `server ensure` never reaches `ensureOutcome`, so no failure category or free-space check is recorded for it. Record the failure in the catch path here as `ensureRecordingOutcome` does, or the full-disk reason stays hidden behind the generic "failed" status.</violation>

<violation number="2" location="Packages/macOS/CmuxNext/Sources/CmuxNextDaemon/Launch/DaemonLauncher+Replace.swift:62">
P2: When `stateDirectory` is nil, this checks `runtimeBase` instead of cmux-tui’s default state volume, so a full state disk can be classified as a generic failure. Check the default state root’s volume in this case.</violation>
</file>

<file name="Packages/macOS/CmuxNext/Sources/CmuxNextDaemon/Connection/RetryPacer.swift">

<violation number="1" location="Packages/macOS/CmuxNext/Sources/CmuxNextDaemon/Connection/RetryPacer.swift:107">
P3: `failed()` now keeps returning a delay past `timedRetries` for `keepsTrying`, but its method and wait-helper docs still promise event-only waiting. Update those docs to describe the exception.</violation>
</file>

<file name="Packages/macOS/CmuxNext/Sources/CmuxNextDaemonRecovery/DaemonRecoveryControls.swift">

<violation number="1" location="Packages/macOS/CmuxNext/Sources/CmuxNextDaemonRecovery/DaemonRecoveryControls.swift:77">
P3: A failed Restart can leave `restartFailure` set while a later ensure still reports a full disk, so this branch hides the more actionable disk-full status. Check `startFailure?.isDiskFull` before showing the generic restart error.</violation>
</file>

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic


private func show(_ toast: CmuxToast) {
if let handle, !handle.isDismissed, handle.toast == toast { return }
guard let window = window() else { return }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: If no visible workspace window exists when the offer starts, show returns without a toast and nothing retries it. The notice then never appears, even after a window opens while the daemon is still down. Retry show when a workspace window becomes available, for example on window key or visibility changes or on app activation.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Packages/macOS/CmuxNext/Sources/CmuxNextDaemonRecovery/DaemonRecoveryNotice.swift, line 123:

<comment>If no visible workspace window exists when the offer starts, `show` returns without a toast and nothing retries it. The notice then never appears, even after a window opens while the daemon is still down. Retry `show` when a workspace window becomes available, for example on window key or visibility changes or on app activation.</comment>

<file context>
@@ -0,0 +1,148 @@
+
+    private func show(_ toast: CmuxToast) {
+        if let handle, !handle.isDismissed, handle.toast == toast { return }
+        guard let window = window() else { return }
+        // The same id replaces a notice that shows in that window (a new reason), in place.
+        if let handle, !handle.isDismissed { end() }
</file context>

/// `error`'s failure; `freeBytes` is the free space on the state's volume (nil: unknown).
init(_ error: any Error, freeBytes: UInt64?) {
let detail = String(describing: error)
let lowSpace = freeBytes.map { $0 < Self.lowSpaceBytes } ?? false

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: Any start failure becomes .diskFull when free space is under 64 MB, even when the error has nothing to do with space. Consider applying the free-space check only to ambiguous failures, such as the "owner did not become ready" case, so a missing binary or a stuck owner keeps the generic reason.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Packages/macOS/CmuxNext/Sources/CmuxNextDaemon/Launch/DaemonStartFailure.swift, line 43:

<comment>Any start failure becomes `.diskFull` when free space is under 64 MB, even when the error has nothing to do with space. Consider applying the free-space check only to ambiguous failures, such as the "owner did not become ready" case, so a missing binary or a stuck owner keeps the generic reason.</comment>

<file context>
@@ -0,0 +1,81 @@
+    /// `error`'s failure; `freeBytes` is the free space on the state's volume (nil: unknown).
+    init(_ error: any Error, freeBytes: UInt64?) {
+        let detail = String(describing: error)
+        let lowSpace = freeBytes.map { $0 < Self.lowSpaceBytes } ?? false
+        self.init(kind: Self.saysDiskFull(detail) || lowSpace ? .diskFull : .failed, detail: detail)
+    }
</file context>

static let lowSpaceBytes: UInt64 = 64 << 20

/// The words a failure caused by a full disk carries (ENOSPC, SQLite's SQLITE_FULL).
private static let diskFullMarkers = ["no space left on device", "os error 28", "enospc", "disk is full", "disk full"]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: The disk-full markers omit EDQUOT. cmux-tui's is_disk_full also matches "disk quota exceeded" and "disc quota exceeded", so a quota-full start is reported as a generic failure. Add those markers.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Packages/macOS/CmuxNext/Sources/CmuxNextDaemon/Launch/DaemonStartFailure.swift, line 32:

<comment>The disk-full markers omit EDQUOT. cmux-tui's `is_disk_full` also matches "disk quota exceeded" and "disc quota exceeded", so a quota-full start is reported as a generic failure. Add those markers.</comment>

<file context>
@@ -0,0 +1,81 @@
+    static let lowSpaceBytes: UInt64 = 64 << 20
+
+    /// The words a failure caused by a full disk carries (ENOSPC, SQLite's SQLITE_FULL).
+    private static let diskFullMarkers = ["no space left on device", "os error 28", "enospc", "disk is full", "disk full"]
+
+    /// Whether `text` (an error's words) says the disk is full.
</file context>
Suggested change
private static let diskFullMarkers = ["no space left on device", "os error 28", "enospc", "disk is full", "disk full"]
private static let diskFullMarkers = ["no space left on device", "os error 28", "enospc", "disk is full", "disk full", "disk quota exceeded", "disc quota exceeded"]

// A cancelled ensure (its connection torn down, or shutdown) says nothing about the owner.
if !Task.isCancelled, !(error is CancellationError) { ensureOutcome.note(error) }
if !Task.isCancelled, !(error is CancellationError) {
let free = DaemonStartFailure.freeBytes(at: configuration.stateDirectory ?? configuration.runtimeBase)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: replaceUnresponsive() records a success but not a failed start. A failure from Restart's own server ensure never reaches ensureOutcome, so no failure category or free-space check is recorded for it. Record the failure in the catch path here as ensureRecordingOutcome does, or the full-disk reason stays hidden behind the generic "failed" status.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Packages/macOS/CmuxNext/Sources/CmuxNextDaemon/Launch/DaemonLauncher+Replace.swift, line 62:

<comment>`replaceUnresponsive()` records a success but not a failed start. A failure from Restart's own `server ensure` never reaches `ensureOutcome`, so no failure category or free-space check is recorded for it. Record the failure in the catch path here as `ensureRecordingOutcome` does, or the full-disk reason stays hidden behind the generic "failed" status.</comment>

<file context>
@@ -58,7 +58,10 @@ extension DaemonLauncher {
             // A cancelled ensure (its connection torn down, or shutdown) says nothing about the owner.
-            if !Task.isCancelled, !(error is CancellationError) { ensureOutcome.note(error) }
+            if !Task.isCancelled, !(error is CancellationError) {
+                let free = DaemonStartFailure.freeBytes(at: configuration.stateDirectory ?? configuration.runtimeBase)
+                ensureOutcome.note(error, freeBytes: free)
+            }
</file context>

}
shown.onDismiss = { [weak self, weak shown] reason in
guard let self, let shown, handle === shown else { return }
handle = nil

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: When the recovery toast is oldest and a fourth toast arrives, CmuxToastCenter removes it as .replaced; this callback only clears handle, while the watcher observes daemon state only. The Restart action then stays absent until daemon state changes; reapply the current offer after .replaced.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Packages/macOS/CmuxNext/Sources/CmuxNextDaemonRecovery/DaemonRecoveryNotice.swift, line 138:

<comment>When the recovery toast is oldest and a fourth toast arrives, `CmuxToastCenter` removes it as `.replaced`; this callback only clears `handle`, while the watcher observes daemon state only. The Restart action then stays absent until daemon state changes; reapply the current offer after `.replaced`.</comment>

<file context>
@@ -0,0 +1,148 @@
+        }
+        shown.onDismiss = { [weak self, weak shown] reason in
+            guard let self, let shown, handle === shown else { return }
+            handle = nil
+            if reason == .closed { closedByPerson = true }
+        }
</file context>
Suggested change
handle = nil
handle = nil
if reason == .replaced {
Task { @MainActor [weak self] in
guard let self, self.observation != nil else { return }
self.apply(self.current)
}
}

Comment on lines +136 to +140
shown.onDismiss = { [weak self, weak shown] reason in
guard let self, let shown, handle === shown else { return }
handle = nil
if reason == .closed { closedByPerson = true }
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: CmuxToastCenter reports .closed for both the toast's close button and a closing host window. This sets the app-wide closedByPerson flag when a workspace window closes, suppressing Restart in later windows until the daemon reconnects; distinguish a person closing the toast from window teardown.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Packages/macOS/CmuxNext/Sources/CmuxNextDaemonRecovery/DaemonRecoveryNotice.swift, line 136:

<comment>`CmuxToastCenter` reports `.closed` for both the toast's close button and a closing host window. This sets the app-wide `closedByPerson` flag when a workspace window closes, suppressing Restart in later windows until the daemon reconnects; distinguish a person closing the toast from window teardown.</comment>

<file context>
@@ -0,0 +1,148 @@
+                apply(current)
+            })
+        }
+        shown.onDismiss = { [weak self, weak shown] reason in
+            guard let self, let shown, handle === shown else { return }
+            handle = nil
</file context>
Suggested change
shown.onDismiss = { [weak self, weak shown] reason in
guard let self, let shown, handle === shown else { return }
handle = nil
if reason == .closed { closedByPerson = true }
}
shown.onDismiss = { [weak self, weak shown, weak window] reason in
guard let self, let shown, handle === shown else { return }
handle = nil
guard reason == .closed else { return }
Task { @MainActor [weak self, weak window] in
guard let self, window?.isVisible == true else { return }
closedByPerson = true
}
}

// A cancelled ensure (its connection torn down, or shutdown) says nothing about the owner.
if !Task.isCancelled, !(error is CancellationError) { ensureOutcome.note(error) }
if !Task.isCancelled, !(error is CancellationError) {
let free = DaemonStartFailure.freeBytes(at: configuration.stateDirectory ?? configuration.runtimeBase)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: When stateDirectory is nil, this checks runtimeBase instead of cmux-tui’s default state volume, so a full state disk can be classified as a generic failure. Check the default state root’s volume in this case.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Packages/macOS/CmuxNext/Sources/CmuxNextDaemon/Launch/DaemonLauncher+Replace.swift, line 62:

<comment>When `stateDirectory` is nil, this checks `runtimeBase` instead of cmux-tui’s default state volume, so a full state disk can be classified as a generic failure. Check the default state root’s volume in this case.</comment>

<file context>
@@ -58,7 +58,10 @@ extension DaemonLauncher {
             // A cancelled ensure (its connection torn down, or shutdown) says nothing about the owner.
-            if !Task.isCancelled, !(error is CancellationError) { ensureOutcome.note(error) }
+            if !Task.isCancelled, !(error is CancellationError) {
+                let free = DaemonStartFailure.freeBytes(at: configuration.stateDirectory ?? configuration.runtimeBase)
+                ensureOutcome.note(error, freeBytes: free)
+            }
</file context>

// A start failure that is not a stuck owner (a spawn that failed, as on a full disk) offers
// Restart too: no daemon runs, so the person needs a way out (restore fuzz h2). Until then
// this asserted Restart hidden here, which left a full disk with nothing to press.
await Self.wait { Self.shown("daemon.restart", in: view) != nil }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: The struct doc at the top of this file still says Restart is offered only after a stuck-owner ensure failure. This change makes Restart appear after any failed start, so update that doc to match.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/DaemonRestartTests.swift, line 176:

<comment>The struct doc at the top of this file still says Restart is offered only after a stuck-owner ensure failure. This change makes Restart appear after any failed start, so update that doc to match.</comment>

<file context>
@@ -170,17 +170,21 @@ struct DaemonRestartTests {
+        // A start failure that is not a stuck owner (a spawn that failed, as on a full disk) offers
+        // Restart too: no daemon runs, so the person needs a way out (restore fuzz h2). Until then
+        // this asserted Restart hidden here, which left a full disk with nothing to press.
+        await Self.wait { Self.shown("daemon.restart", in: view) != nil }
         let retry = Self.shown("daemon.retryNow", in: view)
         #expect(retry != nil)
</file context>

wokeBySocket = false
return failures > policy.timedRetries ? nil : backoff.next()
// Past the budget a loop that keeps trying stays at the backoff's cap (`Backoff.next` caps it).
return failures > policy.timedRetries && !policy.keepsTrying ? nil : backoff.next()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: failed() now keeps returning a delay past timedRetries for keepsTrying, but its method and wait-helper docs still promise event-only waiting. Update those docs to describe the exception.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Packages/macOS/CmuxNext/Sources/CmuxNextDaemon/Connection/RetryPacer.swift, line 107:

<comment>`failed()` now keeps returning a delay past `timedRetries` for `keepsTrying`, but its method and wait-helper docs still promise event-only waiting. Update those docs to describe the exception.</comment>

<file context>
@@ -89,7 +103,8 @@ public struct RetryPacer: Sendable {
         wokeBySocket = false
-        return failures > policy.timedRetries ? nil : backoff.next()
+        // Past the budget a loop that keeps trying stays at the backoff's cap (`Backoff.next` caps it).
+        return failures > policy.timedRetries && !policy.keepsTrying ? nil : backoff.next()
     }
 
</file context>

Comment on lines +77 to +78
if recovery.restartFailure != nil { return DaemonRecoveryStrings.failed }
return recovery.startFailure?.isDiskFull == true ? DaemonRecoveryStrings.diskFull : nil

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: A failed Restart can leave restartFailure set while a later ensure still reports a full disk, so this branch hides the more actionable disk-full status. Check startFailure?.isDiskFull before showing the generic restart error.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Packages/macOS/CmuxNext/Sources/CmuxNextDaemonRecovery/DaemonRecoveryControls.swift, line 77:

<comment>A failed Restart can leave `restartFailure` set while a later ensure still reports a full disk, so this branch hides the more actionable disk-full status. Check `startFailure?.isDiskFull` before showing the generic restart error.</comment>

<file context>
@@ -37,27 +55,33 @@ enum DaemonRestartPrompt {
     static func status(_ recovery: DaemonRecovery) -> String? {
         if recovery.isRestarting { return DaemonRecoveryStrings.restarting }
-        return recovery.restartFailure == nil ? nil : DaemonRecoveryStrings.failed
+        if recovery.restartFailure != nil { return DaemonRecoveryStrings.failed }
+        return recovery.startFailure?.isDiskFull == true ? DaemonRecoveryStrings.diskFull : nil
     }
</file context>
Suggested change
if recovery.restartFailure != nil { return DaemonRecoveryStrings.failed }
return recovery.startFailure?.isDiskFull == true ? DaemonRecoveryStrings.diskFull : nil
if recovery.startFailure?.isDiskFull == true { return DaemonRecoveryStrings.diskFull }
if recovery.restartFailure != nil { return DaemonRecoveryStrings.failed }
return nil

@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Review (claude-review-subagent): SHIP at da8798b

Correctness first. I read the diff and the head files in a detached worktree at da8798b. I found no blocking defect. The follow-ups at the end do not block this PR.

1. Binding rules

  • Rule 1 (reason and offer): every failed ensure that is not cancelled is recorded with a typed DaemonStartFailure (DaemonLauncher+Replace.swift:61-63, DaemonStartFailure.swift). offersRestart = launcher != nil && (startFailure != nil || daemonNotAnswering) (DaemonRecovery.swift:123). A successful ensure clears it (noteStart, :114). The reconnect runs an ensure before every connect, so a connected daemon has no offer. The notice hides while isRestarting (DaemonRecoveryNotice.swift:101) and while the connection is connected or connecting (:95-99). The launch window's button shows while a restart runs, as on the base. The automatic restart still requires a stuck owner (UnresponsiveOwnerWatch is unchanged and still gets every ensure).
  • Rule 2 (no parking): keepsTrying makes isExhausted false and failed() returns backoff.next(), capped at 30 s with ±20% jitter (RetryPacer.swift:77, :107, Backoff.swift:45-50). The timer is the injected clock's DemandTimer in RetryWake.awaitWake. That is one server ensure about every 30 s. There is no busy loop and the timer count is bounded. Only the local paths use it: localConfiguration (DaemonService+LaunchSnapshot.swift:107) and the local first connect (DaemonService.swift:261, :272). Remote start(remote:) keeps the default policy. An .event wake resets the failures and the backoff (RetryPacer.swift:157). The local events are app activation, Retry Now and Restart (DaemonService.swift:118, DaemonRecovery.swift:147/170), so the reset rate is bounded. The crash-loop socket count is kept. DaemonCrashLoopTests uses the default .reconnect, so this change does not affect it.
  • Rule 3 (full disk): ENOSPC/SQLITE_FULL words, or < 64 MB free on the state volume, give disk_full. The person reads daemon.recovery.diskFull around RefusalStrings.diskFull. statfs runs only after a failed ensure, in the non-main-actor daemon module.
  • Rule 4 (person vs script): trackOrReport reads isCapturingWork in the handler's turn (ActionRegistry+Effects.swift:122-129). A capturing caller gets track(task) and the typed error. Any other run shows one action.effect.failed notice through remoteFailureObserver, the path that start/trackRollingBack use, and it obeys failure.shown. shownReason changes only the person's text. message stays the daemon's error for callers.
  • Rule 5 (data): there is no new delete or reset. Restart is the existing --replace-unresponsive. It stops only an owner that does not answer, and a confirm comes first.
  • Main thread: the new paths do not block. The notice's work is one observation step per state change. The registry Task awaits off the turn.

2. Notice UI. It is a standard CmuxToastCenter toast (DaemonRecoveryNotice.swift:116). Motion goes through CmuxToastMotion, fonts are Typography.body, and the accessibility label and identifiers come from CmuxToastView. The confirm is the existing CmuxDialogSpec (DaemonRecoveryControls.swift:416-428). It uses no NSAlert/.alert, no raw animation and no literal font. No capture: <why> is one of the three forms that CLAUDE.md and scripts/ci/pr_capture_check.py accept. The reason is specific (no new view or style, and the capture minis cannot reach the state), so it is acceptable. A capture is not required.

3. DaemonRestartTests. The change is the minimum. One == nil became != nil, there is a wait before it, and the post-Retry-Now wait now waits for Retry Now's own ensure (DaemonRestartTests.swift:187), because Restart already shows. "The press replaces and connects again" is unchanged: same ensure plus --replace-unresponsive, then a plain ensure, one replace only (:191-207).

4. Localization. I parsed the catalogs myself. refusal.daemon.diskFull, daemon.recovery.diskFull and daemon.recovery.stopped each have all 21 locales of their catalog. Every entry is translated, no entry copies English, and the %@ count matches in every locale. The tests assert RefusalStrings.diskFull, DaemonRecoveryStrings.diskFull and typed start_failure == "disk_full", not English. The remaining text assertions check the stand-in's own codes or daemon text (server.spawn_failed, the scripted caller's database or disk is full), not app copy.

  • Decision on raw English daemon text for other failures: acceptable, not a required fix for this PR. The localization rules cover app string literals. Here the frame (action.effect.failed) and the action title are localized, and the rest is another process's error. It is also the app-wide contract today: every action's failed remote half shows failure.message through the same failedText (ActionRegistry+Effects.swift:136), and RefusalStrings.describe falls back to String(describing:) (RefusalStrings.swift:137). A generic reason for newTab only would split one shared path. The right fix is one change in failedText for all actions (follow-up below).

5. Shared behavior. The menu, shortcut, palette, sidebar + and cmux.newWorkspace button all run registry.perform("newTab") without capture, so they show the refusal. The socket action.run/batch, the CLI and MCP go through performActionTracked → capturingRun (RegistryControlBridge.swift:170-173), so they get the typed error. Internal capturing callers (trackedWorkEffect, the agent pane) await the captured work and report it themselves. No script-origin newTab runs without capture.

6. Evidence (checked with cmux-ci status/log).

  • Red h2 ebaff5a8: ref 4d96f95, failed at :103 :115 :117 :208 :212 :213, starts 11 vs > 31.
  • Red h3 b0cc51eb: ref 71665d8, failed at :91 :93 :95 :96, HUD count 0.
  • Greens 4eb957f0 1e64070f c97b5b57 ba699c66 cd94fc92: all at ref da8798b, state done, exit 0, the same 8 suites. In the 4eb957f0 log: "Swift test suites: 8 passed, 0 failed".
  • Compiles: 940c168b (head, CmuxNext + tests), 10b6bc8a (CmuxNext + app) and 1c5b9112 (--app --release) at 4c8a6c2 are all done.
  • git diff 4c8a6c2da3ed da8798baaa71 -- Packages/macOS/CmuxNext/Sources is empty. Only the 3 test files changed.
  • PR checks: pr-cloud-backend-check shows fail, but its run was cancelled (infrastructure, no failed step). Rerun the same SHA. The other checks were pending when I looked.

Follow-ups (do not block)

  1. Shared failure text: failedText should map an unknown daemon error to a short localized reason, and keep the raw text as the detail. Today the person can read "New Workspace did not apply: new workspace: create-workspace failed: … [code]". The "new workspace" label (AppActions+Workspaces.swift:175), like "split", is an English literal that this PR makes visible. The disconnected Cmd-N case (DaemonError.notConnected) was silent before and now shows this English text.
  2. The offer follows ensure failures only. A connect or handshake failure after a successful ensure (a daemon that dies on connect) records no startFailure, so it shows no offer and no reason. In that state Restart would only run the same ensure, so I do not block on it.
  3. A window that shows a top page (WindowController.swift:227) before the first connection gets neither the launch controls nor the notice, because the notice waits for startup == .connected (DaemonRecoveryNotice.swift:95). The base has the same gap, and the h2 repro (the daemon dies after it connected) is covered.
  4. There is no unit test for the .event reset of a keepsTrying pacer (RetryPacer.swift:157). Only the integration test covers keepsTrying.
  5. The same h3 bug class remains elsewhere: AppActions+Workspaces.swift:151 (machineNotConnected via track), and about 98 other registry.track(Task sites in CmuxNextApp drop a person-run failure.
  6. Tests: NewWorkspaceDiskFullRefusalTests.settle() uses a fixed 500 ms sleep (:79-80) to look for a second refusal. That is acceptable for an absence check, but it is a sleep. Each test AppServices keeps its DaemonRecoveryNotice observation for the process lifetime, because nothing calls stop(). That is fine for the one app instance.

@lawrencecchen lawrencecchen added the ready-to-land Reviewed at this head; next-autoland merges it into feat-cmux-next when its only reds are base reds label Nov 1, 2026
@github-actions

github-actions Bot commented Nov 1, 2026 •

Copy link
Copy Markdown
Contributor

cmux status (one comment, edited in place; each line is updated by the job that owns it)

Review da8798b: HQ feed card · gallery before/after runs · checks

App of this head (app.zip, kept 1 day)

gh workflow run reload-build.yml -R manaflow-ai/cmux --ref feat-cmux-next -f tag=pr30496 -f ref=fuzz-red/restore-h2h3 -f platform=macos -f backend_mode=local

Not verified: macOS jobs did not run: compile and app tests were skipped on da8798baaa; ci-status is green without them.

CI passes on da8798baaa (run 41286966989 attempt 1).
macOS jobs did not run: compile and app tests were skipped: Fast static checks skipped (the macos job needs both to succeed). Re-run CI once they pass, before merging.

Written by scripts/ci/classify_failures.py (ci-failure-attribution.yml); signatures are its SIGNATURES table. A machine verdict is the runner's fault, not this PR's; yours means the failing file is one this PR changes, also red on main that main's latest full suite fails the same way, seen on other PRs that it failed on another pull request's run lately.

Default changes

This pull request changes 1 default at da8798baaa71 (what everyone gets without configuring anything):

  • tunable-source.CmuxNextApp/AppActions+Workspaces.swift:newWorkspace: "f4430f92ebebcf729d1809e41fc0a0f64617faebed75af547acca8c0e524fe32" -> "0b887dd5efe6024e592b397c693b34afbcb9aebd56cde76c7ae10b9e8978e9b1"

Signed off on this commit by review (claude-review-subagent).

Merge receipt: merged as 0b6e979cae7c with these defaults changed, signed off by review (claude-review-subagent).

Regenerate the snapshot with python3 scripts/ci/defaults_guard.py snapshot.

Shipped in:

@lawrencecchen
lawrencecchen merged commit 0b6e979 into feat-cmux-next Nov 1, 2026
233 of 269 checks passed
@lawrencecchen
lawrencecchen deleted the fuzz-red/restore-h2h3 branch November 1, 2026 09:54
teamleaderleo added a commit that referenced this pull request Nov 3, 2026
…till restarting it (#33099)

* test(next): a briefly failing cmux-tui restart shows no 'could not start again' notice

Red on base: the notice shows on the first failed server ensure.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-a73aaf1602dba90b5a63

* fix(next): show 'cmux-tui could not start again' only once its starts keep failing

The window notice (#30496) showed on the first failed server ensure after the
local daemon's connection dropped, while the reconnect was still retrying by
itself (a loaded Mac, a version handoff). DaemonRecovery now counts the failed
starts (EnsureOutcome.failedInARow) and their duration: the notice shows after
three failed starts over two minutes, or at once on a full disk, or after the
person's own Restart failed. Restart stays offered meanwhile.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-a73aaf1602dba90b5a63

* fix(next): log each failed cmux-tui start at notice level

The reconnect's lines are .info (memory only), and a failed server ensure
was not logged, so log show found nothing for the notice afterwards.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-a73aaf1602dba90b5a63

* test(next): the notice test restarts on a fast fixed retry policy

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-a73aaf1602dba90b5a63

* fix(next): only the person's failed Restart shows the cmux-tui notice at once

A failed automatic restart waits for the sustained window like any failed
start (review follow-up).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-a73aaf1602dba90b5a63

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

This branch was previously deployed

1 inactive deployment
ci-routing-agent — da8798ba Deployed Nov 1, 2026 by lawrencecchen via namespace autofix #10983
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-to-land Reviewed at this head; next-autoland merges it into feat-cmux-next when its only reds are base reds risk:normal Risk tier (docs/ci/risk-tiers.md)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant