@adhdev/daemon-core 0.9.82-rc.36 → 0.9.82-rc.361

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 (335) hide show
  1. package/dist/boot/daemon-lifecycle.d.ts +8 -0
  2. package/dist/boot/process-hardening.d.ts +50 -0
  3. package/dist/build-info.d.ts +37 -0
  4. package/dist/chat/source-machine.d.ts +166 -0
  5. package/dist/chat/source-resolver.d.ts +104 -0
  6. package/dist/chat/subscription-updates.d.ts +1 -0
  7. package/dist/cli-adapter-types.d.ts +55 -2
  8. package/dist/cli-adapters/cli-script-runner.d.ts +117 -0
  9. package/dist/cli-adapters/cli-state-engine.d.ts +198 -0
  10. package/dist/cli-adapters/provider-cli-adapter.d.ts +116 -64
  11. package/dist/cli-adapters/provider-cli-parse.d.ts +4 -0
  12. package/dist/cli-adapters/provider-cli-runtime.d.ts +34 -0
  13. package/dist/cli-adapters/provider-cli-shared.d.ts +53 -0
  14. package/dist/cli-adapters/raw-terminal-io.d.ts +37 -0
  15. package/dist/cli-adapters/resolve-executable.d.ts +14 -0
  16. package/dist/cli-adapters/terminal-backends/ghostty-vt-backend.d.ts +6 -3
  17. package/dist/cli-adapters/terminal-backends/types.d.ts +10 -2
  18. package/dist/cli-adapters/terminal-screen.d.ts +4 -11
  19. package/dist/commands/cli-manager.d.ts +15 -1
  20. package/dist/commands/handler.d.ts +125 -0
  21. package/dist/commands/low-family/index.d.ts +3 -0
  22. package/dist/commands/low-family/refine-config.d.ts +2 -0
  23. package/dist/commands/low-family/session-host.d.ts +2 -0
  24. package/dist/commands/low-family/spec-providerdev.d.ts +11 -0
  25. package/dist/commands/low-family/types.d.ts +16 -0
  26. package/dist/commands/mesh-coordinator.d.ts +85 -1
  27. package/dist/commands/router.d.ts +336 -2
  28. package/dist/commands/upgrade-helper.d.ts +41 -1
  29. package/dist/config/chat-history.d.ts +9 -0
  30. package/dist/config/config.d.ts +5 -0
  31. package/dist/config/mesh-config.d.ts +77 -1
  32. package/dist/detection/ide-detector.d.ts +13 -0
  33. package/dist/detection/win32-ide-version.d.ts +37 -0
  34. package/dist/git/change-impact-config.d.ts +159 -0
  35. package/dist/git/git-commands.d.ts +11 -1
  36. package/dist/git/git-diff.d.ts +6 -0
  37. package/dist/git/git-executor.d.ts +11 -0
  38. package/dist/git/git-status.d.ts +23 -0
  39. package/dist/git/git-types.d.ts +2 -50
  40. package/dist/git/index.d.ts +2 -0
  41. package/dist/index.d.ts +54 -13
  42. package/dist/index.js +41337 -14723
  43. package/dist/index.js.map +1 -1
  44. package/dist/index.mjs +44100 -17614
  45. package/dist/index.mjs.map +1 -1
  46. package/dist/installer.d.ts +1 -4
  47. package/dist/ipc/local-ipc-server.d.ts +91 -0
  48. package/dist/launch.d.ts +1 -1
  49. package/dist/logging/async-batch-writer.d.ts +10 -0
  50. package/dist/logging/log-redactor.d.ts +24 -0
  51. package/dist/logging/log-tail-reader.d.ts +46 -0
  52. package/dist/logging/logger.d.ts +1 -1
  53. package/dist/mesh/contracts.d.ts +164 -0
  54. package/dist/mesh/coordinator-prompt.d.ts +36 -0
  55. package/dist/mesh/coordinator-registry.d.ts +59 -0
  56. package/dist/mesh/mesh-active-work.d.ts +174 -0
  57. package/dist/mesh/mesh-delivery-policy.d.ts +131 -0
  58. package/dist/mesh/mesh-event-trace.d.ts +21 -0
  59. package/dist/mesh/mesh-events-coordinator.d.ts +170 -0
  60. package/dist/mesh/mesh-events-pending.d.ts +55 -0
  61. package/dist/mesh/mesh-events-stale.d.ts +40 -0
  62. package/dist/mesh/mesh-events-utils.d.ts +60 -0
  63. package/dist/mesh/mesh-events.d.ts +6 -49
  64. package/dist/mesh/mesh-fast-forward.d.ts +81 -0
  65. package/dist/mesh/mesh-host-ownership.d.ts +9 -0
  66. package/dist/mesh/mesh-init.d.ts +86 -0
  67. package/dist/mesh/mesh-ledger-reconciliation.d.ts +23 -3
  68. package/dist/mesh/mesh-ledger.d.ts +77 -1
  69. package/dist/mesh/mesh-missions.d.ts +164 -0
  70. package/dist/mesh/mesh-reconcile-loop.d.ts +7 -0
  71. package/dist/mesh/mesh-refine-batch.d.ts +68 -0
  72. package/dist/mesh/mesh-refine-status.d.ts +62 -0
  73. package/dist/mesh/mesh-review-inbox.d.ts +90 -0
  74. package/dist/mesh/mesh-routing.d.ts +70 -0
  75. package/dist/mesh/mesh-runtime-store.d.ts +429 -0
  76. package/dist/mesh/mesh-task-stats.d.ts +49 -0
  77. package/dist/mesh/mesh-unresolved-forward-outbox.d.ts +30 -0
  78. package/dist/mesh/mesh-work-queue.d.ts +246 -5
  79. package/dist/mesh/preview-freshness.d.ts +18 -0
  80. package/dist/mesh/refine-config.d.ts +216 -0
  81. package/dist/mesh/worktree-bootstrap-config.d.ts +135 -0
  82. package/dist/providers/acp-provider-instance.d.ts +5 -0
  83. package/dist/providers/approval-utils.d.ts +20 -0
  84. package/dist/providers/chat-message-normalization.d.ts +12 -0
  85. package/dist/providers/cli-provider-instance.d.ts +106 -3
  86. package/dist/providers/contracts.d.ts +139 -6
  87. package/dist/providers/external-sources.d.ts +71 -0
  88. package/dist/providers/manual-attendance.d.ts +63 -0
  89. package/dist/providers/native-history/antigravity-cli-transcript.d.ts +100 -0
  90. package/dist/providers/native-history/claude-cli-transcript.d.ts +70 -0
  91. package/dist/providers/native-history/codex-cli-transcript.d.ts +73 -0
  92. package/dist/providers/native-history/dispatcher.d.ts +27 -0
  93. package/dist/providers/native-history/hermes-cli-transcript.d.ts +30 -0
  94. package/dist/providers/native-history/index.d.ts +13 -0
  95. package/dist/providers/provider-instance-manager.d.ts +14 -0
  96. package/dist/providers/provider-instance.d.ts +23 -1
  97. package/dist/providers/provider-loader.d.ts +26 -4
  98. package/dist/providers/provider-trust.d.ts +31 -0
  99. package/dist/providers/read-chat-contract.d.ts +29 -0
  100. package/dist/providers/sdk/v1/builders/acp/detect-status.d.ts +68 -0
  101. package/dist/providers/sdk/v1/builders/cli/detect-status.d.ts +86 -0
  102. package/dist/providers/sdk/v1/builders/cli/parse-approval-squash.d.ts +59 -0
  103. package/dist/providers/sdk/v1/builders/cli/parse-approval.d.ts +65 -0
  104. package/dist/providers/sdk/v1/builders/cli/parse-session.d.ts +91 -0
  105. package/dist/providers/sdk/v1/builders/cli/visible-region.d.ts +42 -0
  106. package/dist/providers/sdk/v1/fixture-tooling/format.d.ts +126 -0
  107. package/dist/providers/sdk/v1/fixture-tooling/index.d.ts +8 -0
  108. package/dist/providers/sdk/v1/fixture-tooling/replay.d.ts +38 -0
  109. package/dist/providers/sdk/v1/index.d.ts +30 -0
  110. package/dist/providers/sdk/v1/sandbox/README-design.d.ts +193 -0
  111. package/dist/providers/sdk/v1/sandbox/require-whitelist.d.ts +74 -0
  112. package/dist/providers/sdk/v1/sandbox/script-runner.d.ts +98 -0
  113. package/dist/providers/sdk/v1/types/cli/index.d.ts +277 -0
  114. package/dist/providers/sdk/v1/types/common/index.d.ts +203 -0
  115. package/dist/providers/sdk/v1/validators/index.d.ts +5 -0
  116. package/dist/providers/sdk/v1/validators/manifest.d.ts +40 -0
  117. package/dist/providers/sdk/v1/validators/taint.d.ts +52 -0
  118. package/dist/providers/spec/adapter.d.ts +91 -0
  119. package/dist/providers/spec/cli-adapter.d.ts +223 -0
  120. package/dist/providers/spec/evaluator.d.ts +45 -0
  121. package/dist/providers/spec/fsm-driver.d.ts +368 -0
  122. package/dist/providers/spec/fsm-evaluator.d.ts +76 -0
  123. package/dist/providers/spec/fsm-loader.d.ts +14 -0
  124. package/dist/providers/spec/fsm-types.d.ts +174 -0
  125. package/dist/providers/spec/native-history-executor.d.ts +41 -0
  126. package/dist/providers/spec/pre-launch-trust.d.ts +16 -0
  127. package/dist/providers/spec/route.d.ts +4 -0
  128. package/dist/providers/spec/types.d.ts +222 -0
  129. package/dist/providers/status-monitor.d.ts +7 -7
  130. package/dist/providers/transcript-v2.d.ts +176 -0
  131. package/dist/providers/types/interactive-prompt.d.ts +84 -0
  132. package/dist/providers/working-dir.d.ts +17 -0
  133. package/dist/repo-mesh-types.d.ts +295 -13
  134. package/dist/sessions/registry.d.ts +6 -0
  135. package/dist/shared-types-extra.d.ts +2 -4
  136. package/dist/shared-types.d.ts +59 -2
  137. package/dist/status/normalize.d.ts +1 -1
  138. package/dist/status/normalize.js +1 -0
  139. package/dist/status/normalize.js.map +1 -1
  140. package/dist/status/normalize.mjs +1 -0
  141. package/dist/status/normalize.mjs.map +1 -1
  142. package/dist/status/reporter.d.ts +2 -0
  143. package/dist/status/snapshot.d.ts +26 -0
  144. package/dist/system/load-better-sqlite3.d.ts +21 -0
  145. package/dist/types.d.ts +5 -0
  146. package/package.json +7 -3
  147. package/src/agent-stream/poller.ts +2 -3
  148. package/src/agent-stream/provider-adapter.ts +1 -1
  149. package/src/boot/daemon-lifecycle.ts +55 -12
  150. package/src/boot/process-hardening.ts +89 -0
  151. package/src/build-info.ts +73 -0
  152. package/src/chat/source-machine.ts +534 -0
  153. package/src/chat/source-resolver.ts +0 -0
  154. package/src/chat/subscription-updates.ts +20 -1
  155. package/src/cli-adapter-types.d.ts +1 -0
  156. package/src/cli-adapter-types.ts +56 -2
  157. package/src/cli-adapters/cli-script-runner.ts +421 -0
  158. package/src/cli-adapters/cli-state-engine.ts +1215 -0
  159. package/src/cli-adapters/provider-cli-adapter.d.ts +1 -1
  160. package/src/cli-adapters/provider-cli-adapter.ts +774 -1141
  161. package/src/cli-adapters/provider-cli-parse.d.ts +1 -0
  162. package/src/cli-adapters/provider-cli-parse.ts +13 -0
  163. package/src/cli-adapters/provider-cli-runtime.ts +56 -7
  164. package/src/cli-adapters/provider-cli-shared.d.ts +4 -0
  165. package/src/cli-adapters/provider-cli-shared.ts +100 -11
  166. package/src/cli-adapters/pty-transport.ts +2 -1
  167. package/src/cli-adapters/raw-terminal-io.ts +252 -0
  168. package/src/cli-adapters/resolve-executable.ts +90 -0
  169. package/src/cli-adapters/session-host-transport.ts +2 -1
  170. package/src/cli-adapters/terminal-backends/ghostty-vt-backend.ts +53 -29
  171. package/src/cli-adapters/terminal-backends/types.ts +10 -3
  172. package/src/cli-adapters/terminal-screen.ts +16 -81
  173. package/src/commands/WINDOWS-UPGRADE-LOCK-FAILURE.md +198 -0
  174. package/src/commands/chat-commands.ts +1823 -62
  175. package/src/commands/cli-manager.ts +405 -23
  176. package/src/commands/handler.ts +841 -2
  177. package/src/commands/low-family/index.ts +23 -0
  178. package/src/commands/low-family/refine-config.ts +106 -0
  179. package/src/commands/low-family/session-host.ts +274 -0
  180. package/src/commands/low-family/spec-providerdev.ts +217 -0
  181. package/src/commands/low-family/types.ts +19 -0
  182. package/src/commands/mesh-coordinator.ts +332 -122
  183. package/src/commands/router.ts +7195 -1423
  184. package/src/commands/stream-commands.ts +8 -0
  185. package/src/commands/upgrade-helper.ts +310 -45
  186. package/src/config/chat-history.ts +483 -24
  187. package/src/config/config.ts +12 -0
  188. package/src/config/mesh-config.ts +388 -6
  189. package/src/config/recent-activity.ts +8 -2
  190. package/src/daemon/dev-auto-implement.ts +3 -2
  191. package/src/daemon/dev-cli-debug.ts +10 -1
  192. package/src/detection/cli-detector.ts +28 -9
  193. package/src/detection/ide-detector.ts +55 -16
  194. package/src/detection/win32-ide-version.ts +106 -0
  195. package/src/git/change-impact-config.ts +354 -0
  196. package/src/git/git-commands.ts +55 -14
  197. package/src/git/git-diff.ts +81 -11
  198. package/src/git/git-executor.ts +12 -0
  199. package/src/git/git-status.ts +574 -43
  200. package/src/git/git-types.ts +14 -62
  201. package/src/git/git-worktree.ts +8 -1
  202. package/src/git/index.ts +16 -0
  203. package/src/index.ts +156 -12
  204. package/src/installer.d.ts +1 -1
  205. package/src/installer.ts +8 -6
  206. package/src/ipc/local-ipc-server.ts +278 -0
  207. package/src/launch.d.ts +1 -1
  208. package/src/launch.ts +37 -28
  209. package/src/logging/async-batch-writer.ts +55 -0
  210. package/src/logging/command-log.ts +7 -5
  211. package/src/logging/log-redactor.ts +100 -0
  212. package/src/logging/log-tail-reader.ts +220 -0
  213. package/src/logging/logger.ts +14 -7
  214. package/src/mesh/contracts.ts +338 -0
  215. package/src/mesh/coordinator-prompt.ts +234 -31
  216. package/src/mesh/coordinator-registry.ts +121 -0
  217. package/src/mesh/mesh-active-work.ts +667 -0
  218. package/src/mesh/mesh-delivery-policy.ts +315 -0
  219. package/src/mesh/mesh-event-trace.ts +67 -0
  220. package/src/mesh/mesh-events-coordinator.ts +2572 -0
  221. package/src/mesh/mesh-events-pending.ts +599 -0
  222. package/src/mesh/mesh-events-stale.ts +309 -0
  223. package/src/mesh/mesh-events-utils.ts +361 -0
  224. package/src/mesh/mesh-events.ts +30 -1035
  225. package/src/mesh/mesh-fast-forward.ts +839 -0
  226. package/src/mesh/mesh-host-ownership.ts +73 -0
  227. package/src/mesh/mesh-init.ts +260 -0
  228. package/src/mesh/mesh-ledger-reconciliation.ts +12 -5
  229. package/src/mesh/mesh-ledger.ts +591 -102
  230. package/src/mesh/mesh-missions.ts +322 -0
  231. package/src/mesh/mesh-reconcile-loop.ts +1096 -0
  232. package/src/mesh/mesh-refine-batch.ts +197 -0
  233. package/src/mesh/mesh-refine-status.ts +231 -0
  234. package/src/mesh/mesh-review-inbox.ts +307 -0
  235. package/src/mesh/mesh-routing.ts +291 -0
  236. package/src/mesh/mesh-runtime-store.ts +1697 -0
  237. package/src/mesh/mesh-task-stats.ts +161 -0
  238. package/src/mesh/mesh-unresolved-forward-outbox.ts +185 -0
  239. package/src/mesh/mesh-work-queue.ts +997 -141
  240. package/src/mesh/preview-freshness.ts +118 -0
  241. package/src/mesh/refine-config.ts +423 -0
  242. package/src/mesh/worktree-bootstrap-config.ts +331 -0
  243. package/src/providers/acp-provider-instance.ts +43 -10
  244. package/src/providers/approval-utils.d.ts +5 -0
  245. package/src/providers/approval-utils.ts +57 -5
  246. package/src/providers/chat-message-normalization.ts +45 -5
  247. package/src/providers/cli-provider-instance.ts +1267 -101
  248. package/src/providers/contracts.d.ts +55 -0
  249. package/src/providers/contracts.ts +150 -6
  250. package/src/providers/extension-provider-instance.ts +12 -7
  251. package/src/providers/external-sources.ts +218 -0
  252. package/src/providers/ide-provider-instance.ts +35 -12
  253. package/src/providers/manual-attendance.ts +85 -0
  254. package/src/providers/native-history/antigravity-cli-transcript.ts +643 -0
  255. package/src/providers/native-history/claude-cli-transcript.ts +396 -0
  256. package/src/providers/native-history/codex-cli-transcript.ts +479 -0
  257. package/src/providers/native-history/dispatcher.ts +340 -0
  258. package/src/providers/native-history/hermes-cli-transcript.ts +230 -0
  259. package/src/providers/native-history/index.ts +30 -0
  260. package/src/providers/provider-instance-manager.ts +30 -0
  261. package/src/providers/provider-instance.ts +19 -1
  262. package/src/providers/provider-loader.ts +670 -50
  263. package/src/providers/provider-schema.ts +87 -14
  264. package/src/providers/provider-trust.ts +114 -0
  265. package/src/providers/read-chat-contract.ts +76 -16
  266. package/src/providers/sdk/README.md +49 -0
  267. package/src/providers/sdk/v1/builders/acp/detect-status.ts +144 -0
  268. package/src/providers/sdk/v1/builders/cli/detect-status.ts +313 -0
  269. package/src/providers/sdk/v1/builders/cli/parse-approval-squash.ts +158 -0
  270. package/src/providers/sdk/v1/builders/cli/parse-approval.ts +250 -0
  271. package/src/providers/sdk/v1/builders/cli/parse-session.ts +276 -0
  272. package/src/providers/sdk/v1/builders/cli/visible-region.ts +143 -0
  273. package/src/providers/sdk/v1/fixture-tooling/format.ts +130 -0
  274. package/src/providers/sdk/v1/fixture-tooling/index.ts +22 -0
  275. package/src/providers/sdk/v1/fixture-tooling/replay.ts +352 -0
  276. package/src/providers/sdk/v1/index.ts +152 -0
  277. package/src/providers/sdk/v1/sandbox/README-design.ts +195 -0
  278. package/src/providers/sdk/v1/sandbox/require-whitelist.ts +472 -0
  279. package/src/providers/sdk/v1/sandbox/script-runner.ts +150 -0
  280. package/src/providers/sdk/v1/schemas/cli/provider.schema.json +504 -0
  281. package/src/providers/sdk/v1/schemas/primitives/acp-session-protocol-v1.json +131 -0
  282. package/src/providers/sdk/v1/schemas/primitives/native-history-codex-rollout-v1.json +66 -0
  283. package/src/providers/sdk/v1/schemas/primitives/tui-approval-squash-v1.json +91 -0
  284. package/src/providers/sdk/v1/schemas/primitives/tui-assistant-block-v1.json +91 -0
  285. package/src/providers/sdk/v1/schemas/primitives/tui-cue-ordering-v1.json +47 -0
  286. package/src/providers/sdk/v1/schemas/primitives/tui-dispatch-order-v1.json +32 -0
  287. package/src/providers/sdk/v1/schemas/primitives/tui-footer-chrome-v1.json +42 -0
  288. package/src/providers/sdk/v1/schemas/primitives/tui-index-finder-v1.json +27 -0
  289. package/src/providers/sdk/v1/schemas/primitives/tui-modal-v1.json +125 -0
  290. package/src/providers/sdk/v1/schemas/primitives/tui-prompt-marker-v1.json +45 -0
  291. package/src/providers/sdk/v1/schemas/primitives/tui-session-id-extraction-v1.json +46 -0
  292. package/src/providers/sdk/v1/schemas/primitives/tui-settled-prompt-v1.json +71 -0
  293. package/src/providers/sdk/v1/schemas/primitives/tui-spinner-v1.json +83 -0
  294. package/src/providers/sdk/v1/schemas/primitives/tui-transcript-pty-v1.json +83 -0
  295. package/src/providers/sdk/v1/schemas/primitives/tui-visible-region-v1.json +57 -0
  296. package/src/providers/sdk/v1/schemas/primitives/tui-welcome-screen-v1.json +35 -0
  297. package/src/providers/sdk/v1/types/cli/index.ts +365 -0
  298. package/src/providers/sdk/v1/types/common/index.ts +229 -0
  299. package/src/providers/sdk/v1/validators/index.ts +19 -0
  300. package/src/providers/sdk/v1/validators/manifest.ts +110 -0
  301. package/src/providers/sdk/v1/validators/taint.ts +309 -0
  302. package/src/providers/spec/adapter.ts +235 -0
  303. package/src/providers/spec/cli-adapter.ts +1051 -0
  304. package/src/providers/spec/evaluator.ts +407 -0
  305. package/src/providers/spec/fsm-driver.ts +1251 -0
  306. package/src/providers/spec/fsm-evaluator.ts +272 -0
  307. package/src/providers/spec/fsm-loader.ts +120 -0
  308. package/src/providers/spec/fsm-types.ts +273 -0
  309. package/src/providers/spec/native-history-executor.ts +1136 -0
  310. package/src/providers/spec/pre-launch-trust.ts +104 -0
  311. package/src/providers/spec/route.ts +51 -0
  312. package/src/providers/spec/types.ts +262 -0
  313. package/src/providers/status-monitor.d.ts +7 -7
  314. package/src/providers/status-monitor.ts +37 -22
  315. package/src/providers/transcript-v2.ts +567 -0
  316. package/src/providers/types/interactive-prompt.ts +536 -0
  317. package/src/providers/version-archive.ts +64 -24
  318. package/src/providers/working-dir.ts +23 -0
  319. package/src/repo-mesh-types.ts +378 -14
  320. package/src/sessions/registry.ts +6 -0
  321. package/src/shared-types-extra.ts +2 -4
  322. package/src/shared-types.d.ts +8 -0
  323. package/src/shared-types.ts +64 -1
  324. package/src/status/builders.ts +26 -6
  325. package/src/status/normalize.ts +2 -0
  326. package/src/status/reporter.ts +19 -1
  327. package/src/status/snapshot.ts +95 -26
  328. package/src/system/host-memory.ts +29 -12
  329. package/src/system/load-better-sqlite3.ts +68 -0
  330. package/src/types.ts +5 -0
  331. package/dist/cli-adapters/terminal-backends/xterm-backend.d.ts +0 -17
  332. package/dist/mesh/mesh-sync.d.ts +0 -53
  333. package/src/cli-adapters/terminal-backends/ghostty-vt-backend.d.ts +0 -16
  334. package/src/cli-adapters/terminal-backends/xterm-backend.ts +0 -97
  335. package/src/mesh/mesh-sync.ts +0 -111
@@ -0,0 +1,90 @@
1
+ import { execFileSync } from 'child_process';
2
+ import { existsSync } from 'fs';
3
+ import * as path from 'path';
4
+
5
+ // Extensions ConPTY/CreateProcess can launch directly (not .cmd/.bat, which
6
+ // need a cmd.exe wrapper).
7
+ const DIRECT_EXEC_EXT = new Set(['.exe', '.com']);
8
+
9
+ // Executable extensions to probe when scanning a directory ourselves, ordered
10
+ // most-directly-launchable first. node-pty's ConPTY backend launches an
11
+ // absolute `.cmd`/`.bat` shim fine (verified) — it only fails to *resolve* a
12
+ // bare command against an incomplete PATH — so once we hand it an absolute
13
+ // path, a `.cmd` shim works just as well as a real `.exe`.
14
+ const WIN_EXEC_EXT = ['.exe', '.com', '.cmd', '.bat'];
15
+
16
+ /**
17
+ * Resolve a bare command against well-known global-bin directories that are
18
+ * frequently NOT on the daemon's inherited PATH, so `where` (which only
19
+ * searches PATH) misses them. A daemon running under one Node install (e.g.
20
+ * nvm) never sees another npm prefix's bin dir — notably npm's Windows default
21
+ * prefix at %APPDATA%\npm, where `npm i -g @openai/codex` lands. Returns an
22
+ * absolute path on the first hit, or null. Mirrors findBinary()'s extraDirs in
23
+ * provider-cli-shared.ts; kept inline here so this lightweight module (loaded
24
+ * by pty-transport) need not pull in the heavier shared module.
25
+ */
26
+ function resolveWin32GlobalBin(trimmed: string): string | null {
27
+ // Only resolve a bare command name — anything with a path separator is the
28
+ // caller's explicit location and must not be re-pointed at a global bin dir.
29
+ if (path.isAbsolute(trimmed) || trimmed.includes('/') || trimmed.includes('\\')) {
30
+ return null;
31
+ }
32
+ const extraDirs: string[] = [];
33
+ if (process.env.APPDATA) extraDirs.push(path.join(process.env.APPDATA, 'npm'));
34
+ try { extraDirs.push(path.dirname(process.execPath)); } catch { /* best-effort */ }
35
+ for (const dir of extraDirs) {
36
+ if (!dir) continue;
37
+ for (const ext of WIN_EXEC_EXT) {
38
+ const full = path.join(dir, trimmed + ext);
39
+ if (existsSync(full)) return full;
40
+ }
41
+ }
42
+ return null;
43
+ }
44
+
45
+ /**
46
+ * Resolve a launch command to an absolute executable path on Windows.
47
+ *
48
+ * node-pty's ConPTY backend resolves a bare/relative command against the
49
+ * *calling process's* `Path` env var and — critically — does NOT apply PATHEXT.
50
+ * So a provider command like `claude` never matches `claude.exe` and the native
51
+ * layer throws `File not found:` (empty), which crashes the daemon. We resolve
52
+ * it to an absolute `.exe` here (in the daemon process, which has the full PATH)
53
+ * before the command ever reaches node-pty.
54
+ *
55
+ * No-op on non-Windows and when the command is already an existing absolute path
56
+ * or cannot be resolved (caller keeps the original behaviour).
57
+ */
58
+ export function resolveWin32Executable(command: string): string {
59
+ if (process.platform !== 'win32') return command;
60
+ const trimmed = (command || '').trim();
61
+ if (!trimmed) return command;
62
+
63
+ // Already an absolute path that exists — keep it.
64
+ if (path.isAbsolute(trimmed) && existsSync(trimmed)) return trimmed;
65
+
66
+ try {
67
+ const out = execFileSync('where', [trimmed], {
68
+ encoding: 'utf8',
69
+ windowsHide: true,
70
+ }).trim();
71
+ if (out) {
72
+ const matches = out.split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
73
+ // Prefer a directly-launchable executable (.exe/.com) over .cmd/.bat shims.
74
+ const direct = matches.find((m) => DIRECT_EXEC_EXT.has(path.extname(m).toLowerCase()));
75
+ return direct || matches[0] || command;
76
+ }
77
+ } catch {
78
+ // `where` not found / non-zero exit — fall through to the global-bin scan.
79
+ }
80
+
81
+ // `where` found nothing on PATH. Before giving up (and letting node-pty crash
82
+ // with "File not found:" on the bare command), search npm's off-PATH global
83
+ // bin dir(s). This is the codex case: `npm i -g @openai/codex` installs to
84
+ // %APPDATA%\npm, which is absent from a nvm-launched daemon's PATH, so a spec
85
+ // binary of "codex" never resolved and the spawn ENOENT'd.
86
+ const globalBin = resolveWin32GlobalBin(trimmed);
87
+ if (globalBin) return globalBin;
88
+
89
+ return command;
90
+ }
@@ -6,6 +6,7 @@ import {
6
6
  type SessionHostRecord,
7
7
  } from '@adhdev/session-host-core';
8
8
  import { LOG } from '../logging/logger.js';
9
+ import { resolveWin32Executable } from './resolve-executable.js';
9
10
  import type { PtyRuntimeMetadata, PtyRuntimeTransport, PtySpawnOptions, PtyTransportFactory } from './pty-transport.js';
10
11
 
11
12
  interface SessionHostPtyTransportFactoryOptions {
@@ -435,7 +436,7 @@ export class SessionHostPtyTransportFactory implements PtyTransportFactory {
435
436
  spawn(command: string, args: string[], spawnOptions: PtySpawnOptions): PtyRuntimeTransport {
436
437
  return new SessionHostRuntimeTransport({
437
438
  ...this.options,
438
- command,
439
+ command: resolveWin32Executable(command),
439
440
  args,
440
441
  spawnOptions,
441
442
  });
@@ -1,7 +1,6 @@
1
1
  import type {
2
2
  TerminalViewportBackend,
3
3
  TerminalViewportBackendOptions,
4
- TerminalViewportBackendPreference,
5
4
  } from './types.js';
6
5
 
7
6
  type GhosttyVtTerminal = {
@@ -49,12 +48,10 @@ function getBindingCandidates(): string[] {
49
48
  return explicit ? [explicit] : DEFAULT_BINDING_CANDIDATES;
50
49
  }
51
50
 
52
- function loadGhosttyVtBinding(required: boolean): GhosttyVtBinding | null {
51
+ function loadGhosttyVtBinding(): GhosttyVtBinding {
53
52
  if (cachedBinding !== undefined) {
54
- if (!cachedBinding && required && cachedBindingError) {
55
- throw cachedBindingError;
56
- }
57
- return cachedBinding;
53
+ if (!cachedBinding && cachedBindingError) throw cachedBindingError;
54
+ return cachedBinding!;
58
55
  }
59
56
 
60
57
  const errors: string[] = [];
@@ -65,7 +62,7 @@ function loadGhosttyVtBinding(required: boolean): GhosttyVtBinding | null {
65
62
  const mod = require(ref);
66
63
  cachedBinding = normalizeBinding(mod, ref);
67
64
  cachedBindingError = null;
68
- return cachedBinding;
65
+ return cachedBinding!;
69
66
  } catch (error) {
70
67
  if (isModuleNotFoundError(error, ref)) {
71
68
  errors.push(`${ref}: module not found`);
@@ -78,57 +75,84 @@ function loadGhosttyVtBinding(required: boolean): GhosttyVtBinding | null {
78
75
 
79
76
  cachedBinding = null;
80
77
  cachedBindingError = new Error(
81
- `ghostty-vt backend requested but no binding is available (${errors.join('; ') || 'no candidates tried'})`,
78
+ `ghostty-vt binding unavailable (${errors.join('; ') || 'no candidates tried'})`,
82
79
  );
83
-
84
- if (required) throw cachedBindingError;
85
- return null;
86
- }
87
-
88
- export function resolveTerminalBackendPreference(): TerminalViewportBackendPreference {
89
- const raw = process.env.ADHDEV_TERMINAL_BACKEND?.trim().toLowerCase();
90
- if (raw === 'ghostty-vt' || raw === 'xterm' || raw === 'auto') return raw;
91
- return 'auto';
92
- }
93
-
94
- export function isGhosttyVtBackendAvailable(): boolean {
95
- return !!loadGhosttyVtBinding(false);
80
+ throw cachedBindingError;
96
81
  }
97
82
 
98
83
  export class GhosttyVtTerminalBackend implements TerminalViewportBackend {
99
84
  readonly kind = 'ghostty-vt' as const;
100
85
  private terminal: GhosttyVtTerminal;
86
+ private rows: number;
87
+ private disposed = false;
101
88
 
102
89
  constructor(options: TerminalViewportBackendOptions) {
103
- const binding = loadGhosttyVtBinding(true);
104
- if (!binding) {
105
- throw new Error('ghostty-vt backend requested but no binding is available');
106
- }
90
+ const binding = loadGhosttyVtBinding();
91
+ this.rows = Math.max(1, options.rows | 0);
107
92
  this.terminal = binding.createTerminal({
108
93
  cols: Math.max(1, options.cols | 0),
109
- rows: Math.max(1, options.rows | 0),
94
+ rows: this.rows,
110
95
  scrollback: Math.max(0, options.scrollback | 0),
111
96
  });
112
97
  }
113
98
 
114
99
  resize(rows: number, cols: number): void {
115
- this.terminal.resize(Math.max(1, cols | 0), Math.max(1, rows | 0));
100
+ this.rows = Math.max(1, rows | 0);
101
+ this.terminal.resize(Math.max(1, cols | 0), this.rows);
116
102
  }
117
103
 
118
104
  write(data: string): void {
119
- if (!data) return;
105
+ if (!data || this.disposed) return;
120
106
  this.terminal.write(data);
121
107
  }
122
108
 
109
+ private formatLines(): string[] {
110
+ // ghostty's `trim:true` collapses CUF-advanced cells — many TUIs including
111
+ // Claude Code render spaces via cursor-forward rather than literal spaces,
112
+ // which would break downstream regex matching. Keep per-row padding and
113
+ // trim trailing whitespace ourselves.
114
+ //
115
+ // formatPlainText uses a .screen pin which includes scrollback history.
116
+ const raw = this.terminal.formatPlainText({ trim: false }) || '';
117
+ if (!raw) return [];
118
+ return raw.split('\n').map((row) => row.replace(/\s+$/, ''));
119
+ }
120
+
121
+ private static trimBlankEnds(lines: string[]): string {
122
+ let first = 0;
123
+ let last = lines.length;
124
+ while (first < last && !lines[first]) first += 1;
125
+ while (last > first && !lines[last - 1]) last -= 1;
126
+ return lines.slice(first, last).join('\n');
127
+ }
128
+
123
129
  getText(): string {
124
- return this.terminal.formatPlainText({ trim: true }) || '';
130
+ if (this.disposed) return '';
131
+ const lines = this.formatLines();
132
+ if (lines.length === 0) return '';
133
+ // Take only the viewport (last `rows` lines) to exclude scrollback history.
134
+ const viewport = lines.length > this.rows ? lines.slice(-this.rows) : lines;
135
+ return GhosttyVtTerminalBackend.trimBlankEnds(viewport);
136
+ }
137
+
138
+ getTextWithScrollback(): string {
139
+ if (this.disposed) return '';
140
+ const lines = this.formatLines();
141
+ if (lines.length === 0) return '';
142
+ // Full buffer including scrollback — does NOT slice to the viewport, so a
143
+ // tall prompt whose top has scrolled above the visible rows is still
144
+ // matchable by content patterns.
145
+ return GhosttyVtTerminalBackend.trimBlankEnds(lines);
125
146
  }
126
147
 
127
148
  getCursorPosition(): { col: number; row: number } {
149
+ if (this.disposed) return { col: 0, row: 0 };
128
150
  return this.terminal.getCursorPosition();
129
151
  }
130
152
 
131
153
  dispose(): void {
154
+ if (this.disposed) return;
155
+ this.disposed = true;
132
156
  this.terminal.dispose();
133
157
  }
134
158
  }
@@ -1,6 +1,4 @@
1
- export type TerminalViewportBackendKind = 'xterm' | 'ghostty-vt';
2
-
3
- export type TerminalViewportBackendPreference = TerminalViewportBackendKind | 'auto';
1
+ export type TerminalViewportBackendKind = 'ghostty-vt';
4
2
 
5
3
  export type TerminalViewportBackendOptions = {
6
4
  rows: number;
@@ -13,6 +11,15 @@ export interface TerminalViewportBackend {
13
11
  resize(rows: number, cols: number): void;
14
12
  write(data: string): void;
15
13
  getText(): string;
14
+ /**
15
+ * Like getText() but includes scrollback history (the lines that have
16
+ * scrolled above the visible viewport). Used by content-pattern matching
17
+ * (e.g. approval/modal button extraction) that must stay correct when a
18
+ * tall prompt — a big diff or long explanation — pushes part of the
19
+ * prompt box above the viewport. NOT for cursor-relative / stable_ms
20
+ * conditions, whose row indices are viewport-relative.
21
+ */
22
+ getTextWithScrollback(): string;
16
23
  getCursorPosition(): { col: number; row: number };
17
24
  dispose(): void;
18
25
  }
@@ -1,99 +1,29 @@
1
1
  /**
2
- * PTY screen snapshot abstraction.
3
- *
4
- * Terminal viewport backend selection.
5
- *
6
- * Runtime preference is backend-agnostic:
7
- * - prefer ghostty-vt when available (or when explicitly requested)
8
- * - fall back to xterm when ghostty-vt is unavailable
2
+ * PTY screen snapshot abstraction — ghostty-vt backend.
9
3
  */
10
4
 
11
- import { LOG } from '../logging/logger.js';
12
- import { GhosttyVtTerminalBackend, isGhosttyVtBackendAvailable, resolveTerminalBackendPreference } from './terminal-backends/ghostty-vt-backend.js';
13
- import type {
14
- TerminalViewportBackend,
15
- TerminalViewportBackendKind,
16
- TerminalViewportBackendOptions,
17
- TerminalViewportBackendPreference,
18
- } from './terminal-backends/types.js';
19
- import { XtermTerminalBackend } from './terminal-backends/xterm-backend.js';
5
+ import { GhosttyVtTerminalBackend } from './terminal-backends/ghostty-vt-backend.js';
6
+ import type { TerminalViewportBackendKind } from './terminal-backends/types.js';
7
+ import { DEFAULT_SESSION_HOST_COLS, DEFAULT_SESSION_HOST_ROWS } from '@adhdev/session-host-core';
20
8
 
21
9
  const DEFAULT_SCROLLBACK = 2000;
22
- const loggedTerminalBackends = new Set<string>();
23
10
 
24
11
  export function getTerminalBackendRuntimeStatus(): {
25
12
  backend: TerminalViewportBackendKind;
26
- preference: TerminalViewportBackendPreference;
27
- ghosttyAvailable: boolean;
28
13
  } {
29
- const preference = resolveTerminalBackendPreference();
30
- const ghosttyAvailable = isGhosttyVtBackendAvailable();
31
- const backend: TerminalViewportBackendKind = (
32
- preference === 'ghostty-vt' || (preference === 'auto' && ghosttyAvailable)
33
- ? 'ghostty-vt'
34
- : 'xterm'
35
- );
36
- return { backend, preference, ghosttyAvailable };
37
- }
38
-
39
- function createTerminalBackend(
40
- options: TerminalViewportBackendOptions,
41
- preference: TerminalViewportBackendPreference,
42
- ): TerminalViewportBackend {
43
- const ghosttyAvailable = isGhosttyVtBackendAvailable();
44
- if (preference === 'ghostty-vt') {
45
- const backend = new GhosttyVtTerminalBackend(options);
46
- logTerminalBackendSelection(preference, ghosttyAvailable, backend.kind);
47
- return backend;
48
- }
49
-
50
- if (preference === 'auto' && ghosttyAvailable) {
51
- const backend = new GhosttyVtTerminalBackend(options);
52
- logTerminalBackendSelection(preference, ghosttyAvailable, backend.kind);
53
- return backend;
54
- }
55
-
56
- const backend = new XtermTerminalBackend(options);
57
- logTerminalBackendSelection(preference, ghosttyAvailable, backend.kind);
58
- return backend;
59
- }
60
-
61
- function logTerminalBackendSelection(
62
- preference: TerminalViewportBackendPreference,
63
- ghosttyAvailable: boolean,
64
- backendKind: TerminalViewportBackendKind,
65
- ): void {
66
- const key = `${preference}:${ghosttyAvailable}:${backendKind}`;
67
- if (loggedTerminalBackends.has(key)) return;
68
- loggedTerminalBackends.add(key);
69
- if (backendKind === 'xterm' && preference !== 'xterm' && !ghosttyAvailable) {
70
- const message = `[terminal-screen] ghostty-vt unavailable; using xterm fallback (preference=${preference})`;
71
- if (preference === 'auto') {
72
- LOG.info('Terminal', message);
73
- } else {
74
- LOG.warn('Terminal', message);
75
- }
76
- return;
77
- }
78
- LOG.info(
79
- 'Terminal',
80
- `[terminal-screen] backend=${backendKind} preference=${preference} ghosttyAvailable=${ghosttyAvailable}`,
81
- );
14
+ return { backend: 'ghostty-vt' };
82
15
  }
83
16
 
84
17
  export class TerminalScreen {
85
- readonly backendKind: TerminalViewportBackendKind;
18
+ readonly backendKind: TerminalViewportBackendKind = 'ghostty-vt';
86
19
  private rows: number;
87
20
  private cols: number;
88
- private readonly preference: TerminalViewportBackendPreference;
89
- private terminal: TerminalViewportBackend;
21
+ private terminal: GhosttyVtTerminalBackend;
90
22
 
91
- constructor(rows = 40, cols = 120) {
23
+ constructor(rows = DEFAULT_SESSION_HOST_ROWS, cols = DEFAULT_SESSION_HOST_COLS) {
92
24
  this.rows = Math.max(1, rows | 0);
93
25
  this.cols = Math.max(1, cols | 0);
94
- this.preference = resolveTerminalBackendPreference();
95
26
  this.terminal = this.createBackend();
96
- this.backendKind = this.terminal.kind;
97
27
  }
98
28
 
99
29
  reset(rows = this.rows, cols = this.cols): void {
@@ -117,6 +47,11 @@ export class TerminalScreen {
117
47
  return this.terminal.getText();
118
48
  }
119
49
 
50
+ /** Full buffer including scrollback history (see backend doc). */
51
+ getTextWithScrollback(): string {
52
+ return this.terminal.getTextWithScrollback();
53
+ }
54
+
120
55
  getCursorPosition(): { col: number; row: number } {
121
56
  return this.terminal.getCursorPosition();
122
57
  }
@@ -125,11 +60,11 @@ export class TerminalScreen {
125
60
  this.terminal.dispose();
126
61
  }
127
62
 
128
- private createBackend(): TerminalViewportBackend {
129
- return createTerminalBackend({
63
+ private createBackend(): GhosttyVtTerminalBackend {
64
+ return new GhosttyVtTerminalBackend({
130
65
  cols: this.cols,
131
66
  rows: this.rows,
132
67
  scrollback: DEFAULT_SCROLLBACK,
133
- }, this.preference);
68
+ });
134
69
  }
135
70
  }
@@ -0,0 +1,198 @@
1
+ # Windows 자동 업그레이드 실패 — 원인 분석 및 패치 명세
2
+
3
+ > 대상 파일: `oss/packages/daemon-core/src/commands/upgrade-helper.ts`
4
+ > 관련 가드: `packages/daemon-cloud/package.json`, `oss/packages/daemon-standalone/package.json` 의 `preinstall`
5
+ > 작성 근거: 2026-06-23 실 사용자(Windows 11, nvm-windows) 환경에서 `adhdev@0.9.82-rc.357 → rc.358` 자동 업그레이드가 반복 실패한 실 사례.
6
+
7
+ ## TL;DR (English)
8
+
9
+ The Windows self-upgrade fails when a process **other than** the parent CLI or the
10
+ known `session-host-daemon` keeps node-pty's `conpty.node` memory-mapped. The
11
+ helper only knows how to stop the single pid in `~/.adhdev/<app>-session-host.pid`,
12
+ so any *foreign* holder (here: three orphaned `pty_*probe*.cjs` scripts left in
13
+ `%TEMP%`) survives all 3 retries and the install dies with `EBUSY`. Two additional
14
+ weaknesses compound it: retry budget is far too small for a never-exiting holder,
15
+ and on failure the user gets no actionable message (only a log file). A separate,
16
+ independently-confirmed failure mode is the Node-24 `preinstall` guard firing when
17
+ the lifecycle-script `node` resolves to an unsupported version on a multi-node
18
+ `PATH`.
19
+
20
+ ---
21
+
22
+ ## 1. 배경 / Context
23
+
24
+ adhdev 데몬은 새 버전을 감지하면 detached 헬퍼 프로세스를 띄워
25
+ (`spawnDetachedDaemonUpgradeHelper`) `npm install -g adhdev@<target> --prefix <pinned>`
26
+ 를 실행한다 (`runDaemonUpgradeHelper`). Windows에서는 네이티브 애드온
27
+ `node-pty/prebuilds/win32-x64/conpty.node` 가 **메모리 매핑된 채 프로세스가
28
+ 완전히 종료될 때까지 배타적 잠금**된다. npm은 기존 설치본을 스테이징 디렉터리
29
+ (`node_modules/.adhdev-<hash>`)로 **복사**한 뒤 새 버전으로 교체하므로, 잠긴
30
+ `conpty.node`를 복사하려다 `EBUSY`로 실패한다.
31
+
32
+ 소스에는 이미 이 문제를 겨냥한 방어가 들어 있다:
33
+ - `stopSessionHostProcesses()` — `~/.adhdev/<app>-session-host.pid`의 pid를
34
+ 죽이고 종료를 기다림(`waitForPidExit`).
35
+ - `buildInstallEnvWithNodeOnPath()` — lifecycle 스크립트가 올바른 node를 쓰도록
36
+ `PATH` 앞에 현재 node 디렉터리를 prepend.
37
+ - 설치 재시도 루프 (Windows에서 `maxInstallAttempts = 3`, 백오프 `attempt*1500ms`).
38
+
39
+ **그런데 이번 실패는 이 방어들이 전부 적용된 상태에서도 발생한다.**
40
+
41
+ ## 2. 실제 실패 타임라인 (증거)
42
+
43
+ `~/.adhdev/daemon-upgrade.log`:
44
+
45
+ ```
46
+ [01:52:18] Upgrade helper started for adhdev@0.9.82-rc.358
47
+ [01:52:18] Using npm executable: C:\nvm4w\nodejs\node.exe
48
+ [01:52:18] Pinned install prefix: C:\Users\kjs0116\AppData\Local\nvm\v22.14.0
49
+ [01:52:18] Waiting for parent pid 38744 to exit
50
+ [01:52:21] Skipped locked stale entry (EPERM): ...\.adhdev-dTz6t6GZ — ...conpty.node
51
+ [01:52:35] Install attempt 1 hit a file lock (lock); cleaning staging and retrying after backoff
52
+ [01:52:57] Install attempt 2 hit a file lock (lock); ...
53
+ [01:53:16] Upgrade helper failed: EBUSY ... copyfile '...\adhdev\node_modules\node-pty\prebuilds\win32-x64\conpty.node'
54
+ -> '...\.adhdev-dTz6t6GZ\...\conpty.node'
55
+ ```
56
+ (01:59 에 동일 패턴으로 한 번 더 실패.)
57
+
58
+ 수동 진단으로 밝혀낸 **실제 잠금 보유자** (`Get-Process node | %{ $_.Modules | ? ModuleName -match conpty }`):
59
+
60
+ | PID | CommandLine | 시작 |
61
+ |-----|-------------|------|
62
+ | 56396 | `node %TEMP%\pty_cr_probe.cjs` | 2026-06-21 |
63
+ | 37304 | `node %TEMP%\pty_probe_parent.cjs` | 2026-06-21 |
64
+ | 34316 | `node %TEMP%\pty_probe2_parent.cjs` | 2026-06-21 |
65
+
66
+ 세 프로세스 모두
67
+ `...\AppData\Local\nvm\v22.14.0\node_modules\adhdev\node_modules\node-pty\prebuilds\win32-x64\conpty.node`
68
+ 를 로드한 채 **이틀째 살아 있었다.** 세 개를 종료한 직후 잠금이 풀렸고
69
+ (`LOCK CLEARED`), Node 22로 설치가 정상 완료됐다.
70
+
71
+ > 참고: `pty_*probe*.cjs` 는 현재 adhdev 소스 트리에 존재하지 않는다(grep 결과 0건).
72
+ > 즉 개발 중 임시로 `%TEMP%`에 떨군 PTY/ConPTY 진단 스크립트가 고아로 남은 것이다.
73
+ > 핵심은 "출처가 무엇이든, **세션 호스트가 아닌 임의의 프로세스가 conpty.node를
74
+ > 쥘 수 있다**"는 점이며, 현재 헬퍼는 이를 처리하지 못한다.
75
+
76
+ ## 3. 근본 원인
77
+
78
+ ### RC1 — (핵심) 세션 호스트가 아닌 임의의 `conpty.node` 보유자를 못 다룸
79
+ `stopSessionHostProcesses()` 는 **딱 하나의 pid**(`<app>-session-host.pid`)만,
80
+ 그것도 커맨드라인이 `/session-host-daemon/i` 에 매칭될 때만 죽인다
81
+ (`isManagedSessionHostPid`, upgrade-helper.ts:278-281, 295-313). 위 probe
82
+ 프로세스처럼 PID 파일에 없고 커맨드라인도 매칭 안 되는 보유자는 **전혀 감지/정리
83
+ 대상이 아니다.** 결과적으로 재시도 루프는 매번 같은 `EBUSY`를 다시 맞고 포기한다.
84
+
85
+ ### RC2 — 재시도/백오프가 "절대 안 죽는 보유자"에 무력
86
+ `maxInstallAttempts = 3`, 백오프 `attempt*1500ms`(1.5s, 3s) (upgrade-helper.ts:449,469).
87
+ 2일째 떠 있는 고아 프로세스에는 의미가 없다. 게다가 최종 실패 시 사용자에게
88
+ 가는 신호는 로그 파일 한 줄(`Upgrade helper failed: ...`)뿐 — **어떤 프로세스가
89
+ 막고 있는지, 어떻게 복구하는지** 알려주지 않는다.
90
+
91
+ ### RC3 — Node 24 preinstall 가드 ↔ 멀티-node PATH (별도로 재현 확인됨)
92
+ `preinstall` 가드(아래)는 Windows에서 lifecycle 스크립트를 실행하는 `node`가
93
+ 24+ 면 설치를 중단한다:
94
+ ```jsonc
95
+ // packages/daemon-cloud/package.json, oss/packages/daemon-standalone/package.json
96
+ "preinstall": "node -e \"... if (win32 && major>=24 && !ADHDEV_BOOTSTRAP && !CI) { process.exit(1) }\""
97
+ ```
98
+ npm은 preinstall을 `cmd /c node -e ...` 로, **PATH에서 찾은 bare `node`**로
99
+ 실행한다(npm을 띄운 node가 아님). 이 머신은 `C:\Program Files\nodejs`(Node 24)가
100
+ nvm node보다 PATH 앞에 있어, 일반 `npm i -g adhdev` 는 이 가드에서 바로 죽는다
101
+ (본 사례에서 수동으로 재현됨).
102
+
103
+ `buildInstallEnvWithNodeOnPath()`(upgrade-helper.ts:185-198)가 `dirname(process.execPath)`
104
+ 를 PATH 앞에 붙여 이를 완화하지만, **이는 "헬퍼 자신을 실행한 node가 지원 버전"이라는
105
+ 가정에 의존**한다. 헬퍼가 nvm 심볼릭 링크(`C:\nvm4w\nodejs\node.exe`)로 떴고 그게
106
+ 현재 Node 24를 가리키면, prepend되는 것도 Node 24라 가드가 그대로 발동한다.
107
+ 실제 설치 타깃 node는 `--prefix`(v22.14.0)로 이미 고정돼 있는데도 그렇다.
108
+
109
+ ### RC4 — 스테이징/잔여물 누적
110
+ 잠금이 유지되는 동안 `safeRemoveStaleEntry`는 항상 `EPERM`으로 스킵되어
111
+ `.adhdev-<hash>` 스테이징이 **여러 실행에 걸쳐 그대로 쌓인다**. 보유자가 죽은
112
+ 뒤에 GC하는 경로가 없다. (부수적으로, 헬퍼 밖에서 사용자가 `npm i -g adhdev`를
113
+ 기본 prefix로 돌리면 `AppData\Roaming\npm`에 **두 번째 깨진 설치본**이 생겨
114
+ shadowing 혼란을 유발 — 헬퍼 책임은 아니나 진단 문서엔 남겨둠.)
115
+
116
+ ## 4. 패치 권고
117
+
118
+ ### P1 (필수) — 임의의 네이티브-애드온 보유자 감지·종료
119
+ 설치 직전(및 각 재시도 전)에, **설치 대상 경로의** `conpty.node`(및
120
+ `ghostty-vt.dll`)를 로드 중인 프로세스를 열거해 self/parent를 제외하고 종료한 뒤
121
+ 종료를 기다린다. 이번에 동작 확인된 PowerShell 패턴:
122
+
123
+ ```powershell
124
+ Get-Process node -ErrorAction SilentlyContinue | ForEach-Object {
125
+ $p = $_
126
+ try {
127
+ if ($p.Modules | Where-Object { $_.FileName -ieq $targetConptyPath }) { $p.Id }
128
+ } catch {}
129
+ }
130
+ ```
131
+ - `stopSessionHostProcesses()` 옆에 `stopForeignNativeAddonHolders(installRoot)`
132
+ 형태로 추가. `installCommand.surface.packageRoot` 기준으로 정확한
133
+ `node_modules/node-pty/prebuilds/<plat-arch>/conpty.node` 절대경로를 만들어
134
+ **그 경로를 매핑한 프로세스만** 대상으로 한다(과잉 종료 방지).
135
+ - 종료한 pid + commandLine을 `appendUpgradeLog` 로 남긴다(진단성 확보).
136
+ - `taskkill /T /F`(기존 `killPid`) 재사용 + `waitForPidExit` 로 매핑 해제 대기.
137
+ - 안전장치: 경로 매칭이 모호하면 죽이지 말고 로그만(아래 P2의 사용자 안내로 위임).
138
+
139
+ ### P2 (필수) — 복구 가능한 실패 신호
140
+ 최종 실패 시(또는 보유자 종료 실패 시) 로그뿐 아니라 **사용자에게 보이는 메시지**를
141
+ 남긴다: 막고 있는 pid/commandLine 목록 + 그대로 붙여넣어 복구할 수 있는 수동 명령
142
+ (`Stop-Process -Id ... ; <pinned-node> <npm-cli> install -g adhdev@<v> --prefix <prefix>`).
143
+ 재시도 예산도 현실화(예: 보유자 능동 정리 후 1~2회면 충분하므로, "정리 → 확인 →
144
+ 설치" 순서로 바꾸고 맹목적 백오프 의존을 줄인다).
145
+
146
+ ### P3 (권장) — Node 가드와의 상호작용 견고화
147
+ `buildInstallEnvWithNodeOnPath()` 가 만드는 install env에 **`ADHDEV_BOOTSTRAP=1`
148
+ 을 함께 설정**한다. 자동 업그레이드 경로에서는 실제 런타임 node가 `--prefix`로
149
+ 이미 고정/검증돼 있으므로, lifecycle 가드를 PATH 순서에만 의존해 우회하는 것은
150
+ 취약하다. 더 견고하게는: `process.execPath`의 major가 지원 범위(예: 22) 밖이면
151
+ `installPrefix` 기준으로 지원되는 node를 명시적으로 찾아 npm 실행과 lifecycle
152
+ 스크립트 양쪽에 쓰도록 한다.
153
+ > 주의: 가드 자체를 약화시키지 말 것. 가드는 "사용자 수동 설치"를 막는 용도로
154
+ > 유지하고, **자동 헬퍼 경로에서만** bootstrap 우회를 적용한다.
155
+
156
+ ### P4 (권장) — 스테이징 GC 시점 추가
157
+ 보유자가 모두 사라진 것을 확인한 뒤 `cleanupStaleGlobalInstallDirs` 를 한 번 더
158
+ 돌리고, 가능하면 **CLI 정상 기동 시점**(잠금 없는 상태)에도 1회 GC를 수행해
159
+ 누적된 `.adhdev-<hash>` 를 청소한다.
160
+
161
+ ## 5. 변경 대상 파일
162
+
163
+ - `oss/packages/daemon-core/src/commands/upgrade-helper.ts` — P1~P4 핵심.
164
+ - 신규 `stopForeignNativeAddonHolders()` (P1), `runDaemonUpgradeHelper` 흐름에
165
+ `stopSessionHostProcesses` 직후 호출.
166
+ - 최종 실패 메시지/예산 조정 (P2): `runDaemonUpgradeHelper` 의 설치 루프 +
167
+ `maybeRunDaemonUpgradeHelperFromEnv` 의 catch.
168
+ - install env에 `ADHDEV_BOOTSTRAP` 주입 (P3): `buildInstallEnvWithNodeOnPath`.
169
+ - (가드는 변경 불필요 — 자동 경로에서 env로 우회하는 것이 P3.)
170
+
171
+ ## 6. 검증
172
+
173
+ 1. **재현 픽스처:** `oss/packages/daemon-core/test/commands/daemon-upgrade-runtime-version.test.ts`
174
+ 에 "세션 호스트가 **아닌** 프로세스가 대상 `conpty.node`를 매핑 중" 케이스 추가.
175
+ P1이 그 holder를 감지·종료 대상에 포함하는지 단위 테스트.
176
+ 2. **수동 E2E (Windows):**
177
+ - 대상 설치본의 `conpty.node`를 로드하는 더미 node 프로세스를 띄워 둔다.
178
+ - 구버전에서 자동 업그레이드를 트리거.
179
+ - 기대: 헬퍼가 더미 holder를 로그에 남기고 종료 → 설치 성공, 잔여 staging 없음.
180
+ - 실패 주입(더미를 못 죽이게)했을 때: 사용자에게 pid/commandLine + 수동 복구
181
+ 명령이 표시되는지 확인.
182
+ 3. **회귀:** `buildInstallEnvWithNodeOnPath` + `ADHDEV_BOOTSTRAP` 조합에서
183
+ Node 24가 PATH 앞에 있어도 preinstall 가드가 자동 경로에선 통과하는지 확인.
184
+
185
+ ## 7. 부록 — 이번 사례 수동 복구에 실제로 통한 명령
186
+
187
+ ```powershell
188
+ # 1) conpty.node 보유자 식별
189
+ Get-Process node | ? { try { $_.Modules | ? ModuleName -match 'conpty' } catch {} } | % Id
190
+ # 2) 보유자 종료 (이번엔 34316,37304,56396)
191
+ Stop-Process -Id 34316,37304,56396 -Force
192
+ # 3) 고정 node(22) + 올바른 prefix로 설치 (PATH 앞에 Node22 → preinstall 가드 통과)
193
+ $pfx="C:\Users\kjs0116\AppData\Local\nvm\v22.14.0"; $env:PATH="$pfx;$env:PATH"
194
+ & "$pfx\node.exe" "$pfx\node_modules\npm\bin\npm-cli.js" install -g adhdev@0.9.82-rc.358 --prefix $pfx
195
+ # 4) 잔여 스테이징/엉뚱한 prefix 설치본 정리
196
+ Remove-Item -Recurse -Force "$pfx\node_modules\.adhdev-*" -EA SilentlyContinue
197
+ Remove-Item -Recurse -Force "$env:APPDATA\npm\node_modules\adhdev" -EA SilentlyContinue
198
+ ```