Skip to content

Refactor daemon supervisor boundary#280

Merged
TraderSamwise merged 1 commit into
masterfrom
chore/core-sidecar-next-4
Jul 3, 2026
Merged

Refactor daemon supervisor boundary#280
TraderSamwise merged 1 commit into
masterfrom
chore/core-sidecar-next-4

Conversation

@TraderSamwise

@TraderSamwise TraderSamwise commented Jul 3, 2026

Copy link
Copy Markdown
Owner

Summary

  • split daemon metadata/state persistence into src/daemon-state.ts
  • move daemon bootstrap, stop, and project-service supervisor helpers into src/daemon-supervisor.ts
  • keep src/daemon.ts focused on the long-lived daemon implementation/routes and add a boundary guard test

Verification

  • PATH="/Users/sam/.nvm/versions/node/v24.16.0/bin:/Users/sam/.local/volta-shims:/Users/sam/.local/bin:/Users/sam/.bun/bin:/Users/sam/.volta/tools/image/packages/@openai/codex/lib/node_modules/@openai/codex/node_modules/@openai/codex-darwin-arm64/vendor/aarch64-apple-darwin/codex-path:/Users/sam/.codex/tmp/arg0/codex-arg0MGAx59:/Applications/cmux.app/Contents/Resources/bin:/opt/homebrew/opt/mysql-client/bin:/Users/sam/.nvm/versions/node/v24.14.0/bin:/opt/homebrew/Caskroom/miniforge/base/envs/py39/bin:/opt/homebrew/Caskroom/miniforge/base/condabin:/Users/sam/.npm-global/bin:/Users/sam/pathother:/Users/sam/apache-ant/bin:/usr/local/mysql/bin:/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/System/Cryptexes/App/usr/bin:/usr/bin:/bin:/usr/sbin:/sbin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/local/bin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/bin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/appleinternal/bin:/pkg/env/global/bin:/Library/Apple/usr/bin:/usr/local/share/dotnet:/var/folders/3k/tp7t81tn3572k4dsfxd51c180000gn/T/cmux-cli-shims/A1AD0E84-DACE-44A4-AE83-10953C6E1028:/Users/sam/.cargo/bin:/Users/sam/bin" yarn typecheck
  • PATH="/Users/sam/.nvm/versions/node/v24.16.0/bin:/Users/sam/.local/volta-shims:/Users/sam/.local/bin:/Users/sam/.bun/bin:/Users/sam/.volta/tools/image/packages/@openai/codex/lib/node_modules/@openai/codex/node_modules/@openai/codex-darwin-arm64/vendor/aarch64-apple-darwin/codex-path:/Users/sam/.codex/tmp/arg0/codex-arg0MGAx59:/Applications/cmux.app/Contents/Resources/bin:/opt/homebrew/opt/mysql-client/bin:/Users/sam/.nvm/versions/node/v24.14.0/bin:/opt/homebrew/Caskroom/miniforge/base/envs/py39/bin:/opt/homebrew/Caskroom/miniforge/base/condabin:/Users/sam/.npm-global/bin:/Users/sam/pathother:/Users/sam/apache-ant/bin:/usr/local/mysql/bin:/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/System/Cryptexes/App/usr/bin:/usr/bin:/bin:/usr/sbin:/sbin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/local/bin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/bin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/appleinternal/bin:/pkg/env/global/bin:/Library/Apple/usr/bin:/usr/local/share/dotnet:/var/folders/3k/tp7t81tn3572k4dsfxd51c180000gn/T/cmux-cli-shims/A1AD0E84-DACE-44A4-AE83-10953C6E1028:/Users/sam/.cargo/bin:/Users/sam/bin" yarn lint
  • PATH="/Users/sam/.nvm/versions/node/v24.16.0/bin:/Users/sam/.local/volta-shims:/Users/sam/.local/bin:/Users/sam/.bun/bin:/Users/sam/.volta/tools/image/packages/@openai/codex/lib/node_modules/@openai/codex/node_modules/@openai/codex-darwin-arm64/vendor/aarch64-apple-darwin/codex-path:/Users/sam/.codex/tmp/arg0/codex-arg0MGAx59:/Applications/cmux.app/Contents/Resources/bin:/opt/homebrew/opt/mysql-client/bin:/Users/sam/.nvm/versions/node/v24.14.0/bin:/opt/homebrew/Caskroom/miniforge/base/envs/py39/bin:/opt/homebrew/Caskroom/miniforge/base/condabin:/Users/sam/.npm-global/bin:/Users/sam/pathother:/Users/sam/apache-ant/bin:/usr/local/mysql/bin:/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/System/Cryptexes/App/usr/bin:/usr/bin:/bin:/usr/sbin:/sbin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/local/bin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/bin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/appleinternal/bin:/pkg/env/global/bin:/Library/Apple/usr/bin:/usr/local/share/dotnet:/var/folders/3k/tp7t81tn3572k4dsfxd51c180000gn/T/cmux-cli-shims/A1AD0E84-DACE-44A4-AE83-10953C6E1028:/Users/sam/.cargo/bin:/Users/sam/bin" yarn vitest run
  • PATH="/Users/sam/.nvm/versions/node/v24.16.0/bin:/Users/sam/.local/volta-shims:/Users/sam/.local/bin:/Users/sam/.bun/bin:/Users/sam/.volta/tools/image/packages/@openai/codex/lib/node_modules/@openai/codex/node_modules/@openai/codex-darwin-arm64/vendor/aarch64-apple-darwin/codex-path:/Users/sam/.codex/tmp/arg0/codex-arg0MGAx59:/Applications/cmux.app/Contents/Resources/bin:/opt/homebrew/opt/mysql-client/bin:/Users/sam/.nvm/versions/node/v24.14.0/bin:/opt/homebrew/Caskroom/miniforge/base/envs/py39/bin:/opt/homebrew/Caskroom/miniforge/base/condabin:/Users/sam/.npm-global/bin:/Users/sam/pathother:/Users/sam/apache-ant/bin:/usr/local/mysql/bin:/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/System/Cryptexes/App/usr/bin:/usr/bin:/bin:/usr/sbin:/sbin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/local/bin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/bin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/appleinternal/bin:/pkg/env/global/bin:/Library/Apple/usr/bin:/usr/local/share/dotnet:/var/folders/3k/tp7t81tn3572k4dsfxd51c180000gn/T/cmux-cli-shims/A1AD0E84-DACE-44A4-AE83-10953C6E1028:/Users/sam/.cargo/bin:/Users/sam/bin" yarn build
  • pre-push: yarn typecheck && yarn lint && yarn test

Summary by CodeRabbit

  • Bug Fixes

    • Improved daemon startup, shutdown, and health checks for more reliable app behavior.
    • Strengthened project service handling so related actions are more consistent and resilient.
  • Refactor

    • Reorganized background-service logic to better separate responsibilities, with no expected change to normal usage.
  • Tests

    • Updated automated checks to cover the new service boundaries and behavior.

@vercel

vercel Bot commented Jul 3, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
app Ready Ready Preview, Comment Jul 3, 2026 8:01pm

@coderabbitai

coderabbitai Bot commented Jul 3, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The daemon module is split into daemon-state.ts (host/port config, daemon info/state persistence) and daemon-supervisor.ts (startup locking, health probing, spawning, termination, request/project-service orchestration). daemon.ts and all consumer modules/tests are updated to import from the new files, with a new boundary test enforcing the separation.

Changes

Daemon module split

Layer / File(s) Summary
Daemon state module
src/daemon-state.ts
New module defines host/port/base-URL helpers, AimuxDaemonInfo/ProjectServiceState/DaemonState types, and JSON persistence functions (loadDaemonInfo, saveDaemonInfo, clearDaemonInfo, loadDaemonState, saveDaemonState).
Daemon supervisor module
src/daemon-supervisor.ts
New module implements filesystem startup locking, health probing/adoption, spawn/termination logic, requestDaemonJson, ensureDaemonRunning, stopDaemon, and project-service wrappers (ensureProjectService, stopProjectService, projectServiceStatus).
daemon.ts refactor
src/daemon.ts, src/core-sidecar-boundary.test.ts
daemon.ts removes local state/lock/probe logic, imports helpers from daemon-state.js, updates ProjectsRouteProject typing, and switches persistence calls to imported helpers; a new boundary test asserts daemon.ts no longer contains supervisor-related code.
Consumer import updates
src/core-command-client.ts, src/core-command-client.test.ts, src/mobile-push-bridge.ts, src/mobile-push-bridge.test.ts, src/main.ts, src/multiplexer/persistence-methods.ts, src/runtime-coherence.ts, src/runtime-restart.ts, src/daemon.test.ts, src/one-shot-node-inventory.test.ts
All consumers and tests update imports/mocks/allowlists to reference daemon-state.js and daemon-supervisor.js instead of daemon.js.

Estimated code review effort: 3 (Moderate) | ~30 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Consumer as core-command-client/runtime-restart
  participant Supervisor as daemon-supervisor.ts
  participant State as daemon-state.ts
  participant Daemon as Spawned Daemon Process

  Consumer->>Supervisor: ensureDaemonRunning(options)
  Supervisor->>State: loadDaemonInfo()
  State-->>Supervisor: AimuxDaemonInfo or null
  alt daemon info valid and healthy
    Supervisor->>Daemon: GET /health
    Daemon-->>Supervisor: health status
  else no valid daemon
    Supervisor->>Supervisor: acquire startup lock
    Supervisor->>Daemon: spawn(command)
    Supervisor->>Daemon: poll /health until match
    Supervisor->>State: saveDaemonInfo(info)
    Supervisor->>Supervisor: release startup lock
  end
  Supervisor-->>Consumer: AimuxDaemonInfo
  Consumer->>Supervisor: requestDaemonJson(path)
  Supervisor->>State: loadDaemonInfo()
  Supervisor->>Daemon: HTTP request
  Daemon-->>Supervisor: JSON response
  Supervisor-->>Consumer: parsed result
Loading

Possibly related PRs

  • TraderSamwise/aimux#199: Also moves ensureDaemonRunning/ensureProjectService/stopDaemon/stopProjectService imports in runtime-restart.ts off of daemon.js, aligning with the same module split.
  • TraderSamwise/aimux#218: Also modifies src/runtime-restart.ts's daemon-control flow, overlapping with the import changes made here.
  • TraderSamwise/aimux#273: Also changes requestCoreCommand in src/core-command-client.ts and its ensureDaemonRunning/requestDaemonJson dependencies.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: refactoring the daemon supervisor boundary into separate modules.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch chore/core-sidecar-next-4

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

@TraderSamwise
TraderSamwise merged commit a6844e0 into master Jul 3, 2026
2 of 3 checks passed

@coderabbitai coderabbitai 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.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/daemon-state.ts`:
- Around line 111-126: `loadDaemonState` can still throw when `loadJson` returns
valid JSON that is `null` or a non-object, because `raw.projects` and
`raw.updatedAt` are accessed without validating `raw` first. Update
`loadDaemonState` to defensively verify the parsed value from `loadJson` is an
object with the expected shape before reading its fields, and fall back to the
default daemon state when it is `null` or malformed; keep the existing
`projects` filtering logic intact.

In `@src/daemon-supervisor.ts`:
- Around line 97-116: The lock acquisition in tryAcquireDaemonStartLock is not
fully serialized because the lock directory can be observed before owner.json is
safely published. Update the acquire path so owner.json is written atomically
only after the lock is unquestionably owned, and make the stale-lock check in
readLockPid/tryAcquireDaemonStartLock ignore or retry while owner.json is
missing or incomplete instead of deleting the directory immediately. Keep the
fix localized to tryAcquireDaemonStartLock and the owner.json publication logic
so concurrent daemon starts cannot both proceed.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a0eede7d-4208-4a24-ac91-a5b1d9b2783e

📥 Commits

Reviewing files that changed from the base of the PR and between 8999b1d and 808a5ef.

📒 Files selected for processing (14)
  • src/core-command-client.test.ts
  • src/core-command-client.ts
  • src/core-sidecar-boundary.test.ts
  • src/daemon-state.ts
  • src/daemon-supervisor.ts
  • src/daemon.test.ts
  • src/daemon.ts
  • src/main.ts
  • src/mobile-push-bridge.test.ts
  • src/mobile-push-bridge.ts
  • src/multiplexer/persistence-methods.ts
  • src/one-shot-node-inventory.test.ts
  • src/runtime-coherence.ts
  • src/runtime-restart.ts

Comment thread src/daemon-state.ts
Comment on lines +111 to +126
export function loadDaemonState(): DaemonState {
const raw = loadJson<DaemonState>(getDaemonStatePath(), {
version: 1,
updatedAt: new Date(0).toISOString(),
projects: {},
});
const projects: Record<string, ProjectServiceState> = {};
for (const [projectId, entry] of Object.entries(raw.projects ?? {})) {
if (entry) projects[projectId] = entry;
}
return {
version: 1,
updatedAt: raw.updatedAt,
projects,
};
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

loadDaemonState can throw on a corrupt/null state file.

loadJson only returns the fallback on a missing file or a JSON.parse throw. A file containing valid JSON null (or any non-object) parses successfully and is cast to DaemonState, after which raw.projects / raw.updatedAt dereference null and throw TypeError. The ?? {} guard doesn't help because raw.projects is evaluated before the coalesce.

🛡️ Proposed null-safe fix
-  const projects: Record<string, ProjectServiceState> = {};
-  for (const [projectId, entry] of Object.entries(raw.projects ?? {})) {
+  const projects: Record<string, ProjectServiceState> = {};
+  for (const [projectId, entry] of Object.entries(raw?.projects ?? {})) {
     if (entry) projects[projectId] = entry;
   }
   return {
     version: 1,
-    updatedAt: raw.updatedAt,
+    updatedAt: raw?.updatedAt ?? new Date(0).toISOString(),
     projects,
   };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function loadDaemonState(): DaemonState {
const raw = loadJson<DaemonState>(getDaemonStatePath(), {
version: 1,
updatedAt: new Date(0).toISOString(),
projects: {},
});
const projects: Record<string, ProjectServiceState> = {};
for (const [projectId, entry] of Object.entries(raw.projects ?? {})) {
if (entry) projects[projectId] = entry;
}
return {
version: 1,
updatedAt: raw.updatedAt,
projects,
};
}
export function loadDaemonState(): DaemonState {
const raw = loadJson<DaemonState>(getDaemonStatePath(), {
version: 1,
updatedAt: new Date(0).toISOString(),
projects: {},
});
const projects: Record<string, ProjectServiceState> = {};
for (const [projectId, entry] of Object.entries(raw?.projects ?? {})) {
if (entry) projects[projectId] = entry;
}
return {
version: 1,
updatedAt: raw?.updatedAt ?? new Date(0).toISOString(),
projects,
};
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/daemon-state.ts` around lines 111 - 126, `loadDaemonState` can still
throw when `loadJson` returns valid JSON that is `null` or a non-object, because
`raw.projects` and `raw.updatedAt` are accessed without validating `raw` first.
Update `loadDaemonState` to defensively verify the parsed value from `loadJson`
is an object with the expected shape before reading its fields, and fall back to
the default daemon state when it is `null` or malformed; keep the existing
`projects` filtering logic intact.

Comment thread src/daemon-supervisor.ts
Comment on lines +97 to +116
function tryAcquireDaemonStartLock(): string | null {
const lockPath = daemonStartLockPath();
mkdirSync(dirname(lockPath), { recursive: true });
const acquire = (): string | null => {
try {
mkdirSync(lockPath);
writeFileSync(join(lockPath, "owner.json"), `${JSON.stringify({ pid: process.pid })}\n`);
return lockPath;
} catch (error) {
if ((error as { code?: string }).code !== "EEXIST") throw error;
return null;
}
};
const acquired = acquire();
if (acquired) return acquired;
const pid = readLockPid(lockPath);
if (pid && isPidAlive(pid)) return null;
rmSync(lockPath, { recursive: true, force: true });
return acquire();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Look for other callers/paths that rely on the daemon-start lock and any existing serialization guarantees
rg -nP -C3 'daemonStartLockPath|tryAcquireDaemonStartLock|releaseDaemonStartLock|owner\.json' src

Repository: TraderSamwise/aimux

Length of output: 9584


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the daemon supervisor lock/startup flow and related tests.
sed -n '84,130p' src/daemon-supervisor.ts
printf '\n----\n'
sed -n '296,360p' src/daemon-supervisor.ts
printf '\n----\n'
rg -n -C3 'probeDefaultDaemon|EADDRINUSE|listen\\(' src/daemon-supervisor.ts src/**/*.test.ts

Repository: TraderSamwise/aimux

Length of output: 3864


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Read the daemon startup branch to see whether the lock is the only guard
# or whether later probe/listen checks prevent a duplicate spawn.
sed -n '240,360p' src/daemon-supervisor.ts

Repository: TraderSamwise/aimux

Length of output: 4062


Make owner.json publication atomic in tryAcquireDaemonStartLock A contender can still see the lock directory before owner.json exists or is fully written, treat the lock as stale, delete it, and acquire the lock itself. The pre-spawn probe and EADDRINUSE path don’t serialize startup, so overlapping daemon starts are still possible.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/daemon-supervisor.ts` around lines 97 - 116, The lock acquisition in
tryAcquireDaemonStartLock is not fully serialized because the lock directory can
be observed before owner.json is safely published. Update the acquire path so
owner.json is written atomically only after the lock is unquestionably owned,
and make the stale-lock check in readLockPid/tryAcquireDaemonStartLock ignore or
retry while owner.json is missing or incomplete instead of deleting the
directory immediately. Keep the fix localized to tryAcquireDaemonStartLock and
the owner.json publication logic so concurrent daemon starts cannot both
proceed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant