Skip to content

fix(daemon): handle macOS socket path length limit - #733

Draft
egmar wants to merge 2 commits into
Nano-Collective:mainfrom
egmar:main
Draft

fix(daemon): handle macOS socket path length limit#733
egmar wants to merge 2 commits into
Nano-Collective:mainfrom
egmar:main

Conversation

@egmar

@egmar egmar commented Aug 1, 2026

Copy link
Copy Markdown

Description

On macOS Unix domain socket paths are limited to 104 bytes. When project root exceeds this, the daemon will fail to start and throw an exception in the log.

Type of Change

  • Bug fix
  • New feature
  • Breaking change
  • Documentation update

Changeset

  • Added a changeset (pnpm changeset) describing this change for the changelog

Docs-only or internal chores need no changeset (or run pnpm changeset --empty to note that intentionally).

Testing

Automated Tests

  • New features include passing tests in .spec.ts/tsx files
  • All existing tests pass (pnpm test:all completes successfully)
  • Tests cover both success and error scenarios

Manual Testing

  • Tested with Ollama
  • Tested with OpenRouter
  • Tested with OpenAI-compatible API
  • Tested MCP integration (if applicable)

Checklist

  • Code follows project style guidelines
  • Self-review completed
  • Documentation updated (if needed)
  • No breaking changes (or clearly documented)
  • Appropriate logging added using structured logging (see CONTRIBUTING.md)

egmar added 2 commits August 1, 2026 16:02
On macOS Unix domain socket paths are limited to 104 bytes.
When project root exceeds this, use a hashed path in temp dir.
Add test coverage for the macOS-specific behavior.
@will-lamerton

Copy link
Copy Markdown
Member

Hey @egmar, thanks for this PR, and nice catch on the underlying problem. Couple of things to sort out before this goes in:

The guard measures the wrong string

source/daemon/lockfile.ts:54 checks Buffer.byteLength(projectRoot), but the path that actually gets bound is join(projectRoot, '.nanocoder', 'daemon.sock'), which is exactly 23 bytes longer. So a root of 85 bytes sits under the threshold, takes the project-local branch at line 65, and still produces a 108-byte socket path. Easiest fix is to build the candidate first and measure that:

const projectSock = join(projectRoot, '.nanocoder', 'daemon.sock');
if (process.platform === 'darwin' && Buffer.byteLength(projectSock) > DARWIN_SOCKET_PATH_CAPACITY) { ... }
return projectSock;

The test can't catch that

The 205-byte root at source/daemon/lockfile.spec.ts:148 is so far over the line that it flips the branch either way, so the test passes with the current condition too. Could you add a boundary case, a root of ~90 bytes whose joined path crosses 104, asserting it falls back to tmpdir()? Also flagging that the macOS-only early return at line 139 means these assertions only ever run locally on macOS and never on the Linux CI runners, so this one is on us to run before merging.

Worth rewording the commit message and changeset

I went and poked at this because I wanted to understand the failure, and an over-long path doesn't actually fail to bind. On Node 22 / libuv 1.49 the listen succeeds and libuv quietly truncates sun_path to 104 bytes, so the socket file lands at .nanocoder/daemon. and clients still connect because connect truncates the same way. The real damage is that source/daemon/ipc.ts:62 and :86 guard stale-socket cleanup on the untruncated path, which never exists, so after an unclean shutdown the leftover file blocks the next start with EADDRINUSE. Two projects sharing a 104-byte prefix will also silently share a socket. Doesn't change your fix at all, just means the description undersells what it's fixing.

Small stuff, all in lockfile.ts unless noted

  • Line 56-57: the comment looks cut off ("to keep and create a unique socket path"), and there's a stray empty // under it.
  • Line 62: .toLowerCase() is a no-op since digest('hex') is already lowercase, and the Windows branch at line 43 doesn't have it.
  • Line 63: maybe a comment noting the fallback assumes a short $TMPDIR? It's ~49 bytes on macOS so you're well clear, but the assumption is invisible right now.
  • lockfile.spec.ts:166 lost its trailing newline. Biome excludes spec files so CI won't moan about it, just diff noise.

For what it's worth, I traced every getSocketPath and socketPath call site before writing any of this and the fallback holds up cleanly. The truncated SHA-256 is stable across restarts, so cli.ts:271 recomputing the path, daemon.ts:99 and :204, and clients reading socketPath back out of the lockfile all agree, and nothing assumes a project-local location. The docstring you left at lines 32-37 made that trace a lot faster, so thank you for that.

Looking forward to reviewing again :)

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.

2 participants