Skip to content

fix: avoid macOS daemon startup crash after fork - #4422

Merged
paul-nechifor merged 8 commits into
mainfrom
ari/fix/macos-daemon-startup
Oct 20, 2026
Merged

paul-nechifor merged 8 commits into
mainfrom
ari/fix/macos-daemon-startup

Conversation

@aa-MONTS

@aa-MONTS aa-MONTS commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

Problem

On macOS, the daemon can crash while initializing native libraries after the double fork, even though coordinator construction happens after forking.

Reproduced on unchanged main at 60aee3c0 (macOS 27 arm64, Python 3.12.13):

dimos --replay run unitree-go2-basic --daemon --viewer none --n-workers 1

The launcher exits 1 with daemon exited during startup. The crash report shows SIGSEGV in CoreFoundation preferences / Curl_macos_init, with crashed on child side of fork pre-exec. The replay regression passes with this patch.

Changes

  • Re-exec the daemon grandchild on macOS before building modules, preserving the Python interpreter and original invocation, including interpreter flags and -m entry points.
  • Carry the startup-status pipe across that one exec, consume the handoff marker, then make the descriptor non-inheritable again.
  • Preserve the launcher's run ID and log directory during the handoff. Leave the Linux double-fork path unchanged.
  • Add handoff/identity tests and extend the daemon RPC regression with Go2 replay. Only the asset-backed Go2 parameter is marked self_hosted, keeping the existing asset-free case in regular CI.

No navigation/controller changes or process-lifecycle redesign.

Validation

  • Unchanged baseline reproduced the native startup crash; the added handoff contract tests also demonstrated the expected baseline failures.
  • 138 selected daemon, coordinator, lifecycle and CLI tests passed locally; no failures, errors or skips.
  • Full Go2 replay: background start → separate-client connection with advancing odometry and camera frames → restart → fresh connection → stop. No owned processes or registry entries remained; no rescue/SIGKILL was needed.
  • Interrupted startup returned 130 and cleaned up its processes/registry entry.
  • Ruff, applicable pre-commit hooks and whitespace checks passed. Targeted mypy checks pass; the existing CLI test file retains the same nine missing-annotation errors as baseline.
  • Independent agent review found no remaining blocking logic/security findings after correcting the heavy-test CI marker. Actual regular/self-hosted CI marker selections were checked.

CI verification

Verified on October 3, 2026 at head b96d3dac: 31 successful checks, 6 skipped, none failed or pending. The standard Python/macOS/Linux/ARM matrix, self-hosted Linux/macOS tests, large tests, lint/type checks, native builds, CodeQL and coverage checks passed.

CI run 37153553534, attempt 2 passed on the unchanged commit. The first attempt had a macOS Codecov upload ExpiredToken after all tests passed, plus a Linux foreground simulation timeout that passed its diagnostic rerun. One failed-job rerun passed both jobs; no code, workflow, test skips or timeouts were changed. The macOS rerun passed 280 tests including the added Go2 daemon replay case; the large-test rerun passed all 7 selected tests. Existing skips remain skips, not validation claims.

Architecture / related work

A separate read-only architecture review found no introduced blockers:

Re-exec resets the daemon coordinator's interpreter; forkserver independently isolates module workers. These are complementary boundaries, not competing process models.

Review scope / limits

Draft for maintainer review of the exec boundary, status-FD lifetime, run identity and failure handling. Local validation used macOS replay data; remote CI exercised the repository's selected Linux/macOS and simulation suites. This is not hardware validation or an assertion that every optional/skipped test was executed. Independent agent review does not replace maintainer approval.

Preserve the interpreter invocation, startup status pipe and run identity across a macOS-only exec before building modules. Keep the Linux fork path unchanged and mark the LFS-backed replay regression self_hosted.
@github-actions github-actions Bot added the first-time-contributor PR opened by an author who had not previously committed to this repository label Oct 3, 2026

@github-actions github-actions Bot left a comment •

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.

go2 replay realtime (arm64)

Details
Benchmark suite Current: 4bd9278 Previous: fbe126e Ratio
peak memory 1591.758 MB 1564.824 MB 1.02
peak threads 352 threads 357 threads 0.99
network (transport) 2722.305 MB 2727.328 MB 1.00
disk write 1.664 MB 1.488 MB 1.12
instructions 114.921 G 114.929 G 1.00

This comment was automatically generated by workflow using github-action-benchmark.

@codecov

codecov Bot commented Oct 3, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.01980% with 2 lines in your changes missing coverage. Please review.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
dimos/cli/dimos.py 0.00% 1 Missing and 1 partial ⚠️
@@            Coverage Diff             @@
##             main    #4422      +/-   ##
==========================================
+ Coverage   81.84%   81.96%   +0.12%     
==========================================
  Files        1656     1657       +1     
  Lines      154293   154388      +95     
  Branches    12988    13461     +473     
==========================================
+ Hits       126275   126547     +272     
+ Misses      24435    24243     -192     
- Partials     3583     3598      +15     
Components Coverage Δ
Tests 96.84% <100.00%> (+0.01%) ⬆️
Flag Coverage Δ
OS-macos-latest 75.82% <97.02%> (-0.02%) ⬇️
OS-ubuntu-24.04-arm 75.59% <97.02%> (-0.02%) ⬇️
OS-ubuntu-latest 75.94% <97.02%> (-0.02%) ⬇️
Py-3.10 75.94% <97.02%> (-0.02%) ⬇️
Py-3.11 75.93% <97.02%> (-0.02%) ⬇️
Py-3.12 75.94% <97.02%> (-0.02%) ⬇️
SelfHosted-Large 32.14% <19.80%> (+0.29%) ⬆️
SelfHosted-Linux 40.82% <29.70%> (+0.18%) ⬆️
SelfHosted-macOS 41.10% <36.63%> (+0.17%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
dimos/cli/commands/lifecycle.py 64.70% <100.00%> (+0.14%) ⬆️
dimos/cli/test_dimos.py 99.00% <100.00%> (+0.03%) ⬆️
dimos/core/daemon.py 93.42% <100.00%> (+1.48%) ⬆️
dimos/core/test_daemon_exec.py 100.00% <100.00%> (ø)
dimos/core/test_daemon_zenoh.py 76.59% <100.00%> (+10.97%) ⬆️
dimos/cli/dimos.py 97.87% <0.00%> (-0.03%) ⬇️

... and 18 files with indirect coverage changes

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@aa-MONTS

aa-MONTS commented Oct 6, 2026

Copy link
Copy Markdown
Contributor Author

@aclauer pls review

@aa-MONTS
aa-MONTS marked this pull request as ready for review October 9, 2026 01:55
@github-actions github-actions Bot added the ready-to-merge Required CI checks have passed on this PR label Oct 9, 2026
@greptile-apps

greptile-apps Bot commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 5/5

[High impact] No blocking issue was found; native macOS startup remains outside the checks performed here.

T-Rex evidence

Evidence from the check

  • The authored script runs the baseline and changed daemon paths through real process operations while selecting the Darwin branch.

Command output from the check

  • The baseline run returned a successful status without re-exec, providing the comparison condition.

Command output from the check

  • The changed run returned a successful status after re-exec and showed that the later worker inherited neither the status FD nor its environment marker.

Command output from the check

  • The scoped pytest command passed all five daemon exec tests, with a subprocess ResourceWarning.

Evidence from the check

  • This authored pytest script invokes the CLI twice with a simulated macOS handoff and records run identity, log destination, and original arguments.

Command output from the check

  • The executed probe loaded the main-branch lifecycle implementation and showed a second run ID and log directory after re-exec.

Command output from the check

  • The same executed probe used the checked-out implementation and showed one run ID, one log directory, and a preserved invocation.

Command output from the check

  • The targeted existing pytest selection completed with 18 passing tests, supporting the simulated handoff result.

Evidence from the check

  • The authored script runs the baseline and changed daemon paths through real process operations while selecting the Darwin branch.

Command output from the check

  • The baseline run returned a successful status without re-exec, providing the comparison condition.

Command output from the check

  • The changed run returned a successful status after re-exec and showed that the later worker inherited neither the status FD nor its environment marker.

Command output from the check

  • The scoped pytest command passed all five daemon exec tests, with a subprocess ResourceWarning.

Evidence from the check

  • This authored pytest script invokes the CLI twice with a simulated macOS handoff and records run identity, log destination, and original arguments.

Command output from the check

  • The executed probe loaded the main-branch lifecycle implementation and showed a second run ID and log directory after re-exec.

Command output from the check

  • The same executed probe used the checked-out implementation and showed one run ID, one log directory, and a preserved invocation.

Command output from the check

  • The targeted existing pytest selection completed with 18 passing tests, supporting the simulated handoff result.

View artifacts

Summary

This PR re-executes the macOS daemon grandchild before building modules, avoiding native-library startup in the forked Python process. It carries the startup-status pipe across that exec, preserves the launcher's run ID and log directory, and leaves Linux startup unchanged. Checks of the status handoff and run identity found no actionable issues. Native macOS startup was not exercised here.

Reviews (1) · Last reviewed commit: "Merge branch 'main' into ari/fix/macos-d..." · Reviewed by Greptile

@aa-MONTS

aa-MONTS commented Oct 9, 2026

Copy link
Copy Markdown
Contributor Author

Review follow-up: dotenv-only XDG_STATE_HOME on macOS

Found one reproducible configuration edge case at 7ad581649ea05f4d497891a52120bb0ca34726c0 (macOS, Python 3.12.13): if XDG_STATE_HOME is set only in .env, daemon startup succeeds, but subsequent dimos status and dimos stop report No running DimOS instance while the daemon is still alive.

Repro: in a disposable checkout with an isolated HOME, leave XDG_STATE_HOME unset in the shell and put XDG_STATE_HOME=${HOME}/xdg-from-dotenv in the checkout's .env. Run:

python -m dimos.cli.dimos run demo-mcp-stress-test --daemon --disable mcp-server --viewer none --n-workers 1
python -m dimos.cli.dimos status
python -m dimos.cli.dimos stop

Real CLI comparison, not mocked fork/exec:

Version / configuration Startup Status / stop
Exact base 9197ea5b1dca07c6c10f4b1141fd627754c146c7; .env only Succeeds Both work
PR head; .env only Succeeds Status cannot find it; stop exits 1
PR head; same setting exported in the shell Succeeds Both work

The affected test daemon was cleaned up by sending SIGTERM to its verified PID; no test processes or registry entries remained. No hardware was used.

Cause: the CLI imports the shared directory constants before loading dotenv. The new exec inherits the dotenv-modified environment and resolves a different registry root. Later control commands still resolve the original root. Late dotenv loading is pre-existing; the daemon/control-command split is introduced by this change.

One possible fix is loading dotenv before imports freeze those paths, preserving shell-environment precedence and test isolation. That has not been implemented or validated and affects shared CLI startup beyond macOS. Would you prefer addressing this here, or in a separate linked CLI-initialization fix? Flagging it for scope guidance rather than silently broadening this PR.

@github-actions github-actions Bot removed the ready-to-merge Required CI checks have passed on this PR label Oct 14, 2026
@aa-MONTS

Copy link
Copy Markdown
Contributor Author

Removed test_foreground_ignores_daemon_exec_identity in f14e4b0. It injected a private daemon-handoff marker into a foreground run, a combination the supported startup path does not produce. The actual daemon handoff and dotenv-only start/status/stop coverage remain.

Verification: 76 targeted daemon/CLI tests passed, including the real asset-free subprocess lifecycle; pinned lint/format hooks passed. No production-code changes in this commit.

@aa-MONTS

Copy link
Copy Markdown
Contributor Author

What these tests protect

Short version: These tests check that a background run starts with the right settings, can be reached, and can be found and stopped. They also check that the Mac fix leaves Linux startup alone.

These are the tests added or changed by this PR:

  • Keep the right run identity: An internal restart must keep the same run ID, while a fresh run gets its own. This keeps logs and status attached to the right run.
  • Finish the startup handoff once: The restarted process must reuse the original startup connection, keep it working, and not pass it on to later workers. This prevents repeated startup or a command waiting forever for a reply.
  • Keep the original launch options: Both supported launch methods must keep all their options after the Mac restart. A restart error must also be reported, not ignored. This prevents starting with different settings or pretending startup worked.
  • Leave Linux startup unchanged: Linux must keep its existing startup path rather than use the Mac-only restart. This prevents fixing one platform by breaking the other.
  • Check the real start-to-stop workflow: Starts a real background run, checks that another process can reach it, finds it with status, and checks that stop succeeds. It also checks a storage location supplied only through the settings file, so later commands do not lose track of the run.

The smaller checks catch specific startup mistakes; the last test checks the real workflow. Its Go2 case replays recorded data, not a live robot, and runs on the self-hosted test runner because it needs the larger recording.

@github-actions github-actions Bot added the ready-to-merge Required CI checks have passed on this PR label Oct 14, 2026
@github-actions github-actions Bot removed the ready-to-merge Required CI checks have passed on this PR label Oct 16, 2026
@github-actions github-actions Bot added the ready-to-merge Required CI checks have passed on this PR label Oct 16, 2026
@github-actions github-actions Bot removed the ready-to-merge Required CI checks have passed on this PR label Oct 20, 2026
@github-actions github-actions Bot added the ready-to-merge Required CI checks have passed on this PR label Oct 20, 2026
@paul-nechifor
paul-nechifor merged commit b71a775 into main Oct 20, 2026
40 checks passed
@paul-nechifor
paul-nechifor deleted the ari/fix/macos-daemon-startup branch October 20, 2026 16:40

This branch was successfully deployed

1 active deployment
cachix — 4bd92788 Deployed Oct 20, 2026 by aa-MONTS via cachix-build #13411
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

first-time-contributor PR opened by an author who had not previously committed to this repository ready-to-merge Required CI checks have passed on this PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants