homegraph 1.1.3 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (246) hide show
  1. package/README.md +43 -18
  2. package/dist/arkts/ohos-api-index.d.ts +15 -0
  3. package/dist/arkts/ohos-api-index.d.ts.map +1 -0
  4. package/dist/arkts/ohos-api-index.js +190 -0
  5. package/dist/arkts/ohos-api-index.js.map +1 -0
  6. package/dist/arkts/ohos-sdk-input.d.ts +36 -0
  7. package/dist/arkts/ohos-sdk-input.d.ts.map +1 -0
  8. package/dist/arkts/ohos-sdk-input.js +214 -0
  9. package/dist/arkts/ohos-sdk-input.js.map +1 -0
  10. package/dist/bin/command-supervision.d.ts +12 -0
  11. package/dist/bin/command-supervision.d.ts.map +1 -0
  12. package/dist/bin/command-supervision.js +86 -0
  13. package/dist/bin/command-supervision.js.map +1 -0
  14. package/dist/bin/homegraph.d.ts +1 -1
  15. package/dist/bin/homegraph.js +706 -168
  16. package/dist/bin/homegraph.js.map +1 -1
  17. package/dist/db/index.d.ts +20 -0
  18. package/dist/db/index.d.ts.map +1 -1
  19. package/dist/db/index.js +39 -0
  20. package/dist/db/index.js.map +1 -1
  21. package/dist/db/migrations.d.ts +1 -1
  22. package/dist/db/migrations.d.ts.map +1 -1
  23. package/dist/db/migrations.js +40 -1
  24. package/dist/db/migrations.js.map +1 -1
  25. package/dist/db/queries.d.ts +14 -0
  26. package/dist/db/queries.d.ts.map +1 -1
  27. package/dist/db/queries.js +174 -3
  28. package/dist/db/queries.js.map +1 -1
  29. package/dist/db/schema.sql +20 -0
  30. package/dist/directory.d.ts +32 -0
  31. package/dist/directory.d.ts.map +1 -1
  32. package/dist/directory.js +83 -0
  33. package/dist/directory.js.map +1 -1
  34. package/dist/extraction/arkts-batch-worker.d.ts +2 -0
  35. package/dist/extraction/arkts-batch-worker.d.ts.map +1 -0
  36. package/dist/extraction/arkts-batch-worker.js +28 -0
  37. package/dist/extraction/arkts-batch-worker.js.map +1 -0
  38. package/dist/extraction/context.d.ts +11 -0
  39. package/dist/extraction/context.d.ts.map +1 -1
  40. package/dist/extraction/context.js +20 -0
  41. package/dist/extraction/context.js.map +1 -1
  42. package/dist/extraction/index.d.ts +16 -2
  43. package/dist/extraction/index.d.ts.map +1 -1
  44. package/dist/extraction/index.js +619 -380
  45. package/dist/extraction/index.js.map +1 -1
  46. package/dist/extraction/languages/arkts-state-decorators.d.ts +13 -0
  47. package/dist/extraction/languages/arkts-state-decorators.d.ts.map +1 -0
  48. package/dist/extraction/languages/arkts-state-decorators.js +26 -0
  49. package/dist/extraction/languages/arkts-state-decorators.js.map +1 -0
  50. package/dist/extraction/languages/arkts-viewtree.d.ts +4 -2
  51. package/dist/extraction/languages/arkts-viewtree.d.ts.map +1 -1
  52. package/dist/extraction/languages/arkts-viewtree.js +21 -6
  53. package/dist/extraction/languages/arkts-viewtree.js.map +1 -1
  54. package/dist/extraction/languages/arkts.d.ts +98 -2
  55. package/dist/extraction/languages/arkts.d.ts.map +1 -1
  56. package/dist/extraction/languages/arkts.js +1290 -61
  57. package/dist/extraction/languages/arkts.js.map +1 -1
  58. package/dist/extraction/languages/c-cpp.d.ts +56 -0
  59. package/dist/extraction/languages/c-cpp.d.ts.map +1 -1
  60. package/dist/extraction/languages/c-cpp.js +198 -1
  61. package/dist/extraction/languages/c-cpp.js.map +1 -1
  62. package/dist/extraction/languages/ohos-api-consumer.d.ts +34 -0
  63. package/dist/extraction/languages/ohos-api-consumer.d.ts.map +1 -0
  64. package/dist/extraction/languages/ohos-api-consumer.js +283 -0
  65. package/dist/extraction/languages/ohos-api-consumer.js.map +1 -0
  66. package/dist/extraction/parse-pool.d.ts +126 -0
  67. package/dist/extraction/parse-pool.d.ts.map +1 -0
  68. package/dist/extraction/parse-pool.js +319 -0
  69. package/dist/extraction/parse-pool.js.map +1 -0
  70. package/dist/extraction/tree-sitter-types.d.ts +17 -0
  71. package/dist/extraction/tree-sitter-types.d.ts.map +1 -1
  72. package/dist/extraction/tree-sitter.d.ts +21 -0
  73. package/dist/extraction/tree-sitter.d.ts.map +1 -1
  74. package/dist/extraction/tree-sitter.js +198 -27
  75. package/dist/extraction/tree-sitter.js.map +1 -1
  76. package/dist/graph/traversal.d.ts.map +1 -1
  77. package/dist/graph/traversal.js +76 -17
  78. package/dist/graph/traversal.js.map +1 -1
  79. package/dist/index.d.ts +25 -0
  80. package/dist/index.d.ts.map +1 -1
  81. package/dist/index.js +78 -4
  82. package/dist/index.js.map +1 -1
  83. package/dist/mcp/daemon-paths.d.ts +30 -3
  84. package/dist/mcp/daemon-paths.d.ts.map +1 -1
  85. package/dist/mcp/daemon-paths.js +50 -10
  86. package/dist/mcp/daemon-paths.js.map +1 -1
  87. package/dist/mcp/daemon-registry.d.ts.map +1 -1
  88. package/dist/mcp/daemon-registry.js +7 -3
  89. package/dist/mcp/daemon-registry.js.map +1 -1
  90. package/dist/mcp/daemon.d.ts +48 -0
  91. package/dist/mcp/daemon.d.ts.map +1 -1
  92. package/dist/mcp/daemon.js +196 -32
  93. package/dist/mcp/daemon.js.map +1 -1
  94. package/dist/mcp/engine.d.ts +17 -0
  95. package/dist/mcp/engine.d.ts.map +1 -1
  96. package/dist/mcp/engine.js +73 -1
  97. package/dist/mcp/engine.js.map +1 -1
  98. package/dist/mcp/index.d.ts.map +1 -1
  99. package/dist/mcp/index.js +25 -43
  100. package/dist/mcp/index.js.map +1 -1
  101. package/dist/mcp/ppid-watchdog.d.ts +18 -0
  102. package/dist/mcp/ppid-watchdog.d.ts.map +1 -1
  103. package/dist/mcp/ppid-watchdog.js +37 -0
  104. package/dist/mcp/ppid-watchdog.js.map +1 -1
  105. package/dist/mcp/query-cache.d.ts +25 -0
  106. package/dist/mcp/query-cache.d.ts.map +1 -0
  107. package/dist/mcp/query-cache.js +191 -0
  108. package/dist/mcp/query-cache.js.map +1 -0
  109. package/dist/mcp/query-pool.d.ts +94 -0
  110. package/dist/mcp/query-pool.d.ts.map +1 -0
  111. package/dist/mcp/query-pool.js +297 -0
  112. package/dist/mcp/query-pool.js.map +1 -0
  113. package/dist/mcp/query-worker.d.ts +24 -0
  114. package/dist/mcp/query-worker.d.ts.map +1 -0
  115. package/dist/mcp/query-worker.js +87 -0
  116. package/dist/mcp/query-worker.js.map +1 -0
  117. package/dist/mcp/server-instructions.d.ts +5 -7
  118. package/dist/mcp/server-instructions.d.ts.map +1 -1
  119. package/dist/mcp/server-instructions.js +12 -14
  120. package/dist/mcp/server-instructions.js.map +1 -1
  121. package/dist/mcp/tools.d.ts +77 -0
  122. package/dist/mcp/tools.d.ts.map +1 -1
  123. package/dist/mcp/tools.js +696 -60
  124. package/dist/mcp/tools.js.map +1 -1
  125. package/dist/project-config.d.ts +20 -0
  126. package/dist/project-config.d.ts.map +1 -1
  127. package/dist/project-config.js +42 -2
  128. package/dist/project-config.js.map +1 -1
  129. package/dist/reasoning/login.js +1 -1
  130. package/dist/reasoning/login.js.map +1 -1
  131. package/dist/resolution/c-fnptr-synthesizer.d.ts +0 -28
  132. package/dist/resolution/c-fnptr-synthesizer.d.ts.map +1 -1
  133. package/dist/resolution/c-fnptr-synthesizer.js +765 -79
  134. package/dist/resolution/c-fnptr-synthesizer.js.map +1 -1
  135. package/dist/resolution/callback-synthesizer.d.ts +1 -1
  136. package/dist/resolution/callback-synthesizer.d.ts.map +1 -1
  137. package/dist/resolution/callback-synthesizer.js +72 -11
  138. package/dist/resolution/callback-synthesizer.js.map +1 -1
  139. package/dist/resolution/cooperative-yield.d.ts +32 -0
  140. package/dist/resolution/cooperative-yield.d.ts.map +1 -0
  141. package/dist/resolution/cooperative-yield.js +42 -0
  142. package/dist/resolution/cooperative-yield.js.map +1 -0
  143. package/dist/resolution/index.d.ts +11 -2
  144. package/dist/resolution/index.d.ts.map +1 -1
  145. package/dist/resolution/index.js +72 -4
  146. package/dist/resolution/index.js.map +1 -1
  147. package/dist/resolution/name-matcher.d.ts +22 -0
  148. package/dist/resolution/name-matcher.d.ts.map +1 -1
  149. package/dist/resolution/name-matcher.js +317 -20
  150. package/dist/resolution/name-matcher.js.map +1 -1
  151. package/dist/spec/config.d.ts +7 -11
  152. package/dist/spec/config.d.ts.map +1 -1
  153. package/dist/spec/config.js +77 -48
  154. package/dist/spec/config.js.map +1 -1
  155. package/dist/spec/db/fragment-node.d.ts +1 -0
  156. package/dist/spec/db/fragment-node.d.ts.map +1 -1
  157. package/dist/spec/db/fragment-node.js +8 -0
  158. package/dist/spec/db/fragment-node.js.map +1 -1
  159. package/dist/spec/db/fts.d.ts +14 -0
  160. package/dist/spec/db/fts.d.ts.map +1 -1
  161. package/dist/spec/db/fts.js +43 -4
  162. package/dist/spec/db/fts.js.map +1 -1
  163. package/dist/spec/db/schema.d.ts +6 -3
  164. package/dist/spec/db/schema.d.ts.map +1 -1
  165. package/dist/spec/db/schema.js +36 -4
  166. package/dist/spec/db/schema.js.map +1 -1
  167. package/dist/spec/db/schema.sql +11 -1
  168. package/dist/spec/evolve/logic-checker.d.ts +2 -2
  169. package/dist/spec/evolve/logic-checker.d.ts.map +1 -1
  170. package/dist/spec/evolve/logic-checker.js +3 -27
  171. package/dist/spec/evolve/logic-checker.js.map +1 -1
  172. package/dist/spec/evolve/pipeline.d.ts +21 -19
  173. package/dist/spec/evolve/pipeline.d.ts.map +1 -1
  174. package/dist/spec/evolve/pipeline.js +284 -31
  175. package/dist/spec/evolve/pipeline.js.map +1 -1
  176. package/dist/spec/evolve/spec-rewriter.d.ts +2 -2
  177. package/dist/spec/evolve/spec-rewriter.d.ts.map +1 -1
  178. package/dist/spec/evolve/spec-rewriter.js +7 -31
  179. package/dist/spec/evolve/spec-rewriter.js.map +1 -1
  180. package/dist/spec/graph/queries.d.ts +97 -0
  181. package/dist/spec/graph/queries.d.ts.map +1 -1
  182. package/dist/spec/graph/queries.js +225 -0
  183. package/dist/spec/graph/queries.js.map +1 -1
  184. package/dist/spec/llm/client.d.ts +29 -0
  185. package/dist/spec/llm/client.d.ts.map +1 -0
  186. package/dist/spec/llm/client.js +123 -0
  187. package/dist/spec/llm/client.js.map +1 -0
  188. package/dist/spec/llm/index.d.ts +3 -0
  189. package/dist/spec/llm/index.d.ts.map +1 -0
  190. package/dist/spec/llm/index.js +11 -0
  191. package/dist/spec/llm/index.js.map +1 -0
  192. package/dist/spec/llm/prompts.d.ts +13 -0
  193. package/dist/spec/llm/prompts.d.ts.map +1 -0
  194. package/dist/spec/llm/prompts.js +75 -0
  195. package/dist/spec/llm/prompts.js.map +1 -0
  196. package/dist/spec/mining/git-scanner.d.ts +12 -0
  197. package/dist/spec/mining/git-scanner.d.ts.map +1 -1
  198. package/dist/spec/mining/git-scanner.js +41 -0
  199. package/dist/spec/mining/git-scanner.js.map +1 -1
  200. package/dist/spec/mining/pipeline.d.ts.map +1 -1
  201. package/dist/spec/mining/pipeline.js +14 -1
  202. package/dist/spec/mining/pipeline.js.map +1 -1
  203. package/dist/spec/utils.d.ts +2 -1
  204. package/dist/spec/utils.d.ts.map +1 -1
  205. package/dist/spec/utils.js +3 -1
  206. package/dist/spec/utils.js.map +1 -1
  207. package/dist/sync/worktree.d.ts +9 -0
  208. package/dist/sync/worktree.d.ts.map +1 -1
  209. package/dist/sync/worktree.js +40 -0
  210. package/dist/sync/worktree.js.map +1 -1
  211. package/dist/types.d.ts +6 -1
  212. package/dist/types.d.ts.map +1 -1
  213. package/dist/ui/shimmer-progress.d.ts +2 -0
  214. package/dist/ui/shimmer-progress.d.ts.map +1 -1
  215. package/dist/ui/shimmer-progress.js +19 -2
  216. package/dist/ui/shimmer-progress.js.map +1 -1
  217. package/dist/upgrade/index.js +1 -1
  218. package/dist/upgrade/index.js.map +1 -1
  219. package/package.json +3 -3
  220. package/scripts/agent-eval/repro-concurrent-explore.mjs +119 -0
  221. package/scripts/agent-eval/repro-daemon-clients.mjs +125 -0
  222. package/scripts/exp_boundary_eval/README.md +247 -0
  223. package/scripts/exp_boundary_eval/__pycache__/_utils.cpython-310.pyc +0 -0
  224. package/scripts/exp_boundary_eval/__pycache__/analyze.cpython-310.pyc +0 -0
  225. package/scripts/exp_boundary_eval/__pycache__/deveco_arm.cpython-310.pyc +0 -0
  226. package/scripts/exp_boundary_eval/__pycache__/run_one.cpython-310.pyc +0 -0
  227. package/scripts/exp_boundary_eval/__pycache__/run_session.cpython-310.pyc +0 -0
  228. package/scripts/exp_boundary_eval/__pycache__/setup.cpython-310.pyc +0 -0
  229. package/scripts/exp_boundary_eval/_test_mcp_chain.py +78 -0
  230. package/scripts/exp_boundary_eval/_test_stdin.py +8 -0
  231. package/scripts/exp_boundary_eval/_utils.py +1116 -0
  232. package/scripts/exp_boundary_eval/analyze.py +1313 -0
  233. package/scripts/exp_boundary_eval/data/agents.json +109 -0
  234. package/scripts/exp_boundary_eval/data/experiments.json +140 -0
  235. package/scripts/exp_boundary_eval/deveco_arm.py +519 -0
  236. package/scripts/exp_boundary_eval/run_all.py +378 -0
  237. package/scripts/exp_boundary_eval/run_one.py +165 -0
  238. package/scripts/exp_boundary_eval/run_session.py +158 -0
  239. package/scripts/exp_boundary_eval/setup.py +120 -0
  240. package/scripts/exp_boundary_eval/win_mcp_launcher.py +73 -0
  241. package/scripts/exp_boundary_eval/win_mcp_stdio_wrap.js +36 -0
  242. package/scripts/exp_boundary_eval/win_node_launcher.py +24 -0
  243. package/scripts/npm-shim.js +25 -3
  244. package/scripts/ohos-sdk-publish.mjs +133 -0
  245. package/scripts/qa_eval/README.md +1 -1
  246. package/scripts/qa_eval/agent_runner.py +1 -1
@@ -16,11 +16,38 @@
16
16
  * an absolute-path hash under `os.tmpdir()`. The pidfile always stays in the
17
17
  * project (it doesn't have a length limit) — and acts as the authoritative
18
18
  * pointer to the socket path the daemon chose.
19
+ *
20
+ * Second special-case (#997, #974): some filesystems can't host an AF_UNIX node
21
+ * AT ALL — ExFAT/FAT external volumes, certain network mounts, WSL2 DrvFs — so
22
+ * `listen()` throws ENOTSUP/EACCES regardless of path length. We can't cheaply
23
+ * tell those apart from a normal volume up front, so instead of guessing we
24
+ * expose an ORDERED candidate list (`getDaemonSocketCandidates`): the in-project
25
+ * path first, the deterministic tmpdir path as the fallback of last resort. The
26
+ * daemon binds the first that works (relocating past a capability error); the
27
+ * proxy connects the first that answers. Both walk the SAME list, so they still
28
+ * converge on whichever the daemon bound with zero coordination.
29
+ */
30
+ /**
31
+ * Ordered socket / named-pipe path candidates the daemon should try to bind (and
32
+ * the proxy should try to connect) for `projectRoot`, most-preferred first.
33
+ * Deterministic given a project root, so independent processes converge without
34
+ * coordination — even when the preferred candidate is unusable and both fall
35
+ * through to the same fallback.
36
+ *
37
+ * - Windows: a single named pipe (lives in the kernel pipe namespace, not on
38
+ * the project FS, so neither the length nor the ExFAT hazard applies).
39
+ * - Short in-project path: `[ .codegraph/daemon.sock , <tmpdir> ]` — try the
40
+ * project first, fall back to tmpdir if its FS can't host a socket (#997).
41
+ * - Long in-project path (deep monorepos, Bazel out dirs): `[ <tmpdir> ]` only
42
+ * — bind would throw ENAMETOOLONG, so we skip straight to tmpdir.
19
43
  */
44
+ export declare function getDaemonSocketCandidates(projectRoot: string): string[];
20
45
  /**
21
- * Compute the socket / named-pipe path the daemon should listen on (and the
22
- * proxy should connect to) for `projectRoot`. Deterministic given a project
23
- * root, so independent processes converge without coordination.
46
+ * The PREFERRED (primary) socket path candidate 0. Use this only where a
47
+ * single representative path is wanted (the lockfile's informational
48
+ * `socketPath` field, status display). For binding/connecting, walk the full
49
+ * {@link getDaemonSocketCandidates} list — the daemon may bind a fallback when
50
+ * candidate 0 is unusable.
24
51
  */
25
52
  export declare function getDaemonSocketPath(projectRoot: string): string;
26
53
  /** Absolute path to the daemon pid lockfile for `projectRoot`. */
@@ -1 +1 @@
1
- {"version":3,"file":"daemon-paths.d.ts","sourceRoot":"","sources":["../../src/mcp/daemon-paths.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAeH;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAS/D;AAED,kEAAkE;AAClE,wBAAgB,gBAAgB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAE5D;AAED,+CAA+C;AAC/C,MAAM,WAAW,cAAc;IAC7B,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,cAAc,GAAG,MAAM,CAE3D;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,cAAc,GAAG,IAAI,CAuBjE"}
1
+ {"version":3,"file":"daemon-paths.d.ts","sourceRoot":"","sources":["../../src/mcp/daemon-paths.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AA0BH;;;;;;;;;;;;;GAaG;AACH,wBAAgB,yBAAyB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,EAAE,CAQvE;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAG/D;AAED,kEAAkE;AAClE,wBAAgB,gBAAgB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAE5D;AAED,+CAA+C;AAC/C,MAAM,WAAW,cAAc;IAC7B,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,cAAc,GAAG,MAAM,CAE3D;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,cAAc,GAAG,IAAI,CAuBjE"}
@@ -17,6 +17,16 @@
17
17
  * an absolute-path hash under `os.tmpdir()`. The pidfile always stays in the
18
18
  * project (it doesn't have a length limit) — and acts as the authoritative
19
19
  * pointer to the socket path the daemon chose.
20
+ *
21
+ * Second special-case (#997, #974): some filesystems can't host an AF_UNIX node
22
+ * AT ALL — ExFAT/FAT external volumes, certain network mounts, WSL2 DrvFs — so
23
+ * `listen()` throws ENOTSUP/EACCES regardless of path length. We can't cheaply
24
+ * tell those apart from a normal volume up front, so instead of guessing we
25
+ * expose an ORDERED candidate list (`getDaemonSocketCandidates`): the in-project
26
+ * path first, the deterministic tmpdir path as the fallback of last resort. The
27
+ * daemon binds the first that works (relocating past a capability error); the
28
+ * proxy connects the first that answers. Both walk the SAME list, so they still
29
+ * converge on whichever the daemon bound with zero coordination.
20
30
  */
21
31
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
22
32
  if (k2 === undefined) k2 = k;
@@ -52,6 +62,7 @@ var __importStar = (this && this.__importStar) || (function () {
52
62
  };
53
63
  })();
54
64
  Object.defineProperty(exports, "__esModule", { value: true });
65
+ exports.getDaemonSocketCandidates = getDaemonSocketCandidates;
55
66
  exports.getDaemonSocketPath = getDaemonSocketPath;
56
67
  exports.getDaemonPidPath = getDaemonPidPath;
57
68
  exports.encodeLockInfo = encodeLockInfo;
@@ -67,20 +78,49 @@ function projectHash(projectRoot) {
67
78
  return crypto.createHash('sha256').update(path.resolve(projectRoot)).digest('hex').slice(0, 16);
68
79
  }
69
80
  /**
70
- * Compute the socket / named-pipe path the daemon should listen on (and the
71
- * proxy should connect to) for `projectRoot`. Deterministic given a project
72
- * root, so independent processes converge without coordination.
81
+ * The deterministic tmpdir socket path for `projectRoot` the fallback used
82
+ * when the in-project location can't host a socket (too long, or an FS that
83
+ * doesn't support AF_UNIX). Hash keeps it project-scoped, and being purely a
84
+ * function of the root means the daemon and the proxy compute the identical
85
+ * path without talking to each other.
73
86
  */
74
- function getDaemonSocketPath(projectRoot) {
87
+ function tmpdirSocketPath(projectRoot) {
88
+ return path.join(os.tmpdir(), `homegraph-${projectHash(projectRoot)}.sock`);
89
+ }
90
+ /**
91
+ * Ordered socket / named-pipe path candidates the daemon should try to bind (and
92
+ * the proxy should try to connect) for `projectRoot`, most-preferred first.
93
+ * Deterministic given a project root, so independent processes converge without
94
+ * coordination — even when the preferred candidate is unusable and both fall
95
+ * through to the same fallback.
96
+ *
97
+ * - Windows: a single named pipe (lives in the kernel pipe namespace, not on
98
+ * the project FS, so neither the length nor the ExFAT hazard applies).
99
+ * - Short in-project path: `[ .codegraph/daemon.sock , <tmpdir> ]` — try the
100
+ * project first, fall back to tmpdir if its FS can't host a socket (#997).
101
+ * - Long in-project path (deep monorepos, Bazel out dirs): `[ <tmpdir> ]` only
102
+ * — bind would throw ENAMETOOLONG, so we skip straight to tmpdir.
103
+ */
104
+ function getDaemonSocketCandidates(projectRoot) {
75
105
  if (process.platform === 'win32') {
76
- return `\\\\.\\pipe\\homegraph-${projectHash(projectRoot)}`;
106
+ return [`\\\\.\\pipe\\homegraph-${projectHash(projectRoot)}`];
77
107
  }
78
108
  const inProject = path.join((0, directory_1.getHomeGraphDir)(projectRoot), 'daemon.sock');
79
- if (inProject.length <= POSIX_SOCKET_PATH_LIMIT)
80
- return inProject;
81
- // Long project paths (deep monorepos, Bazel out dirs) need tmpdir fallback
82
- // or `bind` returns EADDRINUSE / ENAMETOOLONG. Hash keeps it project-scoped.
83
- return path.join(os.tmpdir(), `homegraph-${projectHash(projectRoot)}.sock`);
109
+ const tmp = tmpdirSocketPath(projectRoot);
110
+ if (inProject.length > POSIX_SOCKET_PATH_LIMIT)
111
+ return [tmp];
112
+ return [inProject, tmp];
113
+ }
114
+ /**
115
+ * The PREFERRED (primary) socket path — candidate 0. Use this only where a
116
+ * single representative path is wanted (the lockfile's informational
117
+ * `socketPath` field, status display). For binding/connecting, walk the full
118
+ * {@link getDaemonSocketCandidates} list — the daemon may bind a fallback when
119
+ * candidate 0 is unusable.
120
+ */
121
+ function getDaemonSocketPath(projectRoot) {
122
+ // The candidate list is never empty (≥1 on every platform), so [0] is safe.
123
+ return getDaemonSocketCandidates(projectRoot)[0];
84
124
  }
85
125
  /** Absolute path to the daemon pid lockfile for `projectRoot`. */
86
126
  function getDaemonPidPath(projectRoot) {
@@ -1 +1 @@
1
- {"version":3,"file":"daemon-paths.js","sourceRoot":"","sources":["../../src/mcp/daemon-paths.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;GAkBG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoBH,kDASC;AAGD,4CAEC;AAcD,wCAEC;AAOD,wCAuBC;AA9ED,+CAAiC;AACjC,uCAAyB;AACzB,2CAA6B;AAC7B,4CAA+C;AAE/C,oDAAoD;AACpD,MAAM,uBAAuB,GAAG,GAAG,CAAC;AAEpC,8EAA8E;AAC9E,SAAS,WAAW,CAAC,WAAmB;IACtC,OAAO,MAAM,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;AAClG,CAAC;AAED;;;;GAIG;AACH,SAAgB,mBAAmB,CAAC,WAAmB;IACrD,IAAI,OAAO,CAAC,QAAQ,KAAK,OAAO,EAAE,CAAC;QACjC,OAAO,0BAA0B,WAAW,CAAC,WAAW,CAAC,EAAE,CAAC;IAC9D,CAAC;IACD,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,IAAA,2BAAe,EAAC,WAAW,CAAC,EAAE,aAAa,CAAC,CAAC;IACzE,IAAI,SAAS,CAAC,MAAM,IAAI,uBAAuB;QAAE,OAAO,SAAS,CAAC;IAClE,2EAA2E;IAC3E,6EAA6E;IAC7E,OAAO,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE,EAAE,aAAa,WAAW,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;AAC9E,CAAC;AAED,kEAAkE;AAClE,SAAgB,gBAAgB,CAAC,WAAmB;IAClD,OAAO,IAAI,CAAC,IAAI,CAAC,IAAA,2BAAe,EAAC,WAAW,CAAC,EAAE,YAAY,CAAC,CAAC;AAC/D,CAAC;AAUD;;;GAGG;AACH,SAAgB,cAAc,CAAC,IAAoB;IACjD,OAAO,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,IAAI,CAAC;AAC9C,CAAC;AAED;;;;GAIG;AACH,SAAgB,cAAc,CAAC,GAAW;IACxC,MAAM,OAAO,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC;IAC3B,IAAI,CAAC,OAAO;QAAE,OAAO,IAAI,CAAC;IAC1B,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;QACnC,IACE,MAAM;YACN,OAAO,MAAM,CAAC,GAAG,KAAK,QAAQ;YAC9B,OAAO,MAAM,CAAC,OAAO,KAAK,QAAQ;YAClC,OAAO,MAAM,CAAC,UAAU,KAAK,QAAQ;YACrC,OAAO,MAAM,CAAC,SAAS,KAAK,QAAQ,EACpC,CAAC;YACD,OAAO,MAAwB,CAAC;QAClC,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,MAAM,CAAC;QACP,6CAA6C;IAC/C,CAAC;IACD,MAAM,GAAG,GAAG,MAAM,CAAC,OAAO,CAAC,CAAC;IAC5B,IAAI,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,GAAG,GAAG,CAAC,EAAE,CAAC;QACpC,OAAO,EAAE,GAAG,EAAE,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,EAAE,EAAE,SAAS,EAAE,CAAC,EAAE,CAAC;IACnE,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC"}
1
+ {"version":3,"file":"daemon-paths.js","sourceRoot":"","sources":["../../src/mcp/daemon-paths.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"daemon-registry.d.ts","sourceRoot":"","sources":["../../src/mcp/daemon-registry.ts"],"names":[],"mappings":"AA0BA,MAAM,WAAW,YAAY;IAC3B,iDAAiD;IACjD,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,iDAAiD;IACjD,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;GAGG;AACH,wBAAgB,cAAc,IAAI,MAAM,CAEvC;AAOD;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAQnD;AAED,0EAA0E;AAC1E,wBAAgB,cAAc,CAAC,GAAG,EAAE,YAAY,GAAG,IAAI,CAOtD;AAED,mEAAmE;AACnE,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAMnD;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,IAAI,GAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAA;CAAO,GAAG,YAAY,EAAE,CA2B1E;AAuBD,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,uFAAuF;IACvF,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,aAAa,GAAG,WAAW,CAAC;CACxD;AAED;;;;;GAKG;AACH,wBAAsB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAmCpE;AAED,0CAA0C;AAC1C,wBAAsB,cAAc,IAAI,OAAO,CAAC,UAAU,EAAE,CAAC,CAM5D"}
1
+ {"version":3,"file":"daemon-registry.d.ts","sourceRoot":"","sources":["../../src/mcp/daemon-registry.ts"],"names":[],"mappings":"AA0BA,MAAM,WAAW,YAAY;IAC3B,iDAAiD;IACjD,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,iDAAiD;IACjD,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;GAGG;AACH,wBAAgB,cAAc,IAAI,MAAM,CAEvC;AAOD;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAQnD;AAED,0EAA0E;AAC1E,wBAAgB,cAAc,CAAC,GAAG,EAAE,YAAY,GAAG,IAAI,CAOtD;AAED,mEAAmE;AACnE,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAMnD;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,IAAI,GAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAA;CAAO,GAAG,YAAY,EAAE,CA2B1E;AA2BD,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,uFAAuF;IACvF,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,aAAa,GAAG,WAAW,CAAC;CACxD;AAED;;;;;GAKG;AACH,wBAAsB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAmCpE;AAED,0CAA0C;AAC1C,wBAAsB,cAAc,IAAI,OAAO,CAAC,UAAU,EAAE,CAAC,CAM5D"}
@@ -155,11 +155,15 @@ function cleanupDaemonArtifacts(root) {
155
155
  }
156
156
  catch { /* gone */ }
157
157
  // POSIX sockets are real files; Windows named pipes vanish with the process.
158
+ // Sweep every candidate — a daemon that relocated past an unusable in-project
159
+ // FS (ExFAT/FAT; #997) left its socket at the tmpdir fallback, not candidate 0.
158
160
  if (process.platform !== 'win32') {
159
- try {
160
- fs.unlinkSync((0, daemon_paths_1.getDaemonSocketPath)(root));
161
+ for (const candidate of (0, daemon_paths_1.getDaemonSocketCandidates)(root)) {
162
+ try {
163
+ fs.unlinkSync(candidate);
164
+ }
165
+ catch { /* gone */ }
161
166
  }
162
- catch { /* gone */ }
163
167
  }
164
168
  deregisterDaemon(root);
165
169
  }
@@ -1 +1 @@
1
- {"version":3,"file":"daemon-registry.js","sourceRoot":"","sources":["../../src/mcp/daemon-registry.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"daemon-registry.js","sourceRoot":"","sources":["../../src/mcp/daemon-registry.ts"],"names":[],"mappings":"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"}
@@ -39,7 +39,18 @@
39
39
  * - The decision of *whether* to run as daemon at all — that's `MCPServer`.
40
40
  * - The MCP protocol state machine — that's `./session.ts`.
41
41
  */
42
+ import * as net from 'net';
42
43
  import { DaemonLockInfo } from './daemon-paths';
44
+ /**
45
+ * Finalize daemon shutdown. On POSIX, exit immediately — it's clean and fast.
46
+ * On Windows, do NOT force an exit while watchers may still be closing (that
47
+ * trips the libuv assertion above); instead mark success and let the loop drain
48
+ * to a natural exit, with an UNREF'd backstop that force-exits only if a stray
49
+ * handle would otherwise hang shutdown. Pure and platform-injected so both
50
+ * branches are unit-testable off-Windows. Returns the backstop timer (Windows)
51
+ * so callers/tests can clear it.
52
+ */
53
+ export declare function finalizeDaemonExit(platform: NodeJS.Platform, exit: (code: number) => void): NodeJS.Timeout | null;
43
54
  /** Bytes/parse-window for an oversized hello line — bounded against a malicious peer. */
44
55
  declare const MAX_HELLO_LINE_BYTES = 4096;
45
56
  /**
@@ -177,8 +188,29 @@ export type AcquireResult = {
177
188
  * the pidfile becomes visible in one step already containing a full record.
178
189
  * Whoever links first wins; everyone else gets EEXIST and reads a complete file.
179
190
  * There is no empty-file window at all.
191
+ *
192
+ * Filesystems without hard links (#997): ExFAT/FAT external volumes and some
193
+ * network mounts can't `link()` at all — it throws ENOTSUP/EPERM, which would
194
+ * otherwise kill the daemon before it ever reaches the socket bind. There we
195
+ * fall back to an O_EXCL create (`acquireLockViaExclusiveOpen`): still exclusive
196
+ * ("first writer wins"), but the full record is written through the fd in a
197
+ * second step, so the empty-file window the link approach removed is reopened —
198
+ * only on these filesystems, only for the microseconds between create and write
199
+ * (far narrower than the original bug, which the file watcher's startup latency
200
+ * widened). The race's worst case is two daemons briefly; on a single external
201
+ * drive that's strictly better than the daemon never starting at all.
180
202
  */
181
203
  export declare function tryAcquireDaemonLock(projectRoot: string): AcquireResult;
204
+ /**
205
+ * Exclusive-create the pidfile (O_CREAT|O_EXCL via the `wx` flag) and write the
206
+ * full record through the same fd — the hard-link-free fallback used by
207
+ * {@link tryAcquireDaemonLock} on filesystems without `link()`. Returns true if
208
+ * we created it (acquired the lock), false on EEXIST (another candidate holds
209
+ * it). Any other error propagates. Still exclusive, so "first writer wins" holds
210
+ * exactly as the link path does; the only difference is the brief empty-file
211
+ * window between create and write. Exported for testing.
212
+ */
213
+ export declare function acquireLockViaExclusiveOpen(pidPath: string, info: DaemonLockInfo): boolean;
182
214
  /**
183
215
  * Remove a stale pidfile, but only if it still names a dead process. Re-reads
184
216
  * the file immediately before unlinking so we never delete a lock that a live
@@ -197,6 +229,22 @@ export declare function clearStaleDaemonLock(pidPath: string, expectedDeadPid?:
197
229
  * mistake a live daemon for a dead one and clear its lock.
198
230
  */
199
231
  export declare function isProcessAlive(pid: number): boolean;
232
+ /**
233
+ * Bind the first usable socket from an ordered candidate list, relocating past
234
+ * any path that fails to bind for a non-conflict reason (see {@link
235
+ * SOCKET_BIND_CONFLICT_CODE}). The injected `listen` does the real
236
+ * `net.Server.listen` (and stale-socket clear); abstracted so the relocation
237
+ * policy is unit-testable without a real unsupported filesystem. Returns the
238
+ * server plus the path actually bound. An EADDRINUSE, or any error on the LAST
239
+ * candidate, propagates — the caller releases the lockfile and falls back to
240
+ * direct mode (#974). Exported for testing.
241
+ */
242
+ export declare function bindFirstUsableSocket(candidates: string[], listen: (socketPath: string) => Promise<net.Server>, opts?: {
243
+ onRelocate?: (from: string, to: string, code: string) => void;
244
+ }): Promise<{
245
+ server: net.Server;
246
+ socketPath: string;
247
+ }>;
200
248
  /**
201
249
  * Parse one client-hello line. Returns the peer pids if `line` is a well-formed
202
250
  * client-hello (carries the `homegraph_client` marker), or null otherwise — in
@@ -1 +1 @@
1
- {"version":3,"file":"daemon.d.ts","sourceRoot":"","sources":["../../src/mcp/daemon.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"daemon.d.ts","sourceRoot":"","sources":["../../src/mcp/daemon.ts"],"names":[],"mappings":"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"}
@@ -75,9 +75,12 @@ var __importStar = (this && this.__importStar) || (function () {
75
75
  })();
76
76
  Object.defineProperty(exports, "__esModule", { value: true });
77
77
  exports.MAX_HELLO_LINE_BYTES = exports.Daemon = void 0;
78
+ exports.finalizeDaemonExit = finalizeDaemonExit;
78
79
  exports.tryAcquireDaemonLock = tryAcquireDaemonLock;
80
+ exports.acquireLockViaExclusiveOpen = acquireLockViaExclusiveOpen;
79
81
  exports.clearStaleDaemonLock = clearStaleDaemonLock;
80
82
  exports.isProcessAlive = isProcessAlive;
83
+ exports.bindFirstUsableSocket = bindFirstUsableSocket;
81
84
  exports.parseClientHelloLine = parseClientHelloLine;
82
85
  exports.peerIsDead = peerIsDead;
83
86
  const fs = __importStar(require("fs"));
@@ -100,6 +103,40 @@ const DEFAULT_IDLE_TIMEOUT_MS = 300_000;
100
103
  * Set generously so a real but momentarily-idle session isn't reaped mid-use.
101
104
  */
102
105
  const DEFAULT_MAX_IDLE_MS = 1_800_000; // 30 min
106
+ /**
107
+ * Windows-only shutdown backstop. On Windows, calling `process.exit()` while a
108
+ * recursive `fs.watch` handle is still tearing down aborts the process with a
109
+ * libuv `UV_HANDLE_CLOSING` assertion (`0xC0000409`) — reproducible whenever the
110
+ * watched tree contains a nested repo (submodule / embedded clone), since that's
111
+ * what keeps a watch active at shutdown. The fix is to let the event loop drain
112
+ * so libuv finishes closing those handles, then exit naturally; this timer only
113
+ * force-exits if some unexpected handle keeps the loop alive past the grace
114
+ * window. Kept short so shutdown stays snappy in that fallback. See
115
+ * `finalizeDaemonExit`.
116
+ */
117
+ const DAEMON_SHUTDOWN_BACKSTOP_MS = 2_000;
118
+ /**
119
+ * Finalize daemon shutdown. On POSIX, exit immediately — it's clean and fast.
120
+ * On Windows, do NOT force an exit while watchers may still be closing (that
121
+ * trips the libuv assertion above); instead mark success and let the loop drain
122
+ * to a natural exit, with an UNREF'd backstop that force-exits only if a stray
123
+ * handle would otherwise hang shutdown. Pure and platform-injected so both
124
+ * branches are unit-testable off-Windows. Returns the backstop timer (Windows)
125
+ * so callers/tests can clear it.
126
+ */
127
+ function finalizeDaemonExit(platform, exit) {
128
+ if (platform === 'win32') {
129
+ process.exitCode = 0;
130
+ const backstop = setTimeout(() => exit(0), DAEMON_SHUTDOWN_BACKSTOP_MS);
131
+ // Unref so it never keeps the loop alive: a natural drain (watchers closed,
132
+ // nothing else pending) exits before it fires; it only fires when some other
133
+ // handle is keeping the loop running, which is exactly when we need it.
134
+ backstop.unref?.();
135
+ return backstop;
136
+ }
137
+ exit(0);
138
+ return null;
139
+ }
103
140
  /** How often the daemon sweeps connected clients for a dead peer process (#692). */
104
141
  const DEFAULT_CLIENT_SWEEP_MS = 30_000;
105
142
  /** How long the daemon waits for the optional client-hello before proceeding without it. */
@@ -140,7 +177,12 @@ class Daemon {
140
177
  this.pidPath = (0, daemon_paths_1.getDaemonPidPath)(projectRoot);
141
178
  this.idleTimeoutMs = opts.idleTimeoutMs ?? resolveIdleTimeoutMs();
142
179
  this.maxIdleMs = opts.maxIdleMs ?? resolveMaxIdleMs();
143
- this.engine = new engine_1.MCPEngine();
180
+ // Daemon mode serves many concurrent clients on one event loop, so off-load
181
+ // read-tool dispatch to a worker pool — otherwise concurrent explores
182
+ // serialize and starve the MCP transport (clients time out). Direct mode
183
+ // (one stdio client) leaves the pool off; `CODEGRAPH_QUERY_POOL_SIZE=0`
184
+ // disables it here too.
185
+ this.engine = new engine_1.MCPEngine({ queryPool: true });
144
186
  this.engine.setProjectPathHint(projectRoot);
145
187
  }
146
188
  /**
@@ -154,53 +196,76 @@ class Daemon {
154
196
  // to land waits on `ensureInitialized` either way, and unloaded sessions
155
197
  // (cross-project tool calls only) shouldn't pay any open cost.
156
198
  void this.engine.ensureInitialized(this.projectRoot);
157
- // Stale socket file (left over from a SIGKILL'd previous daemon) will
158
- // wedge `listen` with EADDRINUSE. We arrived here holding the lockfile,
159
- // which means there's no live daemon, so it's safe to clear.
160
- if (process.platform !== 'win32') {
161
- try {
162
- fs.unlinkSync(this.socketPath);
199
+ const candidates = (0, daemon_paths_1.getDaemonSocketCandidates)(this.projectRoot);
200
+ const listen = (socketPath) => new Promise((resolve, reject) => {
201
+ if (process.platform !== 'win32') {
202
+ try {
203
+ fs.unlinkSync(socketPath);
204
+ }
205
+ catch { /* not-exists is fine */ }
163
206
  }
164
- catch { /* not-exists is fine */ }
165
- }
166
- await new Promise((resolve, reject) => {
167
207
  const server = net.createServer((socket) => this.handleConnection(socket));
168
- server.once('error', (err) => reject(err));
169
- server.listen(this.socketPath, () => {
208
+ server.once('error', reject);
209
+ server.listen(socketPath, () => {
170
210
  // POSIX: tighten permissions to user-only — the socket lives under
171
- // `.homegraph/`, which is git-ignored but may be on a shared FS.
211
+ // `.homegraph/` (git-ignored, maybe a shared FS) or tmpdir.
172
212
  if (process.platform !== 'win32') {
173
213
  try {
174
- fs.chmodSync(this.socketPath, 0o600);
214
+ fs.chmodSync(socketPath, 0o600);
175
215
  }
176
216
  catch { /* best-effort */ }
177
217
  }
178
- this.server = server;
179
- resolve();
218
+ resolve(server);
180
219
  });
181
- }).catch((err) => {
182
- // Bind failed — e.g. AF_UNIX is unsupported/unreliable on this filesystem
183
- // (the WSL2 DrvFs hazard behind #974), or a stale socket we couldn't clear.
184
- // We already hold the lockfile that `tryAcquireDaemonLock` wrote; release it
185
- // and any partial socket so the NEXT launcher doesn't spin respawning us on
186
- // a stale lock that points at our now-dying pid. Then re-throw so the caller
187
- // (the bin's try/catch) exits this detached daemon cleanly and every
188
- // launcher falls back to direct mode.
220
+ });
221
+ let bound;
222
+ try {
223
+ bound = await bindFirstUsableSocket(candidates, listen, {
224
+ onRelocate: (from, to, code) => process.stderr.write(`[CodeGraph daemon] Socket ${from} unusable (${code}); relocating to ${to}.\n`),
225
+ });
226
+ }
227
+ catch (err) {
228
+ // Every candidate failed (the last one, or a non-relocatable error like a
229
+ // racing EADDRINUSE). We already hold the lockfile `tryAcquireDaemonLock`
230
+ // wrote; release it and any partial sockets so the NEXT launcher doesn't
231
+ // spin respawning us on a stale lock pointing at our now-dying pid. Then
232
+ // re-throw so the caller (the bin's try/catch) exits this detached daemon
233
+ // cleanly and every launcher falls back to direct mode (#974).
189
234
  this.cleanupLockfile();
190
235
  if (process.platform !== 'win32') {
191
- try {
192
- fs.unlinkSync(this.socketPath);
236
+ for (const candidate of candidates) {
237
+ try {
238
+ fs.unlinkSync(candidate);
239
+ }
240
+ catch { /* may not exist */ }
193
241
  }
194
- catch { /* may not exist */ }
195
242
  }
196
243
  throw err;
197
- });
244
+ }
245
+ this.server = bound.server;
246
+ // Adopt the path we ACTUALLY bound — it may be a tmpdir fallback past an
247
+ // unusable in-project location. Everything downstream (lockfile, registry,
248
+ // chmod, cleanup, status) keys off this real path, not the preferred guess.
249
+ this.socketPath = bound.socketPath;
198
250
  const lock = {
199
251
  pid: process.pid,
200
252
  version: version_1.HomeGraphPackageVersion,
201
253
  socketPath: this.socketPath,
202
254
  startedAt: Date.now(),
203
255
  };
256
+ // `tryAcquireDaemonLock` wrote the pidfile with the PREFERRED path (candidate
257
+ // 0) before we knew which one would bind. If we relocated, rewrite it so the
258
+ // per-project record is honest. Atomic temp+rename; safe because we hold the
259
+ // lock and we're alive — `clearStaleDaemonLock` pid-verifies, so no racing
260
+ // candidate clears or clobbers a live daemon's lock.
261
+ if (this.socketPath !== candidates[0]) {
262
+ try {
263
+ const tmpPid = `${this.pidPath}.${process.pid}.relocate`;
264
+ fs.writeFileSync(tmpPid, (0, daemon_paths_1.encodeLockInfo)(lock), { mode: 0o600 });
265
+ fs.renameSync(tmpPid, this.pidPath);
266
+ }
267
+ catch { /* best-effort; the registry record below carries the real path */ }
268
+ }
204
269
  // Drop a discovery record so `homegraph list` / `stop --all` can find us.
205
270
  // Best-effort; a missing record only means list's liveness prune covers it.
206
271
  (0, daemon_registry_1.registerDaemon)({ root: this.projectRoot, ...lock });
@@ -260,7 +325,10 @@ class Daemon {
260
325
  }
261
326
  catch { /* may already be gone */ }
262
327
  }
263
- process.exit(0);
328
+ // POSIX exits here; Windows drains first (engine.stop() above began closing
329
+ // the file watcher, and exiting mid-teardown aborts the process). See
330
+ // finalizeDaemonExit / DAEMON_SHUTDOWN_BACKSTOP_MS.
331
+ finalizeDaemonExit(process.platform, (code) => process.exit(code));
264
332
  }
265
333
  handleConnection(socket) {
266
334
  // Hello first so the proxy can verify versions before piping any
@@ -415,6 +483,17 @@ exports.Daemon = Daemon;
415
483
  * the pidfile becomes visible in one step already containing a full record.
416
484
  * Whoever links first wins; everyone else gets EEXIST and reads a complete file.
417
485
  * There is no empty-file window at all.
486
+ *
487
+ * Filesystems without hard links (#997): ExFAT/FAT external volumes and some
488
+ * network mounts can't `link()` at all — it throws ENOTSUP/EPERM, which would
489
+ * otherwise kill the daemon before it ever reaches the socket bind. There we
490
+ * fall back to an O_EXCL create (`acquireLockViaExclusiveOpen`): still exclusive
491
+ * ("first writer wins"), but the full record is written through the fd in a
492
+ * second step, so the empty-file window the link approach removed is reopened —
493
+ * only on these filesystems, only for the microseconds between create and write
494
+ * (far narrower than the original bug, which the file watcher's startup latency
495
+ * widened). The race's worst case is two daemons briefly; on a single external
496
+ * drive that's strictly better than the daemon never starting at all.
418
497
  */
419
498
  function tryAcquireDaemonLock(projectRoot) {
420
499
  const pidPath = (0, daemon_paths_1.getDaemonPidPath)(projectRoot);
@@ -433,12 +512,23 @@ function tryAcquireDaemonLock(projectRoot) {
433
512
  try {
434
513
  fs.writeFileSync(tmp, (0, daemon_paths_1.encodeLockInfo)(info), { mode: 0o600 });
435
514
  try {
436
- fs.linkSync(tmp, pidPath); // atomic + exclusive
515
+ fs.linkSync(tmp, pidPath); // atomic + exclusive (race-free; see must-fix 1)
437
516
  acquired = true;
438
517
  }
439
518
  catch (err) {
440
- if (err.code !== 'EEXIST')
441
- throw err;
519
+ if (err.code === 'EEXIST') {
520
+ // Lost the race — another candidate already holds it. Fall through to read.
521
+ }
522
+ else {
523
+ // link() failed for a non-conflict reason — nearly always "this filesystem
524
+ // has no hard links" (ExFAT/FAT external volumes, some network mounts),
525
+ // which surfaces as a DIFFERENT errno on every OS: ENOTSUP on macOS, EPERM
526
+ // on Linux, EISDIR on Windows (#997). Enumerating them is whack-a-mole and
527
+ // unnecessary: the `tmp` write above already proved this directory is
528
+ // writable, so an O_EXCL create is a valid atomic+exclusive substitute. If
529
+ // IT fails too, that's a genuine error and propagates. EEXIST ⇒ taken.
530
+ acquired = acquireLockViaExclusiveOpen(pidPath, info);
531
+ }
442
532
  }
443
533
  }
444
534
  finally {
@@ -459,6 +549,33 @@ function tryAcquireDaemonLock(projectRoot) {
459
549
  catch { /* unreadable lockfile — treat as malformed */ }
460
550
  return { kind: 'taken', existing, pidPath };
461
551
  }
552
+ /**
553
+ * Exclusive-create the pidfile (O_CREAT|O_EXCL via the `wx` flag) and write the
554
+ * full record through the same fd — the hard-link-free fallback used by
555
+ * {@link tryAcquireDaemonLock} on filesystems without `link()`. Returns true if
556
+ * we created it (acquired the lock), false on EEXIST (another candidate holds
557
+ * it). Any other error propagates. Still exclusive, so "first writer wins" holds
558
+ * exactly as the link path does; the only difference is the brief empty-file
559
+ * window between create and write. Exported for testing.
560
+ */
561
+ function acquireLockViaExclusiveOpen(pidPath, info) {
562
+ let fd;
563
+ try {
564
+ fd = fs.openSync(pidPath, 'wx', 0o600); // O_CREAT | O_EXCL | O_WRONLY
565
+ }
566
+ catch (err) {
567
+ if (err.code === 'EEXIST')
568
+ return false;
569
+ throw err;
570
+ }
571
+ try {
572
+ fs.writeSync(fd, (0, daemon_paths_1.encodeLockInfo)(info));
573
+ }
574
+ finally {
575
+ fs.closeSync(fd);
576
+ }
577
+ return true;
578
+ }
462
579
  /**
463
580
  * Remove a stale pidfile, but only if it still names a dead process. Re-reads
464
581
  * the file immediately before unlinking so we never delete a lock that a live
@@ -509,6 +626,53 @@ function isProcessAlive(pid) {
509
626
  return false;
510
627
  }
511
628
  }
629
+ /**
630
+ * The one `listen()` error we must NOT relocate past. EADDRINUSE means the path
631
+ * is genuinely occupied — a racing daemon that legitimately owns it, or a
632
+ * leftover node we couldn't clear (the #974 planted-dir case) — so relocating
633
+ * would abandon a path another daemon owns; the caller instead releases its lock
634
+ * and falls back to direct mode. EVERY OTHER bind error just means "this path
635
+ * didn't work," almost always a filesystem that can't host an AF_UNIX node at all
636
+ * (ExFAT/FAT, network mounts, WSL2 DrvFs), which reports a DIFFERENT errno per OS
637
+ * (ENOTSUP macOS, EPERM Linux; #997). Enumerating the "unsupported" codes is
638
+ * whack-a-mole, so we relocate on anything-but-conflict instead — robust and
639
+ * self-correcting: if the deterministic tmpdir fallback ALSO fails, that error
640
+ * propagates from the last candidate. (ENAMETOOLONG never reaches here — the
641
+ * candidate list already routes over-long paths straight to tmpdir.)
642
+ */
643
+ const SOCKET_BIND_CONFLICT_CODE = 'EADDRINUSE';
644
+ /**
645
+ * Bind the first usable socket from an ordered candidate list, relocating past
646
+ * any path that fails to bind for a non-conflict reason (see {@link
647
+ * SOCKET_BIND_CONFLICT_CODE}). The injected `listen` does the real
648
+ * `net.Server.listen` (and stale-socket clear); abstracted so the relocation
649
+ * policy is unit-testable without a real unsupported filesystem. Returns the
650
+ * server plus the path actually bound. An EADDRINUSE, or any error on the LAST
651
+ * candidate, propagates — the caller releases the lockfile and falls back to
652
+ * direct mode (#974). Exported for testing.
653
+ */
654
+ async function bindFirstUsableSocket(candidates, listen, opts = {}) {
655
+ let lastErr;
656
+ for (let i = 0; i < candidates.length; i++) {
657
+ const socketPath = candidates[i]; // i < length, so always defined
658
+ const isLast = i === candidates.length - 1;
659
+ try {
660
+ const server = await listen(socketPath);
661
+ return { server, socketPath };
662
+ }
663
+ catch (err) {
664
+ lastErr = err;
665
+ const code = err.code;
666
+ if (!isLast && code !== SOCKET_BIND_CONFLICT_CODE) {
667
+ opts.onRelocate?.(socketPath, candidates[i + 1], code ?? ''); // !isLast ⇒ i+1 in range
668
+ continue;
669
+ }
670
+ throw err;
671
+ }
672
+ }
673
+ // Only reachable with an empty candidate list — a programmer error.
674
+ throw lastErr ?? new Error('no socket candidates to bind');
675
+ }
512
676
  function resolveIdleTimeoutMs() {
513
677
  const raw = process.env.HOMEGRAPH_DAEMON_IDLE_TIMEOUT_MS;
514
678
  if (raw === undefined || raw === '')