Skip to content

Reload a daemon still running as launchd Adaptive instead of deferring forever #1351

Closed

Description

@alexeyzimarev

kcap 1.3.0-beta.6, macOS 26.6, 24 cores / 64 GB, 11 hosted agents.

Symptom

The desktop app stays responsive, but typing into and redrawing a hosted agent's terminal gets slow whenever the agents work, and worse the more agents there are. With the agents idle it is fine.

Cause

The daemon is still loaded as ProcessType=Adaptive:

  • launchctl print gui/501/io.kurrent.kcap.daemon.alexey shows spawn type = adaptive (6), jetsam priority 40.
  • ps -M <daemon pid> shows PRI 4 on every thread. All 12 children (11 claude, 1 codex) run at PRI 4, and so do the VBCSCompiler (7.3 GB and 4.4 GB) and csc processes their builds spawn. The app's main thread runs at 46; a Claude started from a terminal at 31.
  • The agents' parallel builds pushed the machine into swap: 23 GB in the compressor, 13 of 14 GB swap used, load 129 with 59% CPU idle. The background band is throttled on CPU and I/O, so the daemon and the agents on the keystroke path wait on page-ins while the foreground app does not.

Ruled out: the app keeps one workspace alive (a hidden agent has no attach, chat poll or PR poll), PTY reads run on dedicated threads (#1024), both sinks are non-blocking, and the cloud mirror's desyncs today were all reconnects.

Why the migration in #1269 does not reach this host

  1. The deferral has no end. RefreshUnit reloads only after the daemon accepts an idle-only restart. A host that always has agents running never idles, so the plist stays Adaptive for good, and nothing in kcap daemon status or the app says so.
  2. The upgrade is a byte-exact match. UpgradeProcessType looks for the writer's single line <key>ProcessType</key><string>Adaptive</string>. The plist on this machine is in Apple's canonical layout (tab-indented, one element per line, keys sorted), which is what anything that went through plutil, PlistBuddy or defaults ends up as. The upgrade returns null, DeclaresStandardProcessType is false, and the refresh reports Unchanged.
  3. There is no in-process escape. taskpolicy -B -p <pid> exits 0 and changes nothing, on the daemon or on a child. The background state is derived from the spawn type and inherited as derived state, so the daemon cannot lift it at startup. Only a launchd reload applies the new type.
  4. kcap daemon restart does not apply it either. KeepAlive relaunches from the definition launchd cached at load.

Proposal

  • kcap daemon status, kcap doctor and the desktop app's daemon row report when the loaded job runs as Adaptive (LaunchdUnit.LoadedAsAdaptive already reads it) and say that a reload is needed.
  • kcap daemon restart and the app's restart action, when the loaded spawn type is Adaptive or the plist on disk differs from the loaded definition, run the bootout-while-exiting + bootstrap sequence from [AI-3419] Run the macOS daemon as a Standard launchd process #1269 instead of letting KeepAlive relaunch the cached definition. --when-idle does the same once idle.
  • The migration parses the plist and rewrites ProcessType whatever its layout, instead of matching one line.
  • The reload ends every hosted agent, and uncommitted work in their worktrees is deleted with them, so the command must say so and refuse while agents run unless forced.

Workaround used here: edit the plist to Standard by hand, then launchctl bootout and bootstrap the job once the agents are done.

Refs: #1269 (AI-3419), #1024.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions