berry-agent 0.1.0-alpha.1

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 (401) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +96 -0
  3. package/dist/.api-emit.stamp +1 -0
  4. package/dist/.build-meta.json +6 -0
  5. package/dist/agent/events.js +1 -0
  6. package/dist/agent/index.js +2 -0
  7. package/dist/agent/loop.js +139 -0
  8. package/dist/agent/queue.js +119 -0
  9. package/dist/agent/stream.js +62 -0
  10. package/dist/agent/tools-batch.js +221 -0
  11. package/dist/agent/types.js +8 -0
  12. package/dist/api/berry-agent-llm.d.ts +18 -0
  13. package/dist/api/berry-agent.d.ts +9 -0
  14. package/dist/api/surface.json +1632 -0
  15. package/dist/api/tsconfig.paths.json +12 -0
  16. package/dist/api/typebox-compile.d.ts +2 -0
  17. package/dist/api/typebox-value.d.ts +2 -0
  18. package/dist/api/typebox.d.ts +2 -0
  19. package/dist/browser/cdp.js +256 -0
  20. package/dist/browser/codes.js +31 -0
  21. package/dist/browser/discover.js +107 -0
  22. package/dist/browser/engine.js +169 -0
  23. package/dist/browser/index.js +17 -0
  24. package/dist/browser/install.js +342 -0
  25. package/dist/browser/page.js +321 -0
  26. package/dist/browser/service.js +158 -0
  27. package/dist/browser/tools.js +198 -0
  28. package/dist/browser/types.js +81 -0
  29. package/dist/channels/ask-queue.js +82 -0
  30. package/dist/channels/commands.js +129 -0
  31. package/dist/channels/engine/cell.js +143 -0
  32. package/dist/channels/engine/diff.js +143 -0
  33. package/dist/channels/engine/engine.js +416 -0
  34. package/dist/channels/engine/index.js +8 -0
  35. package/dist/channels/engine/input-keys.js +86 -0
  36. package/dist/channels/engine/input.js +596 -0
  37. package/dist/channels/engine/memory-io.js +71 -0
  38. package/dist/channels/engine/process-io.js +38 -0
  39. package/dist/channels/engine/types.js +17 -0
  40. package/dist/channels/engine/width.js +122 -0
  41. package/dist/channels/index.js +15 -0
  42. package/dist/channels/registry.js +56 -0
  43. package/dist/channels/sdk/admit.js +29 -0
  44. package/dist/channels/sdk/backend.js +102 -0
  45. package/dist/channels/sdk/cursor.js +56 -0
  46. package/dist/channels/sdk/index.js +20 -0
  47. package/dist/channels/sdk/jsonl.js +208 -0
  48. package/dist/channels/sdk/protocol.js +24 -0
  49. package/dist/channels/sdk/schema.js +91 -0
  50. package/dist/channels/sdk/wire-core.js +446 -0
  51. package/dist/channels/service.js +169 -0
  52. package/dist/channels/tui/autocomplete/autocomplete.js +42 -0
  53. package/dist/channels/tui/autocomplete/file-mentions.js +106 -0
  54. package/dist/channels/tui/autocomplete/popup.js +148 -0
  55. package/dist/channels/tui/autocomplete/provider.js +7 -0
  56. package/dist/channels/tui/autocomplete/token.js +68 -0
  57. package/dist/channels/tui/backend/ansi-rows.js +211 -0
  58. package/dist/channels/tui/backend/main-screen.js +207 -0
  59. package/dist/channels/tui/backend/osc.js +91 -0
  60. package/dist/channels/tui/backend/transcript.js +230 -0
  61. package/dist/channels/tui/backend/tui-backend.js +915 -0
  62. package/dist/channels/tui/editor/editor-model.js +543 -0
  63. package/dist/channels/tui/editor/editor-view.js +140 -0
  64. package/dist/channels/tui/editor/editor.js +275 -0
  65. package/dist/channels/tui/editor/undo-stack.js +35 -0
  66. package/dist/channels/tui/editor/visual-lines.js +126 -0
  67. package/dist/channels/tui/editor/word-nav.js +106 -0
  68. package/dist/channels/tui/history/history-viewer.js +403 -0
  69. package/dist/channels/tui/index.js +3 -0
  70. package/dist/channels/tui/layout.js +167 -0
  71. package/dist/channels/tui/markdown/blocks.js +111 -0
  72. package/dist/channels/tui/markdown/inline.js +75 -0
  73. package/dist/channels/tui/markdown/markdown.js +182 -0
  74. package/dist/channels/tui/memory/memory-viewer.js +540 -0
  75. package/dist/channels/tui/overlay/alt-screen.js +105 -0
  76. package/dist/channels/tui/overlay/overlay.js +57 -0
  77. package/dist/channels/tui/overlay/select-confirm.js +156 -0
  78. package/dist/channels/tui/panels/todo-panel.js +54 -0
  79. package/dist/channels/tui/panels/tool-progress-panel.js +93 -0
  80. package/dist/channels/tui/scroll/scroll-view.js +254 -0
  81. package/dist/channels/tui/status/status-line.js +76 -0
  82. package/dist/channels/tui/text.js +38 -0
  83. package/dist/channels/tui/theme.js +41 -0
  84. package/dist/channels/types.js +11 -0
  85. package/dist/channels/ui-core.js +209 -0
  86. package/dist/checkpoint/capture.js +45 -0
  87. package/dist/checkpoint/codes.js +33 -0
  88. package/dist/checkpoint/command.js +82 -0
  89. package/dist/checkpoint/gate.js +48 -0
  90. package/dist/checkpoint/index.js +20 -0
  91. package/dist/checkpoint/restore.js +172 -0
  92. package/dist/checkpoint/store.js +247 -0
  93. package/dist/checkpoint/types.js +4 -0
  94. package/dist/checkpoint/walk.js +146 -0
  95. package/dist/compaction/ccr-tools.js +112 -0
  96. package/dist/compaction/ccr.js +84 -0
  97. package/dist/compaction/codes.js +21 -0
  98. package/dist/compaction/index.js +18 -0
  99. package/dist/compaction/policy.js +200 -0
  100. package/dist/compaction/service.js +422 -0
  101. package/dist/compaction/slots.js +121 -0
  102. package/dist/compaction/types.js +10 -0
  103. package/dist/context/codes.js +40 -0
  104. package/dist/context/events.js +160 -0
  105. package/dist/context/index.js +13 -0
  106. package/dist/context/logger.js +136 -0
  107. package/dist/context/scope.js +182 -0
  108. package/dist/context/workspace.js +104 -0
  109. package/dist/contracts/agent-events.d.ts +73 -0
  110. package/dist/contracts/agent-events.js +16 -0
  111. package/dist/contracts/api.d.ts +249 -0
  112. package/dist/contracts/api.js +299 -0
  113. package/dist/contracts/approval.d.ts +37 -0
  114. package/dist/contracts/approval.js +9 -0
  115. package/dist/contracts/env-ref.d.ts +41 -0
  116. package/dist/contracts/env-ref.js +32 -0
  117. package/dist/contracts/errors.d.ts +56 -0
  118. package/dist/contracts/errors.js +266 -0
  119. package/dist/contracts/events.d.ts +70 -0
  120. package/dist/contracts/events.js +329 -0
  121. package/dist/contracts/index.d.ts +32 -0
  122. package/dist/contracts/index.js +26 -0
  123. package/dist/contracts/llm.d.ts +275 -0
  124. package/dist/contracts/llm.js +12 -0
  125. package/dist/contracts/messages.d.ts +63 -0
  126. package/dist/contracts/messages.js +65 -0
  127. package/dist/contracts/redact.d.ts +46 -0
  128. package/dist/contracts/redact.js +223 -0
  129. package/dist/contracts/tools.d.ts +197 -0
  130. package/dist/contracts/tools.js +33 -0
  131. package/dist/contracts/types.d.ts +302 -0
  132. package/dist/contracts/types.js +50 -0
  133. package/dist/contracts/ui.d.ts +122 -0
  134. package/dist/contracts/ui.js +15 -0
  135. package/dist/conversation/agent-service.js +61 -0
  136. package/dist/conversation/approval-wiring.js +82 -0
  137. package/dist/conversation/backoff.js +38 -0
  138. package/dist/conversation/codes.js +47 -0
  139. package/dist/conversation/control-tools.js +102 -0
  140. package/dist/conversation/control.js +210 -0
  141. package/dist/conversation/driver.js +922 -0
  142. package/dist/conversation/index.js +12 -0
  143. package/dist/conversation/model-visible.js +169 -0
  144. package/dist/conversation/open-tools.js +126 -0
  145. package/dist/conversation/reseed.js +132 -0
  146. package/dist/conversation/sessions.js +215 -0
  147. package/dist/conversation/todo.js +203 -0
  148. package/dist/conversation/types.js +35 -0
  149. package/dist/conversation/wiring.js +204 -0
  150. package/dist/credentials/codes.js +59 -0
  151. package/dist/credentials/commands.js +186 -0
  152. package/dist/credentials/env-ref.js +34 -0
  153. package/dist/credentials/index.js +28 -0
  154. package/dist/credentials/migration.js +18 -0
  155. package/dist/credentials/oauth.js +266 -0
  156. package/dist/credentials/refresh.js +152 -0
  157. package/dist/credentials/secrets.js +118 -0
  158. package/dist/credentials/types.js +30 -0
  159. package/dist/exec/bash.js +279 -0
  160. package/dist/exec/codes.js +37 -0
  161. package/dist/exec/env.js +99 -0
  162. package/dist/exec/environment.js +39 -0
  163. package/dist/exec/git-guard.js +623 -0
  164. package/dist/exec/index.js +24 -0
  165. package/dist/exec/registry.js +120 -0
  166. package/dist/exec/spawn.js +281 -0
  167. package/dist/exec/tail.js +77 -0
  168. package/dist/exec/types.js +5 -0
  169. package/dist/goal/codes.js +33 -0
  170. package/dist/goal/command.js +175 -0
  171. package/dist/goal/fold.js +121 -0
  172. package/dist/goal/gates.js +122 -0
  173. package/dist/goal/index.js +22 -0
  174. package/dist/goal/migration.js +48 -0
  175. package/dist/goal/service.js +501 -0
  176. package/dist/goal/todo-tool.js +174 -0
  177. package/dist/goal/types.js +1 -0
  178. package/dist/goal/update-tool.js +46 -0
  179. package/dist/host/approval-cmd.js +276 -0
  180. package/dist/host/assembly.js +1146 -0
  181. package/dist/host/boot-failures.js +104 -0
  182. package/dist/host/budget-advisory.js +74 -0
  183. package/dist/host/budget-broadcast.js +50 -0
  184. package/dist/host/builtins.js +54 -0
  185. package/dist/host/cli.js +544 -0
  186. package/dist/host/codes.js +120 -0
  187. package/dist/host/config-schema.js +195 -0
  188. package/dist/host/conversation-stack.js +654 -0
  189. package/dist/host/core-plugins.js +1685 -0
  190. package/dist/host/credentials-cmd.js +42 -0
  191. package/dist/host/disclosure.js +30 -0
  192. package/dist/host/dispatch.js +105 -0
  193. package/dist/host/doors-cmd.js +154 -0
  194. package/dist/host/dump-config.js +44 -0
  195. package/dist/host/hook-dispatch-guard.js +27 -0
  196. package/dist/host/import-gate.js +153 -0
  197. package/dist/host/index.js +54 -0
  198. package/dist/host/issue-session.js +241 -0
  199. package/dist/host/loader.js +372 -0
  200. package/dist/host/main.js +161 -0
  201. package/dist/host/manifest.js +381 -0
  202. package/dist/host/mcp-entry.js +53 -0
  203. package/dist/host/plugin-boot.js +952 -0
  204. package/dist/host/plugin-context.js +672 -0
  205. package/dist/host/plugin-install.js +480 -0
  206. package/dist/host/plugin-reload.js +124 -0
  207. package/dist/host/plugin-store.js +462 -0
  208. package/dist/host/plugin-tools.js +359 -0
  209. package/dist/host/plugin-uninstall.js +257 -0
  210. package/dist/host/plugins-cmd.js +302 -0
  211. package/dist/host/plugins-command.js +130 -0
  212. package/dist/host/plugins-config.js +195 -0
  213. package/dist/host/prompt-sections.js +133 -0
  214. package/dist/host/run-entry.js +544 -0
  215. package/dist/host/runtime.js +209 -0
  216. package/dist/host/scheduler-tick.js +294 -0
  217. package/dist/host/serve-daemon.js +501 -0
  218. package/dist/host/serve-entry.js +279 -0
  219. package/dist/host/session-anchor.js +41 -0
  220. package/dist/host/sessions-cmd.js +219 -0
  221. package/dist/host/sessions-face.js +47 -0
  222. package/dist/host/settings-store.js +112 -0
  223. package/dist/host/signals.js +69 -0
  224. package/dist/host/single-instance.js +105 -0
  225. package/dist/host/subagent-factory.js +291 -0
  226. package/dist/host/testkit/harness.js +283 -0
  227. package/dist/host/testkit/index.js +14 -0
  228. package/dist/host/testkit/matrix.js +302 -0
  229. package/dist/host/tool-policy-store.js +177 -0
  230. package/dist/host/triggers.js +212 -0
  231. package/dist/host/tui-entry.js +216 -0
  232. package/dist/host/webui-bridge.js +165 -0
  233. package/dist/issue/codes.js +38 -0
  234. package/dist/issue/filter.js +146 -0
  235. package/dist/issue/github.js +116 -0
  236. package/dist/issue/index.js +17 -0
  237. package/dist/issue/mount.js +70 -0
  238. package/dist/issue/poll.js +58 -0
  239. package/dist/issue/service.js +341 -0
  240. package/dist/issue/tools.js +70 -0
  241. package/dist/issue/types.js +34 -0
  242. package/dist/issue/webhook.js +126 -0
  243. package/dist/llm/codes.js +77 -0
  244. package/dist/llm/complete.js +196 -0
  245. package/dist/llm/events.js +15 -0
  246. package/dist/llm/index.js +24 -0
  247. package/dist/llm/inflight.js +65 -0
  248. package/dist/llm/model-id.js +58 -0
  249. package/dist/llm/provider-face.d.ts +32 -0
  250. package/dist/llm/provider-face.js +34 -0
  251. package/dist/llm/recovery.js +146 -0
  252. package/dist/llm/runtime.js +46 -0
  253. package/dist/llm/stream-fn.js +147 -0
  254. package/dist/lsp/codes.js +31 -0
  255. package/dist/lsp/connection.js +147 -0
  256. package/dist/lsp/frame.js +110 -0
  257. package/dist/lsp/index.js +16 -0
  258. package/dist/lsp/inject.js +58 -0
  259. package/dist/lsp/instance.js +264 -0
  260. package/dist/lsp/service.js +522 -0
  261. package/dist/lsp/tools.js +53 -0
  262. package/dist/lsp/types.js +201 -0
  263. package/dist/mcp/bridge.js +222 -0
  264. package/dist/mcp/codes.js +21 -0
  265. package/dist/mcp/index.js +19 -0
  266. package/dist/mcp/jsonrpc.js +230 -0
  267. package/dist/mcp/service.js +125 -0
  268. package/dist/mcp/tools.js +145 -0
  269. package/dist/mcp/types.js +108 -0
  270. package/dist/memory/cite.js +131 -0
  271. package/dist/memory/codes.js +50 -0
  272. package/dist/memory/command.js +71 -0
  273. package/dist/memory/consolidate.js +219 -0
  274. package/dist/memory/cycle.js +109 -0
  275. package/dist/memory/dao.js +878 -0
  276. package/dist/memory/diff.js +238 -0
  277. package/dist/memory/extract.js +181 -0
  278. package/dist/memory/fts.js +58 -0
  279. package/dist/memory/index.js +42 -0
  280. package/dist/memory/inject.js +354 -0
  281. package/dist/memory/merge.js +178 -0
  282. package/dist/memory/migration.js +132 -0
  283. package/dist/memory/pollution.js +59 -0
  284. package/dist/memory/port.js +385 -0
  285. package/dist/memory/review.js +201 -0
  286. package/dist/memory/scan.js +89 -0
  287. package/dist/memory/tools.js +508 -0
  288. package/dist/memory/types.js +140 -0
  289. package/dist/obs/codes.js +38 -0
  290. package/dist/obs/db.js +115 -0
  291. package/dist/obs/index.js +20 -0
  292. package/dist/obs/rollup.js +61 -0
  293. package/dist/obs/service.js +363 -0
  294. package/dist/obs/session-tools.js +282 -0
  295. package/dist/obs/session-view.js +225 -0
  296. package/dist/obs/tool.js +97 -0
  297. package/dist/obs/types.js +1 -0
  298. package/dist/persist/audit.js +63 -0
  299. package/dist/persist/aux.js +39 -0
  300. package/dist/persist/codes.js +35 -0
  301. package/dist/persist/index.js +30 -0
  302. package/dist/persist/load-history.js +50 -0
  303. package/dist/persist/migrations.js +37 -0
  304. package/dist/persist/paths.js +108 -0
  305. package/dist/persist/persistence.js +229 -0
  306. package/dist/persist/schema.js +99 -0
  307. package/dist/persist/secret-box.js +95 -0
  308. package/dist/persist/store.js +706 -0
  309. package/dist/persist/write-behind.js +238 -0
  310. package/dist/safety/approval.js +171 -0
  311. package/dist/safety/bwrap.js +123 -0
  312. package/dist/safety/codes.js +57 -0
  313. package/dist/safety/danger.js +505 -0
  314. package/dist/safety/gate.js +226 -0
  315. package/dist/safety/index.js +24 -0
  316. package/dist/safety/presets.js +73 -0
  317. package/dist/safety/roots.js +139 -0
  318. package/dist/safety/sandbox.js +214 -0
  319. package/dist/safety/seatbelt.js +88 -0
  320. package/dist/safety/sensitive.js +34 -0
  321. package/dist/safety/tool-policy.js +203 -0
  322. package/dist/safety/types.js +13 -0
  323. package/dist/scheduler/codes.js +54 -0
  324. package/dist/scheduler/cron-backend.js +135 -0
  325. package/dist/scheduler/engine.js +280 -0
  326. package/dist/scheduler/gates.js +60 -0
  327. package/dist/scheduler/index.js +25 -0
  328. package/dist/scheduler/migration.js +22 -0
  329. package/dist/scheduler/runner.js +171 -0
  330. package/dist/scheduler/schedule.js +193 -0
  331. package/dist/scheduler/service.js +283 -0
  332. package/dist/scheduler/tick.js +139 -0
  333. package/dist/scheduler/types.js +1 -0
  334. package/dist/sdk/http.js +865 -0
  335. package/dist/sdk/index.js +22 -0
  336. package/dist/sdk/mcp.js +289 -0
  337. package/dist/sdk/plugin-route-registry.js +126 -0
  338. package/dist/sdk/plugin-routes.js +149 -0
  339. package/dist/sdk/security.js +116 -0
  340. package/dist/sdk/types.js +32 -0
  341. package/dist/session/budget.js +159 -0
  342. package/dist/session/codes.js +40 -0
  343. package/dist/session/derive.js +180 -0
  344. package/dist/session/event-data.js +1 -0
  345. package/dist/session/fork.js +72 -0
  346. package/dist/session/import-gates.js +148 -0
  347. package/dist/session/index.js +18 -0
  348. package/dist/session/recover.js +103 -0
  349. package/dist/session/session.js +279 -0
  350. package/dist/session/snapshot.js +107 -0
  351. package/dist/skills/agents.js +242 -0
  352. package/dist/skills/codes.js +51 -0
  353. package/dist/skills/discovery.js +266 -0
  354. package/dist/skills/frontmatter.js +215 -0
  355. package/dist/skills/index.js +28 -0
  356. package/dist/skills/load.js +118 -0
  357. package/dist/skills/manage.js +201 -0
  358. package/dist/skills/registry.js +138 -0
  359. package/dist/skills/render.js +124 -0
  360. package/dist/skills/sections.js +171 -0
  361. package/dist/skills/types.js +21 -0
  362. package/dist/subagent/codes.js +50 -0
  363. package/dist/subagent/declarative.js +55 -0
  364. package/dist/subagent/index.js +19 -0
  365. package/dist/subagent/notify.js +51 -0
  366. package/dist/subagent/provide.js +7 -0
  367. package/dist/subagent/registry.js +149 -0
  368. package/dist/subagent/service.js +338 -0
  369. package/dist/subagent/surface.js +44 -0
  370. package/dist/subagent/tool.js +153 -0
  371. package/dist/subagent/types.js +25 -0
  372. package/dist/tools/apply-patch.js +164 -0
  373. package/dist/tools/codes.js +109 -0
  374. package/dist/tools/fs.js +501 -0
  375. package/dist/tools/index.js +17 -0
  376. package/dist/tools/observed.js +99 -0
  377. package/dist/tools/pipeline.js +176 -0
  378. package/dist/tools/protected-read.js +54 -0
  379. package/dist/tools/registry.js +250 -0
  380. package/dist/tools/search.js +451 -0
  381. package/dist/tools/worktree.js +327 -0
  382. package/dist/web/codes.js +37 -0
  383. package/dist/web/gate.js +43 -0
  384. package/dist/web/hygiene.js +195 -0
  385. package/dist/web/index.js +16 -0
  386. package/dist/web/service.js +207 -0
  387. package/dist/web/ssrf-guard.js +44 -0
  388. package/dist/web/tool.js +76 -0
  389. package/dist/web/types.js +10 -0
  390. package/dist/webui/assets/index-B-t2O8gh.js +22 -0
  391. package/dist/webui/assets/index-DOlgSZ9Q.css +2 -0
  392. package/dist/webui/index.html +14 -0
  393. package/dist/webui/index.js +14 -0
  394. package/dist/webui/server.js +549 -0
  395. package/dist/webui/types.js +38 -0
  396. package/examples/README.md +26 -0
  397. package/examples/minimal-code-plugin/entry.js +43 -0
  398. package/examples/minimal-code-plugin/package.json +12 -0
  399. package/examples/pure-skill-pack/package.json +14 -0
  400. package/examples/pure-skill-pack/skills/markdown-table/SKILL.md +32 -0
  401. package/package.json +86 -0
@@ -0,0 +1,501 @@
1
+ /**
2
+ * host/serve-daemon — daemon 编舞与管理动词(批 13e-3;07 §5 serve 旗标块
3
+ * daemon/status/stop 三动词 + 02 数据域表 serve/ 行三足迹)。
4
+ *
5
+ * 三动词分职:
6
+ * - **spawner**(`berry-agent serve --daemon`):spawn 自镜像 detached 子进程
7
+ * (POSIX setsid 族 = Node `detached: true`;stdio stdin/stdout ignore——
8
+ * daemon 与 stdio 传输互斥〔07 §5〕、stderr 追加重定向 daemon.log)后即退;
9
+ * 起活确认窗内轮询 pid 登记(child 写 pid 才算起活)——child 秒死(撞
10
+ * HOST_DATA_DIR_BUSY 等)则转发其退出码并转述 daemon.log 尾行。child 身份
11
+ * 经 env `BERRY_AGENT_SERVE_DAEMON_CHILD=1` 标记(不入 CLI 词面——env 双
12
+ * 载体先例 BERRY_AGENT_SDK_TOKEN 同族),防 spawn 递归。
13
+ * - **daemon child**(`runDaemonServe`):组装 runtime + conversation 栈 +
14
+ * serve 装配桥 → core:sdk 件 kit createFace(批 19e 装载晚绑——件禁用
15
+ * ⇒ kit 缺席 ⇒ 拒启退 2,07 §5;sock = 数据目录 serve/daemon.sock
16
+ * 缺省接入点;TCP 侧 --sdk-port/--sdk-host 可选 SDK 面 + `--port` 人面
17
+ * 〔18a-3' 三入口咬合——TCP 侧多监听并存,webui 路由经 core:webui 件 kit
18
+ * 挂进本面(件禁用 = warn 不挂——/api/* 404 面仍在),daemon 人面 =
19
+ * serve HTTP 面 TCP 侧〕)→ 写 pid 登记 → token
20
+ * 披露一行进 stderr(= daemon.log——自动生成档唯一披露位)→ 常驻至优雅停
21
+ * (SIGTERM → main 信号编舞 → runtime 六步退出序 → 本件 closer:face.stop
22
+ * + 清 pid 登记)。崩溃恢复语义与前台同一条(durable 投影——04 §1 P4),
23
+ * 本件零第二套恢复路径。
24
+ * - **status/stop**(只读豁免面——05 §6.6):status 只读 pid 登记文件不动库
25
+ * (退出码 0 = 在跑 / 1 = 未跑或登记损坏 / 2 = 用法错);stop = SIGTERM →
26
+ * 待退出(10s 窗〔07 §5 落码定名批定值〕后升格 SIGKILL)→ 清 pid/sock
27
+ * 登记——增量只有发信号/待退出/清登记三动作,未运行幂等退 0。
28
+ *
29
+ * 双登记分职注记:`active.json`(single-instance 件)= 单活跃机执法标记
30
+ * (runtime 组装自持);`serve/daemon.pid` = daemon 编舞足迹(status/stop
31
+ * 消费——02 数据域表 serve/ 行明列)。两文件两职责并存,记录同形
32
+ * `{pid, startedAt}`(uptime = now - startedAt 的报告源)。
33
+ *
34
+ * 裁量钉位(规范未明文、本笔定形):起活确认窗 10s(child 含运行时组装
35
+ * 秒级;与 stop 升格窗同值族);status 活跃会话数经 sock 面 GET /v1/sessions
36
+ * 计数——仅在 env 配得 BERRY_AGENT_SDK_TOKEN(显式 token 形态)时可查,
37
+ * 自动生成档无凭证打 `n/a`(05 §6.6 豁免面 = 不动库,sock 面是唯一不动库
38
+ * 的会话读径)。
39
+ */
40
+ import { spawn } from 'node:child_process';
41
+ import { mkdirSync, openSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
42
+ import { join } from 'node:path';
43
+ import { stderr as defaultStderr } from 'node:process';
44
+ import { resolveDataDir } from '../persist/index.js';
45
+ import { createSdkHttpFace, judgeListenConfig, } from '../sdk/index.js';
46
+ import { WEBUI_DEFAULT_HOST } from '../webui/index.js';
47
+ import { createServeBridge } from './serve-entry.js';
48
+ import { assembleHostStack } from './assembly.js';
49
+ import { startSchedulerClock } from './core-plugins.js';
50
+ /* ---------------- 足迹词面(02 数据域表 serve/ 行) ---------------- */
51
+ /** daemon 三足迹所在子目录名(数据目录下) */
52
+ export const SERVE_DIR_BASENAME = 'serve';
53
+ /** pid 登记文件名(status/stop 消费——uptime 报告源) */
54
+ export const DAEMON_PID_BASENAME = 'daemon.pid';
55
+ /** Unix-domain socket 监听位文件名(HTTP 面缺省接入点) */
56
+ export const DAEMON_SOCK_BASENAME = 'daemon.sock';
57
+ /** stderr 重定向日志文件名(token 披露位/崩溃取证面) */
58
+ export const DAEMON_LOG_BASENAME = 'daemon.log';
59
+ /** child 身份 env 标记(不入 CLI 词面——防 spawn 递归) */
60
+ export const DAEMON_CHILD_ENV = 'BERRY_AGENT_SERVE_DAEMON_CHILD';
61
+ /** stop 升格 SIGKILL 窗(07 §5 落码定名批定值 10s) */
62
+ export const STOP_SIGKILL_GRACE_MS = 10_000;
63
+ /** spawner 起活确认窗(裁量钉位——child 写 pid 登记才算起活) */
64
+ export const SPAWN_CONFIRM_TIMEOUT_MS = 10_000;
65
+ /** 三足迹路径推导(数据目录 → serve/ 三件) */
66
+ export function daemonPaths(dataDir) {
67
+ const dir = join(dataDir, SERVE_DIR_BASENAME);
68
+ return {
69
+ dir,
70
+ pidPath: join(dir, DAEMON_PID_BASENAME),
71
+ sockPath: join(dir, DAEMON_SOCK_BASENAME),
72
+ logPath: join(dir, DAEMON_LOG_BASENAME),
73
+ };
74
+ }
75
+ const DEFAULT_FS = {
76
+ ensureDir: (dir) => mkdirSync(dir, { recursive: true }),
77
+ writeFile: (path, text) => writeFileSync(path, text),
78
+ readFile: (path) => readFileSync(path, 'utf8'),
79
+ tryUnlink: (path) => {
80
+ try {
81
+ unlinkSync(path);
82
+ }
83
+ catch {
84
+ // ENOENT 幂等;其余错上抛(数据目录病态 fail-loud)
85
+ }
86
+ },
87
+ };
88
+ /** 进程探活缺省实现(信号 0——与 single-instance 同式) */
89
+ function defaultIsAlive(pid) {
90
+ try {
91
+ process.kill(pid, 0);
92
+ return true;
93
+ }
94
+ catch (err) {
95
+ return err.code === 'EPERM';
96
+ }
97
+ }
98
+ /** 写 pid 登记(child 起活序——face 监听成功后写) */
99
+ export function writeDaemonPid(paths, record, fs = {}) {
100
+ const f = { ...DEFAULT_FS, ...fs };
101
+ f.ensureDir(paths.dir);
102
+ f.writeFile(paths.pidPath, `${JSON.stringify(record)}\n`);
103
+ }
104
+ /**
105
+ * 读 pid 登记探活:返回 running 与登记原文。
106
+ * 登记缺席/坏 JSON/pid 字段坏 = 未跑(status 同码 1 的「登记损坏罕见路径
107
+ * 同码 1、接受并注记」语义——probe 不区分未跑与坏登记,报告面打注记)。
108
+ */
109
+ export function probeDaemon(paths, isAlive = defaultIsAlive, fs = {}) {
110
+ const f = { ...DEFAULT_FS, ...fs };
111
+ let text;
112
+ try {
113
+ text = f.readFile(paths.pidPath);
114
+ }
115
+ catch {
116
+ return { running: false, malformed: false }; // 登记缺席 = 未跑(常态)
117
+ }
118
+ try {
119
+ const parsed = JSON.parse(text);
120
+ if (typeof parsed.pid !== 'number' || !Number.isInteger(parsed.pid) || parsed.pid <= 0) {
121
+ return { running: false, malformed: true };
122
+ }
123
+ const record = {
124
+ pid: parsed.pid,
125
+ startedAt: typeof parsed.startedAt === 'number' ? parsed.startedAt : 0,
126
+ };
127
+ return { running: isAlive(record.pid), record, malformed: false };
128
+ }
129
+ catch {
130
+ return { running: false, malformed: true };
131
+ }
132
+ }
133
+ /** 清 pid/sock 登记(stop 收尾 + 未跑残留清扫——幂等) */
134
+ export function clearDaemonFootprints(paths, fs = {}) {
135
+ const f = { ...DEFAULT_FS, ...fs };
136
+ f.tryUnlink(paths.pidPath);
137
+ f.tryUnlink(paths.sockPath);
138
+ }
139
+ /**
140
+ * serve status:探活与报告(运行态/pid/uptime/活跃会话数)。
141
+ * 退出码 0 = 在跑 / 1 = 未跑(含登记损坏罕见路径——接受并注记)。
142
+ * 活跃会话数:env 配得 token 才经 sock 面查(自动生成档无凭证打 n/a)。
143
+ */
144
+ export async function runServeStatus(options) {
145
+ const paths = daemonPaths(options.dataDir);
146
+ const write = options.write ?? ((line) => process.stdout.write(`${line}\n`));
147
+ const probe = probeDaemon(paths, options.isAlive ?? defaultIsAlive, options.fs);
148
+ if (!probe.running) {
149
+ write(probe.malformed ? '未运行(pid 登记损坏——serve stop 可清残留登记)' : '未运行');
150
+ return 1;
151
+ }
152
+ const now = options.now ?? (() => Date.now());
153
+ const uptimeSec = Math.max(0, Math.round((now() - (probe.record?.startedAt ?? 0)) / 1000));
154
+ // 活跃会话数:显式 token 形态经 sock 面查(豁免面不动库——sock 是唯一读径)
155
+ const env = options.env ?? process.env;
156
+ const token = env.BERRY_AGENT_SDK_TOKEN;
157
+ let sessionsText = 'n/a(自动 token 形态无查询凭证——配 BERRY_AGENT_SDK_TOKEN 可查)';
158
+ if (token !== undefined && token !== '') {
159
+ const count = await options.fetchSessionCount?.(paths.sockPath, token);
160
+ sessionsText = count === undefined ? 'n/a(sock 面不可达)' : String(count);
161
+ }
162
+ write(`运行中(pid ${probe.record?.pid},uptime ${uptimeSec}s,活跃会话 ${sessionsText})`);
163
+ return 0;
164
+ }
165
+ /**
166
+ * serve stop:优雅停编舞(04 §1 信号退出序为真源——SIGTERM 走 SIGINT① 同款
167
+ * 优雅序)。增量只有发信号/待退出/清登记三动作:SIGTERM → 轮询探活至 10s 窗
168
+ * (STOP_SIGKILL_GRACE_MS)→ 仍活升格 SIGKILL → 清 pid/sock 登记。
169
+ * 未运行(登记缺/坏/pid 死)= 清残留登记后幂等退 0(自动化脚本可裸调)。
170
+ */
171
+ export async function runServeStop(options) {
172
+ const paths = daemonPaths(options.dataDir);
173
+ const write = options.write ?? ((line) => process.stdout.write(`${line}\n`));
174
+ const isAlive = options.isAlive ?? defaultIsAlive;
175
+ const signal = options.signal ?? ((pid, sig) => process.kill(pid, sig));
176
+ const sleep = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
177
+ const now = options.now ?? (() => Date.now());
178
+ const probe = probeDaemon(paths, isAlive, options.fs);
179
+ if (!probe.running || probe.record === undefined) {
180
+ clearDaemonFootprints(paths, options.fs); // 幂等清残留(含坏登记清扫)
181
+ write('未运行(登记已清)');
182
+ return 0;
183
+ }
184
+ const pid = probe.record.pid;
185
+ write(`发 SIGTERM(pid ${pid})——优雅停编舞`);
186
+ signal(pid, 'SIGTERM');
187
+ const deadline = now() + STOP_SIGKILL_GRACE_MS;
188
+ while (isAlive(pid) && now() < deadline) {
189
+ await sleep(100);
190
+ }
191
+ if (isAlive(pid)) {
192
+ write(`优雅窗(10s)尽仍未退——升格 SIGKILL(pid ${pid})`);
193
+ signal(pid, 'SIGKILL');
194
+ // SIGKILL 后短轮询收尸(内核异步收——不假设立即死)
195
+ const killDeadline = now() + 2_000;
196
+ while (isAlive(pid) && now() < killDeadline) {
197
+ await sleep(50);
198
+ }
199
+ }
200
+ clearDaemonFootprints(paths, options.fs);
201
+ write('已停(pid/sock 登记已清)');
202
+ return 0;
203
+ }
204
+ /**
205
+ * daemon 开面配置(child 实配与 spawner 预拦同参真源——judgeListenConfig
206
+ * 双侧执法)。TCP 侧多监听并存(18a-3'):人面 spec 恒推首位(`--port` 恒
207
+ * 回环绑定——远程暴露走 --sdk-host 显式载体;SDK 面独立可选)。归一前提:
208
+ * 判定与实配都按「数组首位 = 人面」约定消费 info.tcp[0]。
209
+ */
210
+ /**
211
+ * env 双载体回落位(07 §5:BERRY_AGENT_SDK_PORT/SDK_HOST/SDK_TOKEN 三名——
212
+ * 旗标缺席时 env 补位,旗标恒胜出):port 字串全串 /^\d+$/ 判 fail-loud
213
+ * (lane 帽同律——parseInt 截停会把 '8080x' 类尾随垃圾静默放行成 8080)。
214
+ */
215
+ function readSdkPortEnv(raw) {
216
+ if (raw === undefined)
217
+ return undefined;
218
+ if (!/^\d+$/.test(raw)) {
219
+ throw new RangeError(`BERRY_AGENT_SDK_PORT 须为非负整数字串,收到 ${raw}——坏值是死配置(fail-loud 拒启)`);
220
+ }
221
+ return Number.parseInt(raw, 10);
222
+ }
223
+ function daemonListenConfig(paths, flags, env) {
224
+ const tcpSpecs = [];
225
+ if (flags.port !== undefined)
226
+ tcpSpecs.push({ host: WEBUI_DEFAULT_HOST, port: flags.port });
227
+ // SDK 面旗标 ?? env 双载体回落(07 §5 三名——旗标恒胜出,env 补位同走
228
+ // 下方 judgeListenConfig 三防线执法)
229
+ const sdkPort = flags.sdkPort ?? readSdkPortEnv(env.BERRY_AGENT_SDK_PORT);
230
+ const sdkHost = flags.sdkHost ?? env.BERRY_AGENT_SDK_HOST;
231
+ if (sdkPort !== undefined || sdkHost !== undefined) {
232
+ tcpSpecs.push({ host: sdkHost ?? '127.0.0.1', port: sdkPort ?? 0 });
233
+ }
234
+ return {
235
+ socketPath: paths.sockPath,
236
+ ...(tcpSpecs.length > 0 ? { tcp: tcpSpecs } : {}),
237
+ ...(env.BERRY_AGENT_SDK_TOKEN !== undefined ? { token: env.BERRY_AGENT_SDK_TOKEN } : {}),
238
+ };
239
+ }
240
+ /**
241
+ * daemon child 主体:HTTP 面常驻(stdio 环零装配——daemon 与 stdio 传输互斥)。
242
+ *
243
+ * 装配序:开面判定(judgeListenConfig——非回环 × 无 token 退 2,fail-closed
244
+ * 不豁免)→ 装配公共段(批 19a-3 迁 assembly 件:运行时〔单活跃机占标记在先
245
+ * ——撞在场 daemon 即 HOST_DATA_DIR_BUSY〕→ 共享根 → 栈 → **插件装载**〔core
246
+ * 注册表 + enabled.yaml 真跑——无人值守主通道装载面与 TUI 同构〕)→ face
247
+ * (sock 缺省 + TCP 可选)→ 写 pid 登记 → token 披露(自动生成档唯一披露位 =
248
+ * stderr → daemon.log)→ 常驻 await。
249
+ * 退出序:runtime closer(face.stop + 清 pid)——SIGTERM 经 main 信号编舞
250
+ * → runtime.shutdown 六步触发(closer drain 序 = 注册序——webui-server 先
251
+ * 注册先摘挂,再停面);测试直调注入 runtime.shutdown 同径。
252
+ */
253
+ export async function runDaemonServe(options) {
254
+ const env = options.env ?? process.env;
255
+ const writeErr = options.writeErr ?? ((line) => defaultStderr.write(`${line}\n`));
256
+ const dataDir = options.dataDir ?? resolveDataDir();
257
+ const paths = daemonPaths(dataDir);
258
+ // —— 开面判定(三防线①——非回环绑定必配凭证,违例 daemon 形退 2)——
259
+ const config = daemonListenConfig(paths, options.flags, env);
260
+ const judged = judgeListenConfig(config);
261
+ if (!judged.ok) {
262
+ writeErr(`拒启:${judged.reason}`);
263
+ return 2;
264
+ }
265
+ // —— 装配公共段(与 TUI 同一合成代码路径——装载面随批 19a 活化)——
266
+ const assembly = await assembleHostStack({
267
+ runtime: { dataDir },
268
+ noPlugins: false,
269
+ debug: false, // 零旗标面:daemon 日志级只走 env(BERRY_AGENT_LOG_LEVEL)
270
+ version: options.version ?? '0.0.0-unknown',
271
+ ...(options.providers !== undefined ? { providers: options.providers } : {}),
272
+ ...(options.model !== undefined ? { model: options.model } : {}),
273
+ ...(options.env !== undefined ? { env: options.env } : {}),
274
+ ...(options.sandboxMode !== undefined ? { sandboxMode: options.sandboxMode } : {}),
275
+ ...(options.onRuntime !== undefined ? { onRuntime: options.onRuntime } : {}),
276
+ ...(options.corePlugins !== undefined ? { corePlugins: options.corePlugins } : {}),
277
+ });
278
+ if (!assembly.ok) {
279
+ writeErr(`${assembly.crashed ? `daemon 运行失败:${assembly.message}` : assembly.message}`);
280
+ return assembly.exitCode;
281
+ }
282
+ const { runtime, stack, scope } = assembly;
283
+ // —— sdk 件在场性执法(批 19e——07 §5 daemon 拒启律):daemon 接入面本体
284
+ // = sdk HTTP 面,core:sdk 件禁用/零装载 ⇒ kit 缺席 ⇒ 拒启退 2(与开面
285
+ // 判定拒启同码族——配置档干净退出不写 crash.log,enabled.yaml 可修)——
286
+ const sdkKit = scope.tryGet('sdk-http-face');
287
+ if (sdkKit === undefined) {
288
+ writeErr('拒启:core:sdk 件未装载(daemon 接入面本体缺席——enabled.yaml 禁用即拒启,07 §5)');
289
+ await runtime.shutdown().catch(() => { });
290
+ return 2;
291
+ }
292
+ const faceFactory = options.faceFactory ?? sdkKit.createFace;
293
+ // —— scheduler 挂钟起钟(批 20c——长驻形编舞;daemon = 宿主停机期不断
294
+ // 保的形态,挂钟常驻尤其正业):置位在 sdk 拒启检查后——拒启形不起钟;
295
+ // 件在场即起 + 停钟挂 closer(同 TUI/serve 律)——
296
+ startSchedulerClock(scope, runtime);
297
+ const bridge = createServeBridge(stack, runtime, { cwd: options.cwd ?? process.cwd() });
298
+ // sock 目录先建(三足迹目录 serve/——face 监听与 pid 登记共同前置;
299
+ // 修前 face.start 先跑而目录由 writeDaemonPid 后建——listen 对缺目录
300
+ // EACCES/ENOENT,回归锁 = 全环集成例「face 起 → pid 登记」序)
301
+ mkdirSync(paths.dir, { recursive: true });
302
+ const face = faceFactory({
303
+ config,
304
+ bridge,
305
+ // U5-2 构造期 replay:装载序已受理的插件道路由快照注入 routes 位
306
+ // (受理与挂载两时点解耦——03 §10.6 时序缝定形;kit 缺位 = 测试替身形零路由)
307
+ ...(sdkKit.pluginRoutes !== undefined ? { routes: sdkKit.pluginRoutes.snapshot() } : {}),
308
+ coreOptions: {
309
+ ...(options.heartbeatIntervalMs !== undefined ? { heartbeatIntervalMs: options.heartbeatIntervalMs } : {}),
310
+ // --no-delta 宿主立场缺省(07 §5——与 stdio 形 serve-entry 同款传位)
311
+ initialConnectionNoDelta: options.flags.noDelta,
312
+ },
313
+ });
314
+ let info;
315
+ try {
316
+ info = await face.start();
317
+ }
318
+ catch (err) {
319
+ writeErr(`HTTP 面启动失败:${err instanceof Error ? err.message : String(err)}`);
320
+ await runtime.shutdown().catch(() => { });
321
+ return 1;
322
+ }
323
+ // 起活三足:pid 登记(spawner 确认源)+ token 披露 + 信封回流
324
+ writeDaemonPid(paths, { pid: process.pid, startedAt: Date.now() });
325
+ // U5-2 晚注册路挂接:此后受理(/reload 换代重注册)走 face.register——
326
+ // 构造期已注入 snapshot,此处不重放(双注册 throw 防线)
327
+ sdkKit.pluginRoutes?.attachFace(face);
328
+ stack.channels.addBackend(face.backend); // 信封回流自动馈送(conversation-stack onEvent → emit)
329
+ // —— --port 人面挂载(18a-3' 三入口咬合;04 §1 daemon 人面 = serve HTTP
330
+ // 面 TCP 侧):webui 路由族经 core:webui 件 kit 挂进本面(批 19e 装载晚
331
+ // 绑——件 apply 期只 provide,开面后 mountOnFace;面归 daemon 单源零第
332
+ // 二套映射);kit 缺席 = 件禁用语义族——warn 一行不挂(/api/* 404 面
333
+ // 仍在,/v1/* 程序调用族不受累)——
334
+ let webuiMount;
335
+ if (options.flags.port !== undefined) {
336
+ const webuiKit = scope.tryGet('webui-face-mount');
337
+ if (webuiKit !== undefined) {
338
+ webuiMount = webuiKit.mountOnFace(face);
339
+ }
340
+ else {
341
+ writeErr('warn:core:webui 件未装载——人面 Web 界面与 /api/* 缺席(/v1/* 仍在场)');
342
+ }
343
+ }
344
+ // —— issue webhook 挂载(批 19e——core:issue 件 kit 晚绑):件装载且
345
+ // 装配面在场时 mount 将 webhook 路由族挂进本面;kit 缺席 = 件零装载
346
+ // (主闸链缺席静默——daemon 形同律,不 warn)——
347
+ const issueMountKit = scope.tryGet('issue-webhook-mount');
348
+ const issueWebhook = issueMountKit !== undefined ? issueMountKit.mount(face) : undefined;
349
+ // 披露行(18a-1' 多监听扩形适配:TCP 侧数组 join——多监听并存全披露;sock 恒在场,缺席位 'off' 兜底)
350
+ const tcpPart = info.tcp.length > 0 ? ` tcp=${info.tcp.map((spec) => `${spec.host}:${spec.port}`).join(',')}` : '';
351
+ writeErr(`daemon 就绪:sock=${info.socketPath ?? 'off'}${tcpPart}(SDK 协议版本头 x-sdk-protocol: 1)`);
352
+ if (env.BERRY_AGENT_SDK_TOKEN === undefined) {
353
+ writeErr(`daemon token(自动生成——本地调用方接入凭证,已进本日志不再复现):Bearer ${face.token}`);
354
+ }
355
+ // 人面披露两行(18a-3' 差异面⑤——daemon 披露通道 = daemon.log 即 stderr
356
+ // 重定向;人面监听位 = tcp 数组首位〔构建序人面先推——port 0 内核指派后
357
+ // 实口见 info〕);同面单 token——面 token 覆盖 /v1 与 webui 人面,不再复述值;
358
+ // 分档(批 19e):kit 缺席形诚实报 SDK 面形不虚报 Web 界面
359
+ if (webuiMount !== undefined) {
360
+ const face0 = info.tcp[0];
361
+ writeErr(`Web 界面已开面:http://${face0.host}:${face0.port}/`);
362
+ writeErr('访问令牌即 daemon token(同面单 token——见 daemon token 披露行)');
363
+ }
364
+ else if (options.flags.port !== undefined) {
365
+ const face0 = info.tcp[0];
366
+ writeErr(`HTTP 人面已开面(webui 件未装载):http://${face0.host}:${face0.port}/(/v1/* 程序调用面在场,/api/* 404)`);
367
+ }
368
+ // 常驻至优雅停:closer(face.stop + 清 pid)挂 runtime 六步退出序——drain
369
+ // 序 = 注册序:webui-server 先注册(先摘 backend/路由/流,再停面——避免停面
370
+ // 后信封扇出仍写入已关流)
371
+ let resolveExit;
372
+ const exited = new Promise((resolve) => {
373
+ resolveExit = resolve;
374
+ });
375
+ let settled = false;
376
+ runtime.registerCloser({
377
+ label: 'webui-server',
378
+ fn: () => webuiMount?.detach(), // 幂等(未挂载形 = no-op)
379
+ });
380
+ if (issueWebhook !== undefined) {
381
+ runtime.registerCloser({
382
+ // webhook 路由摘除先于面停(注册序)——面 stop 后路由已无消费面
383
+ label: 'issue-webhook',
384
+ fn: () => issueWebhook.dispose(),
385
+ });
386
+ }
387
+ runtime.registerCloser({
388
+ label: 'sdk-http-face',
389
+ fn: async () => {
390
+ if (settled)
391
+ return;
392
+ settled = true;
393
+ sdkKit.pluginRoutes?.detachFace(); // U5-2 收口对称:受理账回 pending 态
394
+ await face.stop();
395
+ clearDaemonFootprints(paths); // 优雅停自清(猝死残留归 stop 幂等清扫)
396
+ resolveExit(0);
397
+ },
398
+ });
399
+ return exited;
400
+ }
401
+ /**
402
+ * spawner:spawn 自镜像 daemon child 后即退(脱离控制终端——POSIX setsid 族)。
403
+ *
404
+ * 编舞:win32 诚实拒(07 §5——随 TUI 支持矩阵 v1 不背书同裁)→ 开面判定
405
+ * 预拦(非回环 × 无 token 退 2——免无谓 spawn)→ stderr 追加重定向 daemon.log
406
+ * → spawn detached child(env 注 CHILD 标记防递归;argv 原样传 serve 旗标)
407
+ * → 起活确认窗(SPAWN_CONFIRM_TIMEOUT_MS)轮询 pid 登记:pid 现 = 起活成功
408
+ * 退 0;child 先退 = 转发其退出码 + 转述 daemon.log 尾行;窗尽未现 = 退 1
409
+ * 报确认超窗(罕见——组装超 10s)。
410
+ */
411
+ export async function spawnDaemonServe(options) {
412
+ const writeErr = options.writeErr ?? ((line) => defaultStderr.write(`${line}\n`));
413
+ const platform = options.platform ?? process.platform;
414
+ if (platform === 'win32') {
415
+ writeErr('daemon 形态暂不背书 win32(07 §5——随 TUI 支持矩阵 v1 同裁)');
416
+ return 1;
417
+ }
418
+ const dataDir = options.dataDir ?? resolveDataDir();
419
+ const paths = daemonPaths(dataDir);
420
+ const env = options.env ?? process.env;
421
+ // 开面判定预拦(child 侧 judgeListenConfig 同参兜底——fail-closed 双侧;
422
+ // daemonListenConfig 真源共用——人面 + SDK 面多监听同参)
423
+ const judged = judgeListenConfig(daemonListenConfig(paths, options.flags, env));
424
+ if (!judged.ok) {
425
+ writeErr(`拒启:${judged.reason}`);
426
+ return 2;
427
+ }
428
+ const self = options.self ?? { execPath: process.execPath, mainPath: process.argv[1] ?? 'dist/host/main.js' };
429
+ const args = [
430
+ self.mainPath,
431
+ 'serve',
432
+ '--daemon',
433
+ ...(options.flags.port !== undefined ? ['--port', String(options.flags.port)] : []),
434
+ ...(options.flags.sdkPort !== undefined ? ['--sdk-port', String(options.flags.sdkPort)] : []),
435
+ ...(options.flags.sdkHost !== undefined ? ['--sdk-host', options.flags.sdkHost] : []),
436
+ ...(options.flags.noDelta ? ['--no-delta'] : []),
437
+ ...(options.flags.debug ? ['--debug'] : []),
438
+ ];
439
+ // spawn 面注入:logFd 的开落在缺省实现内(测试注假 spawn 不触真 fs——
440
+ // serve/ 目录建与日志开同属产码缺省路径)
441
+ const spawnFn = options.spawnFn ??
442
+ ((cmd, argv, opts) => {
443
+ mkdirSync(paths.dir, { recursive: true }); // 三足迹先备其二——pid 由 child 写
444
+ const logFd = openSync(opts.logPath, 'a'); // stderr 追加重定向(07 §5)
445
+ return spawn(cmd, argv, {
446
+ detached: opts.detached,
447
+ stdio: ['ignore', 'ignore', logFd], // stdin/stdout ignore——daemon 与 stdio 互斥
448
+ env: opts.env,
449
+ });
450
+ });
451
+ const child = spawnFn(self.execPath, args, {
452
+ detached: true,
453
+ logPath: paths.logPath,
454
+ env: { ...env, [DAEMON_CHILD_ENV]: '1' },
455
+ });
456
+ child.unref(); // spawner 即退——child 生命周期独立(setsid 脱离会话)
457
+ // 起活确认窗:pid 登记现 + 探活 = 成功;child 先退 = 转发退码 + log 尾行
458
+ const isAlive = options.isAlive ?? defaultIsAlive;
459
+ const now = options.now ?? (() => Date.now());
460
+ const sleep = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
461
+ let childExit; // undefined = 未退
462
+ if (child.pid !== undefined) {
463
+ child.once('exit', (code) => {
464
+ childExit = code;
465
+ });
466
+ }
467
+ else {
468
+ childExit = child.exitCode; // spawn 即败(ENOENT 等)——假句柄测试形态
469
+ }
470
+ const deadline = now() + SPAWN_CONFIRM_TIMEOUT_MS;
471
+ for (;;) {
472
+ const probe = probeDaemon(paths, isAlive, options.fs);
473
+ if (probe.running) {
474
+ writeErr(`daemon 已起(pid ${probe.record?.pid})——pid/sock 登记 ${paths.dir};日志 ${paths.logPath}`);
475
+ return 0;
476
+ }
477
+ if (childExit !== undefined) {
478
+ // child 秒死——转述 log 尾行(BUSY 等失败信息在 child stderr → log)
479
+ writeErr(`daemon 启动即退(码 ${childExit})——daemon.log 尾行:`);
480
+ writeErr(tailLines(paths.logPath, options.fs));
481
+ return childExit ?? 1;
482
+ }
483
+ if (now() >= deadline) {
484
+ writeErr(`daemon 起活确认超窗(${SPAWN_CONFIRM_TIMEOUT_MS}ms 内 pid 登记未现)——查 ${paths.logPath}`);
485
+ return 1;
486
+ }
487
+ await sleep(100);
488
+ }
489
+ }
490
+ /** 读日志尾行(child 秒死转述——最多 3 行诊断面) */
491
+ function tailLines(logPath, fs) {
492
+ const f = { ...DEFAULT_FS, ...fs };
493
+ try {
494
+ const text = f.readFile(logPath);
495
+ const lines = text.trimEnd().split('\n');
496
+ return lines.slice(-3).join(' | ');
497
+ }
498
+ catch {
499
+ return '(日志不可读)';
500
+ }
501
+ }