dsh-plugin-local-infer 0.1.2 → 0.1.4

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 (251) hide show
  1. package/README.md +32 -6
  2. package/cordis.patch.yml +7 -0
  3. package/docs/zh/README.md +24 -4
  4. package/dsh/client.js +4200 -4049
  5. package/package.json +33 -11
  6. package/packages/core/lib/adapter.d.ts +177 -11
  7. package/packages/core/lib/adapter.js +74 -1
  8. package/packages/core/lib/config.d.ts +1 -1
  9. package/packages/core/lib/config.js +1 -1
  10. package/packages/core/lib/debug.d.ts +1 -1
  11. package/packages/core/lib/debug.js +1 -1
  12. package/packages/core/lib/defaults.d.ts +1 -1
  13. package/packages/core/lib/defaults.js +1 -1
  14. package/packages/core/lib/deps.d.ts +48 -20
  15. package/packages/core/lib/deps.js +24 -8
  16. package/packages/core/lib/estimator.d.ts +1 -1
  17. package/packages/core/lib/estimator.js +1 -1
  18. package/packages/core/lib/guideline.d.ts +1 -1
  19. package/packages/core/lib/guideline.js +1 -1
  20. package/packages/core/lib/hardware.d.ts +1 -1
  21. package/packages/core/lib/hardware.js +1 -1
  22. package/packages/core/lib/hf-cache.d.ts +70 -0
  23. package/packages/core/lib/hf-cache.js +83 -0
  24. package/packages/core/lib/index.d.ts +16 -9
  25. package/packages/core/lib/index.js +10 -6
  26. package/packages/core/lib/infer-cache.d.ts +39 -0
  27. package/packages/core/lib/infer-cache.js +53 -0
  28. package/packages/core/lib/launch-command.d.ts +8 -4
  29. package/packages/core/lib/launch-command.js +13 -6
  30. package/packages/core/lib/launch-record.d.ts +1 -1
  31. package/packages/core/lib/launch-record.js +1 -1
  32. package/packages/core/lib/logs.d.ts +8 -1
  33. package/packages/core/lib/logs.js +12 -4
  34. package/packages/core/lib/models.d.ts +120 -14
  35. package/packages/core/lib/models.js +209 -17
  36. package/packages/core/lib/parameter.d.ts +31 -3
  37. package/packages/core/lib/parameter.js +1 -1
  38. package/packages/core/lib/progress.d.ts +54 -0
  39. package/packages/core/lib/progress.js +120 -0
  40. package/packages/core/lib/registrar.d.ts +25 -14
  41. package/packages/core/lib/registrar.js +30 -10
  42. package/packages/core/lib/reserve.d.ts +11 -3
  43. package/packages/core/lib/reserve.js +6 -4
  44. package/packages/core/lib/status.d.ts +12 -6
  45. package/packages/core/lib/status.js +21 -10
  46. package/packages/core/lib/sudo.d.ts +80 -0
  47. package/packages/core/lib/sudo.js +78 -0
  48. package/packages/core/package.json +1 -1
  49. package/packages/orchestrator/lib/config.d.ts +63 -0
  50. package/packages/orchestrator/lib/config.js +111 -0
  51. package/packages/orchestrator/lib/index.d.ts +51 -0
  52. package/packages/orchestrator/lib/index.js +73 -0
  53. package/packages/orchestrator/lib/route.d.ts +60 -0
  54. package/packages/orchestrator/lib/route.js +159 -0
  55. package/packages/orchestrator/locale/en.json +6 -0
  56. package/packages/orchestrator/package.json +39 -0
  57. package/packages/orchestrator/prompt.md +87 -0
  58. package/packages/providers/lib/acquire.d.ts +95 -15
  59. package/packages/providers/lib/acquire.js +239 -40
  60. package/packages/providers/lib/adapters.d.ts +155 -221
  61. package/packages/providers/lib/adapters.js +589 -935
  62. package/packages/providers/lib/debug.d.ts +1 -1
  63. package/packages/providers/lib/debug.js +1 -1
  64. package/packages/providers/lib/deps.d.ts +31 -212
  65. package/packages/providers/lib/deps.js +71 -205
  66. package/packages/providers/lib/engines/basalt/basalt.d.ts +130 -0
  67. package/packages/providers/lib/engines/basalt/basalt.js +304 -0
  68. package/packages/providers/lib/engines/basalt/basaltAdapter.d.ts +85 -0
  69. package/packages/providers/lib/engines/basalt/basaltAdapter.js +439 -0
  70. package/packages/providers/lib/engines/basalt/basaltLinux.d.ts +23 -0
  71. package/packages/providers/lib/engines/basalt/basaltLinux.js +47 -0
  72. package/packages/providers/lib/engines/basalt/basaltMacOS.d.ts +17 -0
  73. package/packages/providers/lib/engines/basalt/basaltMacOS.js +19 -0
  74. package/packages/providers/lib/engines/basalt/basaltWindows.d.ts +24 -0
  75. package/packages/providers/lib/engines/basalt/basaltWindows.js +49 -0
  76. package/packages/providers/lib/engines/engineOs.d.ts +24 -0
  77. package/packages/providers/lib/engines/engineOs.js +17 -0
  78. package/packages/providers/lib/{hyperqwen.d.ts → engines/hyperqwen/hyperqwen.d.ts} +2 -2
  79. package/packages/providers/lib/{hyperqwen.js → engines/hyperqwen/hyperqwen.js} +14 -2
  80. package/packages/providers/lib/engines/hyperqwen/hyperqwenAdapter.d.ts +38 -0
  81. package/packages/providers/lib/engines/hyperqwen/hyperqwenAdapter.js +115 -0
  82. package/packages/providers/lib/engines/hyperqwen/hyperqwenArch.d.ts +9 -0
  83. package/packages/providers/lib/engines/hyperqwen/hyperqwenArch.js +9 -0
  84. package/packages/providers/lib/engines/hyperqwen/hyperqwenDebian.d.ts +9 -0
  85. package/packages/providers/lib/engines/hyperqwen/hyperqwenDebian.js +9 -0
  86. package/packages/providers/lib/engines/hyperqwen/hyperqwenFedora.d.ts +9 -0
  87. package/packages/providers/lib/engines/hyperqwen/hyperqwenFedora.js +9 -0
  88. package/packages/providers/lib/engines/hyperqwen/hyperqwenLinux.d.ts +10 -0
  89. package/packages/providers/lib/engines/hyperqwen/hyperqwenLinux.js +14 -0
  90. package/packages/providers/lib/engines/hyperqwen/hyperqwenMacOS.d.ts +9 -0
  91. package/packages/providers/lib/engines/hyperqwen/hyperqwenMacOS.js +9 -0
  92. package/packages/providers/lib/engines/hyperqwen/hyperqwenWindows.d.ts +9 -0
  93. package/packages/providers/lib/engines/hyperqwen/hyperqwenWindows.js +9 -0
  94. package/packages/providers/lib/{infernix.d.ts → engines/infernix/infernix.d.ts} +7 -4
  95. package/packages/providers/lib/{infernix.js → engines/infernix/infernix.js} +53 -8
  96. package/packages/providers/lib/engines/infernix/infernixAdapter.d.ts +52 -0
  97. package/packages/providers/lib/engines/infernix/infernixAdapter.js +235 -0
  98. package/packages/providers/lib/engines/infernix/infernixArch.d.ts +9 -0
  99. package/packages/providers/lib/engines/infernix/infernixArch.js +9 -0
  100. package/packages/providers/lib/engines/infernix/infernixDebian.d.ts +9 -0
  101. package/packages/providers/lib/engines/infernix/infernixDebian.js +9 -0
  102. package/packages/providers/lib/engines/infernix/infernixFedora.d.ts +9 -0
  103. package/packages/providers/lib/engines/infernix/infernixFedora.js +9 -0
  104. package/packages/providers/lib/engines/infernix/infernixLinux.d.ts +17 -0
  105. package/packages/providers/lib/engines/infernix/infernixLinux.js +35 -0
  106. package/packages/providers/lib/engines/infernix/infernixMacOS.d.ts +16 -0
  107. package/packages/providers/lib/engines/infernix/infernixMacOS.js +18 -0
  108. package/packages/providers/lib/engines/infernix/infernixWindows.d.ts +16 -0
  109. package/packages/providers/lib/engines/infernix/infernixWindows.js +34 -0
  110. package/packages/providers/lib/{llamacpp.d.ts → engines/llamacpp/llamacpp.d.ts} +2 -2
  111. package/packages/providers/lib/{llamacpp.js → engines/llamacpp/llamacpp.js} +12 -2
  112. package/packages/providers/lib/engines/llamacpp/llamacppAdapter.d.ts +50 -0
  113. package/packages/providers/lib/engines/llamacpp/llamacppAdapter.js +274 -0
  114. package/packages/providers/lib/engines/llamacpp/llamacppArch.d.ts +9 -0
  115. package/packages/providers/lib/engines/llamacpp/llamacppArch.js +9 -0
  116. package/packages/providers/lib/engines/llamacpp/llamacppDebian.d.ts +9 -0
  117. package/packages/providers/lib/engines/llamacpp/llamacppDebian.js +9 -0
  118. package/packages/providers/lib/engines/llamacpp/llamacppFedora.d.ts +9 -0
  119. package/packages/providers/lib/engines/llamacpp/llamacppFedora.js +9 -0
  120. package/packages/providers/lib/engines/llamacpp/llamacppLinux.d.ts +17 -0
  121. package/packages/providers/lib/engines/llamacpp/llamacppLinux.js +28 -0
  122. package/packages/providers/lib/engines/llamacpp/llamacppMacOS.d.ts +15 -0
  123. package/packages/providers/lib/engines/llamacpp/llamacppMacOS.js +17 -0
  124. package/packages/providers/lib/engines/llamacpp/llamacppWindows.d.ts +15 -0
  125. package/packages/providers/lib/engines/llamacpp/llamacppWindows.js +26 -0
  126. package/packages/providers/lib/{ninfer.d.ts → engines/ninfer/ninfer.d.ts} +2 -2
  127. package/packages/providers/lib/{ninfer.js → engines/ninfer/ninfer.js} +25 -3
  128. package/packages/providers/lib/engines/ninfer/ninferAdapter.d.ts +81 -0
  129. package/packages/providers/lib/engines/ninfer/ninferAdapter.js +392 -0
  130. package/packages/providers/lib/engines/ninfer/ninferArch.d.ts +9 -0
  131. package/packages/providers/lib/engines/ninfer/ninferArch.js +9 -0
  132. package/packages/providers/lib/engines/ninfer/ninferDebian.d.ts +9 -0
  133. package/packages/providers/lib/engines/ninfer/ninferDebian.js +9 -0
  134. package/packages/providers/lib/engines/ninfer/ninferFedora.d.ts +9 -0
  135. package/packages/providers/lib/engines/ninfer/ninferFedora.js +9 -0
  136. package/packages/providers/lib/engines/ninfer/ninferLinux.d.ts +17 -0
  137. package/packages/providers/lib/engines/ninfer/ninferLinux.js +37 -0
  138. package/packages/providers/lib/engines/ninfer/ninferMacOS.d.ts +15 -0
  139. package/packages/providers/lib/engines/ninfer/ninferMacOS.js +17 -0
  140. package/packages/providers/lib/engines/ninfer/ninferWindows.d.ts +29 -0
  141. package/packages/providers/lib/engines/ninfer/ninferWindows.js +59 -0
  142. package/packages/providers/lib/engines/strata/strata.d.ts +1 -0
  143. package/packages/providers/lib/engines/strata/strata.js +264 -0
  144. package/packages/providers/lib/engines/strata/strataAdapter.d.ts +97 -0
  145. package/packages/providers/lib/engines/strata/strataAdapter.js +463 -0
  146. package/packages/providers/lib/engines/strata/strataArch.d.ts +9 -0
  147. package/packages/providers/lib/engines/strata/strataArch.js +9 -0
  148. package/packages/providers/lib/engines/strata/strataDebian.d.ts +9 -0
  149. package/packages/providers/lib/engines/strata/strataDebian.js +9 -0
  150. package/packages/providers/lib/engines/strata/strataFedora.d.ts +9 -0
  151. package/packages/providers/lib/engines/strata/strataFedora.js +9 -0
  152. package/packages/providers/lib/engines/strata/strataLinux.d.ts +21 -0
  153. package/packages/providers/lib/engines/strata/strataLinux.js +30 -0
  154. package/packages/providers/lib/engines/strata/strataMacOS.d.ts +16 -0
  155. package/packages/providers/lib/engines/strata/strataMacOS.js +17 -0
  156. package/packages/providers/lib/engines/strata/strataWindows.d.ts +27 -0
  157. package/packages/providers/lib/engines/strata/strataWindows.js +37 -0
  158. package/packages/providers/lib/engines.d.ts +8 -5
  159. package/packages/providers/lib/engines.js +22 -11
  160. package/packages/providers/lib/guidelines.d.ts +5 -4
  161. package/packages/providers/lib/guidelines.js +18 -5
  162. package/packages/providers/lib/index.d.ts +33 -24
  163. package/packages/providers/lib/index.js +46 -24
  164. package/packages/providers/lib/model-defaults.d.ts +10 -5
  165. package/packages/providers/lib/model-defaults.js +47 -5
  166. package/packages/providers/lib/platform.d.ts +8 -5
  167. package/packages/providers/lib/platform.js +27 -7
  168. package/packages/providers/lib/presets.d.ts +6 -4
  169. package/packages/providers/lib/presets.js +53 -5
  170. package/packages/providers/lib/systemDeps.d.ts +313 -0
  171. package/packages/providers/lib/systemDeps.js +284 -0
  172. package/packages/providers/lib/vram.d.ts +34 -8
  173. package/packages/providers/lib/vram.js +81 -8
  174. package/packages/providers/package.json +2 -2
  175. package/packages/service/lib/index.d.ts +187 -23
  176. package/packages/service/lib/index.js +667 -90
  177. package/packages/service/lib/launch-target.d.ts +19 -0
  178. package/packages/service/lib/launch-target.js +28 -0
  179. package/packages/service/lib/run-logs.d.ts +72 -0
  180. package/packages/service/lib/run-logs.js +125 -0
  181. package/packages/service/lib/types.d.ts +166 -8
  182. package/packages/service/lib/types.js +1 -1
  183. package/packages/service/package.json +2 -2
  184. package/packages/tool/lib/index.d.ts +1 -1
  185. package/packages/tool/lib/index.js +1 -1
  186. package/packages/tool/package.json +2 -2
  187. package/packages/ui/lib/config.d.ts +89 -0
  188. package/packages/ui/lib/config.js +251 -0
  189. package/packages/ui/lib/deps.d.ts +4 -4
  190. package/packages/ui/lib/deps.js +1 -1
  191. package/packages/ui/lib/form.d.ts +1 -1
  192. package/packages/ui/lib/form.js +1 -1
  193. package/packages/ui/lib/index.d.ts +7 -1
  194. package/packages/ui/lib/index.js +12 -2
  195. package/packages/ui/lib/meter.d.ts +1 -1
  196. package/packages/ui/lib/meter.js +1 -1
  197. package/packages/ui/lib/ops/engine.d.ts +216 -0
  198. package/packages/ui/lib/ops/engine.js +697 -0
  199. package/packages/ui/lib/ops/get-steps.d.ts +98 -0
  200. package/packages/ui/lib/ops/get-steps.js +359 -0
  201. package/packages/ui/lib/ops/hf.d.ts +42 -0
  202. package/packages/ui/lib/ops/hf.js +150 -0
  203. package/packages/ui/lib/ops/logs.d.ts +78 -0
  204. package/packages/ui/lib/ops/logs.js +119 -0
  205. package/packages/ui/lib/ops/model.d.ts +71 -0
  206. package/packages/ui/lib/ops/model.js +123 -0
  207. package/packages/ui/lib/request.d.ts +106 -0
  208. package/packages/ui/lib/request.js +80 -0
  209. package/packages/ui/lib/route.d.ts +106 -422
  210. package/packages/ui/lib/route.js +136 -1845
  211. package/packages/ui/lib/routes/chat-template.d.ts +1 -0
  212. package/packages/ui/lib/routes/chat-template.js +1 -0
  213. package/packages/ui/lib/routes/deps.d.ts +1 -0
  214. package/packages/ui/lib/routes/deps.js +1 -0
  215. package/packages/ui/lib/routes/download.d.ts +1 -0
  216. package/packages/ui/lib/routes/download.js +1 -0
  217. package/packages/ui/lib/routes/get.d.ts +1 -0
  218. package/packages/ui/lib/routes/get.js +106 -0
  219. package/packages/ui/lib/routes/install.d.ts +16 -0
  220. package/packages/ui/lib/routes/install.js +36 -0
  221. package/packages/ui/lib/routes/launch.d.ts +24 -0
  222. package/packages/ui/lib/routes/launch.js +74 -0
  223. package/packages/ui/lib/routes/logs.d.ts +57 -0
  224. package/packages/ui/lib/routes/logs.js +117 -0
  225. package/packages/ui/lib/routes/provider-delete.d.ts +15 -0
  226. package/packages/ui/lib/routes/provider-delete.js +37 -0
  227. package/packages/ui/lib/routes/register.d.ts +16 -0
  228. package/packages/ui/lib/routes/register.js +28 -0
  229. package/packages/ui/lib/routes/save.d.ts +18 -0
  230. package/packages/ui/lib/routes/save.js +159 -0
  231. package/packages/ui/lib/routes/stop.d.ts +19 -0
  232. package/packages/ui/lib/routes/stop.js +32 -0
  233. package/packages/ui/lib/routes/stream.d.ts +40 -0
  234. package/packages/ui/lib/routes/stream.js +68 -0
  235. package/packages/ui/lib/routes/summary.d.ts +15 -0
  236. package/packages/ui/lib/routes/summary.js +26 -0
  237. package/packages/ui/lib/routes/unregister.d.ts +14 -0
  238. package/packages/ui/lib/routes/unregister.js +26 -0
  239. package/packages/ui/lib/viewmodels/deps.d.ts +1 -0
  240. package/packages/ui/lib/viewmodels/deps.js +1 -0
  241. package/packages/ui/lib/viewmodels/depsHelpers.d.ts +1 -0
  242. package/packages/ui/lib/viewmodels/depsHelpers.js +94 -0
  243. package/packages/ui/lib/viewmodels/summary.d.ts +145 -0
  244. package/packages/ui/lib/viewmodels/summary.js +245 -0
  245. package/packages/ui/lib/viewmodels/summaryHelpers.d.ts +30 -0
  246. package/packages/ui/lib/viewmodels/summaryHelpers.js +42 -0
  247. package/packages/ui/lib/warnings.d.ts +1 -1
  248. package/packages/ui/lib/warnings.js +1 -1
  249. package/packages/ui/package.json +4 -4
  250. package/packages/providers/lib/strata.d.ts +0 -15
  251. package/packages/providers/lib/strata.js +0 -14
@@ -0,0 +1,697 @@
1
+ /**
2
+ * The engine lifecycle operations: launchEngine runs the shared Get-steps
3
+ * routine (idempotent) and then renders the plan the registered adapter
4
+ * produces and runs the service's launch entry point (deploy, then the health
5
+ * poll), preserving the Get-step logs in the persisted last-run logs;
6
+ * stopEngine stops the exact container or process the launch started (a
7
+ * re-adopted engine through the record's pid), and a failed stop persists a
8
+ * note to the last-run logs and the log file; getEngine is a thin wrapper
9
+ * over the shared Get-steps routine (deps, git sync, build, model download,
10
+ * chat-template download). The launch record (the engine pid, start time,
11
+ * launch arguments, and the DSH process pid that launched it) is what a DSH
12
+ * restart re-links a surviving engine to its config:
13
+ * relinkStoredProviders re-adopts surviving engines on mount and per summary
14
+ * read, handleEngineDeath settles the stored state the moment an engine dies
15
+ * outside the stop path, and persistLaunchLogs stores the captured output.
16
+ * The platform-config registry sync keeps the service registry in step with
17
+ * the stored config.
18
+ * @module dsh-plugin-local-infer/ui/ops/engine
19
+ */
20
+ import { SUDO_CLI_NOTE, formatLaunchCommand, launchRecordFromPlan, } from '../../../core/lib/index.js';
21
+ import { findLaunchTarget, runLogKey } from '../../../service/lib/index.js';
22
+ import { nodeLifecycleHost } from '../../../providers/lib/index.js';
23
+ import { readConfig, writeStored } from "../config.js";
24
+ import { CANCELED_LINE, LAUNCH_RUN_SEED, STOP_RUN_SEED, capRunLogs, persistProcessLog, writeRunFile, } from "./logs.js";
25
+ import { resolveEngineDirectory, runGetSteps } from "./get-steps.js";
26
+ import { RouteError } from "../route.js";
27
+ /**
28
+ * CANCELED_LINE names the shared "Canceled" run line; it lives with the other
29
+ * run-line constants in ops/logs.ts and is re-exported here for the launch
30
+ * persisters' import surface.
31
+ */
32
+ export { CANCELED_LINE } from "./logs.js";
33
+ /**
34
+ * GET_EVENT_PREFIX leads a Get run's event lines in the streamed and
35
+ * persisted run (the route's `get failed` line and getEngine's sudo CLI
36
+ * note), so both read alike and the `!!` marker has one home (routes/get.ts
37
+ * and getEngine both build it from here).
38
+ */
39
+ export const GET_EVENT_PREFIX = '!! ';
40
+ /**
41
+ * resolveAdapter picks the registered engine adapter an engine id routes to. A
42
+ * card that names an engine no adapter serves gets a 404, not a blank panel.
43
+ */
44
+ export function resolveAdapter(localInfer, engineId) {
45
+ const adapter = localInfer.registeredEngines().find((engine) => engine.id === engineId);
46
+ if (adapter === undefined) {
47
+ throw new RouteError(404, `no engine adapter is registered for "${engineId}"`);
48
+ }
49
+ return adapter;
50
+ }
51
+ /**
52
+ * launchCommandNote renders the plan's child-process launch line for the
53
+ * card's Logs section: the core formatter's command under a [local-infer]
54
+ * launch prefix, newline-terminated. The optional autoRebuild is the
55
+ * effective deploy flag (the syncFailed override included), so a compose
56
+ * plan's note carries the same pull policy the deploy's up command runs and
57
+ * the printed line stays the exact string the host executed. A plan that
58
+ * names no command (a bare docker plan with no argv) has no note, and the
59
+ * run logs stay as they are.
60
+ */
61
+ function launchCommandNote(plan, autoRebuild) {
62
+ const command = formatLaunchCommand(plan, autoRebuild);
63
+ if (command === undefined)
64
+ return undefined;
65
+ return `[local-infer] launch: ${command}\n`;
66
+ }
67
+ /**
68
+ * applyComposeArgs seeds a compose plan's compose arguments from the stored
69
+ * platform config (the card's docker-tag field for a compose engine, e.g.
70
+ * `--profile single`), so the up line the deploy runs and the down line the
71
+ * stop runs both select the profile the user named. A compose plan with no
72
+ * stored docker-tag keeps the engine's default profile; a non-compose plan is
73
+ * untouched.
74
+ */
75
+ function applyComposeArgs(plan, localInfer, engineId) {
76
+ if (plan.kind !== 'compose')
77
+ return;
78
+ const platform = localInfer.enginePlatformConfig(engineId);
79
+ if (platform?.dockerTag !== undefined) {
80
+ plan.composeArgs = platform.dockerTag;
81
+ }
82
+ }
83
+ /**
84
+ * persistStoredLogs overwrites the stored last-run logs (the card's Logs
85
+ * section shows them after a reload) and leaves every other stored field as
86
+ * the launch reads it.
87
+ */
88
+ function persistStoredLogs(logs) {
89
+ const stored = readConfig();
90
+ writeStored(stored, { lastLogs: capRunLogs(logs) });
91
+ }
92
+ /**
93
+ * resolveStoredRequest resolves a route request's engine and config against
94
+ * the stored config: the request's fields win, and an absent field falls back
95
+ * to the stored value, the same rule every route applies. The Get route
96
+ * reads it for its run-log key before delegating to getEngine.
97
+ */
98
+ export function resolveStoredRequest(request) {
99
+ const stored = readConfig();
100
+ return {
101
+ stored,
102
+ engineId: request.engine ?? stored.engine,
103
+ config: request.config ?? stored.config,
104
+ };
105
+ }
106
+ /**
107
+ * registerLaunchPlatform registers the request's platform override (the
108
+ * card's unsaved Engine Directory / Engine Fork edits) with the service
109
+ * registry, and returns the effective platform entry the Get steps and the
110
+ * deploy step read. A request with no Engine Directory override falls back
111
+ * to the engine's default checkout home (`~/.cache/dsh/infer/{engineId}`),
112
+ * so the deploy step resolves a relative launch path (the NInfer Windows
113
+ * build tree) against a real checkout; the fallback is registered in the
114
+ * service registry only (memory, never the stored platform map), and a
115
+ * stored entry that already names a directory is left as-is. An absent
116
+ * request platform keeps the stored entry, which the save route and route
117
+ * registration already synced.
118
+ */
119
+ function registerLaunchPlatform(localInfer, request, stored, engineId) {
120
+ if (request.platform !== undefined) {
121
+ setPlatformConfig(localInfer, engineId, request.platform);
122
+ }
123
+ const platform = request.platform ?? stored.platform[engineId];
124
+ // resolveEngineDirectory is the one home of the override-or-default rule,
125
+ // so the registration decision compares against its answer instead of
126
+ // restating the blank check: a platform that already names the effective
127
+ // directory needs no registration.
128
+ const engineDirectory = resolveEngineDirectory(engineId, platform);
129
+ if (platform?.engineDirectory !== engineDirectory) {
130
+ setPlatformConfig(localInfer, engineId, { ...platform, engineDirectory });
131
+ }
132
+ return platform;
133
+ }
134
+ /**
135
+ * getStepsRequest builds the shared Get-steps request both getEngine and
136
+ * launchEngine run: the engine and config, the platform entry the checkout
137
+ * resolves against, the Play/Run trust gate, and the lifecycle host plus the
138
+ * optional cancel signal and download-progress sink. The exactOptionalProperty-
139
+ * types-safe spreads keep an absent field from writing `undefined`, so the
140
+ * host, signal, and sink travel as one group built in one place. Each caller
141
+ * layers its own extras on the result: the launch adds autoRebuild, the step
142
+ * reporter, and the sync override, a standalone Get adds none (its control
143
+ * keeps its throbber, so no reporter rides).
144
+ */
145
+ function getStepsRequest(engineId, config, stored, platform, host, signal, onChunk) {
146
+ return {
147
+ engineId,
148
+ config,
149
+ ...(platform !== undefined ? { platform } : {}),
150
+ allowRunDeps: stored.allowRunDeps,
151
+ host,
152
+ ...(signal !== undefined ? { signal } : {}),
153
+ ...(onChunk !== undefined ? { onChunk } : {}),
154
+ };
155
+ }
156
+ /**
157
+ * registerSudoObserver registers the run's sudo hook on the service under
158
+ * the run's key (the hub's own keying) and returns the disposer that removes
159
+ * only this hook: concurrent runs each keep their own slot, a re-launch
160
+ * under the same key takes the slot from the older run, and the first settle
161
+ * never clears another run's note routing. The service hands every guarded
162
+ * announcement to every registered hook (the shared CLI password wait can
163
+ * block any run, and the announce seam does not name the triggering run), so
164
+ * one wait notes every run in flight. launchEngine and getEngine share the
165
+ * helper so the sudo-note rule has one home: the hook appends noteLine to
166
+ * the run's own sink (the note rides the streamed run and the run text
167
+ * persisted when the step settles) and through persistProcessLog (so a run
168
+ * still hung on the prompt reads the note in the Logs section and the log
169
+ * file before it settles), then calls onSudo, the route's stream event; a
170
+ * Get passes none because its control keeps its throbber. The caller invokes
171
+ * the disposer in a finally so a failed run leaves no hook behind.
172
+ */
173
+ export function registerSudoObserver(localInfer, runKey, noteLine, noteTo, onSudo) {
174
+ return localInfer.registerSudoObserver(runKey, () => {
175
+ noteTo(noteLine);
176
+ persistProcessLog(noteLine.replace(/\n$/, ''));
177
+ onSudo?.();
178
+ });
179
+ }
180
+ /**
181
+ * openRunLog begins the run under runKey (the seed resets the key's buffer,
182
+ * so a mid-run re-launch replaces the previous run's text) and hands back the
183
+ * bracket. The caller closes it in a finally so every settle path clears the
184
+ * running flag.
185
+ */
186
+ function openRunLog(localInfer, runKey, seed) {
187
+ localInfer.beginRun(runKey, seed);
188
+ let text = '';
189
+ return {
190
+ append(chunk) {
191
+ text += chunk;
192
+ localInfer.appendRunLog(runKey, chunk);
193
+ },
194
+ text() {
195
+ return text;
196
+ },
197
+ end() {
198
+ localInfer.endRun(runKey);
199
+ },
200
+ };
201
+ }
202
+ /**
203
+ * launchEngine runs the shared Get-steps routine (idempotent: deps, git sync,
204
+ * build, model download, chat-template download, each skipped when already
205
+ * satisfied) and then renders the plan the registered adapter produces for
206
+ * the config and the detected hardware, and runs the service's launch entry
207
+ * point (deploy, then the health poll). A git sync of an existing checkout
208
+ * that fails under the continueOnSyncFailure override (always on for a
209
+ * launch) keeps the Get run going with the checkout's current version and
210
+ * reports syncFailed on the result; the launch then threads autoRebuild:
211
+ * false into the deploy step, which skips its own git sync (it would hit the
212
+ * same rebase conflict) and launches the current version, while the Get
213
+ * error line persists in the last-run logs and the log file. The detail the
214
+ * launch returns names the broken step when the engine does not come up.
215
+ * The launch line (the
216
+ * engine binary and its arguments) leads the engine output in the run logs
217
+ * the card's Logs section and the log file carry, and the Get-step output the
218
+ * launch ran leads it, so a broken setup diagnoses against the exact steps
219
+ * and child process the launch started, and the Get logs are preserved, not
220
+ * wiped, by the launch's persistence. The launch step persists the
221
+ * launch record (the engine pid, start time, launch arguments, and the DSH
222
+ * process pid that launched it) on the request's provider entry, so a DSH
223
+ * restart can re-link a surviving engine to its config. The optional signal
224
+ * is the card's cancel: an abort interrupts the in-flight Get-step download
225
+ * child, and the persisted run carries a "Canceled" line. The optional
226
+ * onChunk is the hf download's raw stderr progress sink: the download runner
227
+ * hands each streamed chunk to it as it lands, so a launch that runs the Get
228
+ * steps streams the download progress the same way a Get does. The optional
229
+ * onSudo is the route's sudo-event writer: while the run is in flight, the
230
+ * service's sudo observer answers a guarded command waiting for a password on
231
+ * the launching CLI by landing the core CLI note in the run logs and the log
232
+ * file and calling onSudo; the observer is unregistered when the run settles,
233
+ * failed or not, so a failing run leaves none behind. The whole run brackets
234
+ * one live hub run under the runLogKey rule (the "--- Launching..." seed,
235
+ * the Get-steps sink, the command note, the deploy and engine output at
236
+ * settle, end in a finally), so the streaming logs route reads the launch.
237
+ */
238
+ export async function launchEngine(localInfer, request, onProgress, host = nodeLifecycleHost, signal, onChunk, onSudo) {
239
+ const { stored, engineId, config } = resolveStoredRequest(request);
240
+ const adapter = resolveAdapter(localInfer, engineId);
241
+ // The run key this launch's live buffer rides, the shared runLogKey rule
242
+ // the streaming logs route resolves the same way. The seed resets the
243
+ // key's buffer, so a mid-run re-launch replaces the previous run's text.
244
+ const runKey = runLogKey(stored.providers, engineId, request.providerId, config);
245
+ // The run's live-buffer bracket (see openRunLog): the Get-step sink and the
246
+ // sudo note append through it (one chunk, two readers: the persisted run
247
+ // text and the live hub), and the finally closes it on every settle path.
248
+ const run = openRunLog(localInfer, runKey, LAUNCH_RUN_SEED);
249
+ // The run owns the sudo observer, not the route, so the note lands in the
250
+ // persisted run and in the stream from one place (see
251
+ // registerSudoObserver): the CLI note appends to the Get-step log sink
252
+ // (composeLaunchLogs and persistLaunchOutcome carry it into the last-run
253
+ // logs and the log file), persists to disk while the run is still hung
254
+ // waiting on the prompt, and tells the route's stream writer.
255
+ const clearSudoObserver = registerSudoObserver(localInfer, runKey, `${SUDO_CLI_NOTE}\n`, run.append, onSudo);
256
+ try {
257
+ const hardware = await localInfer.detectHardware();
258
+ const platform = registerLaunchPlatform(localInfer, request, stored, engineId);
259
+ // The Get steps (deps, git sync, build, model download, chat-template
260
+ // download) run idempotently before the launch plan renders, so a launch
261
+ // never reads a missing checkout, binary, model, or template. The Auto
262
+ // Rebuild checkbox off skips the sync and build steps, the same rule the
263
+ // adapter's deploy step applies. The launch always sets
264
+ // continueOnSyncFailure: a failed git sync of an existing checkout
265
+ // reports syncFailed on the result (the run keeps going with the
266
+ // checkout's current version) instead of halting the launch. The
267
+ // progress sink rides into the steps as onStep: each step announces its
268
+ // phase as its work actually starts (the prereqs announcement belongs to
269
+ // the dep install, not to the launch's start), so the card's button
270
+ // tracks the phase actually running.
271
+ const get = await runGetSteps(localInfer, {
272
+ ...getStepsRequest(engineId, config, stored, platform, host, signal, onChunk),
273
+ ...(request.autoRebuild !== undefined ? { autoRebuild: request.autoRebuild } : {}),
274
+ ...(onProgress !== undefined ? { onStep: onProgress } : {}),
275
+ continueOnSyncFailure: true,
276
+ }, run.append);
277
+ const plan = adapter.renderLaunch(config, hardware);
278
+ if (!get.ok) {
279
+ if (signal?.aborted === true) {
280
+ localInfer.appendRunLog(runKey, CANCELED_LINE);
281
+ }
282
+ return settleGetFailure(plan, get, engineId, config, request.providerId, run.text(), signal?.aborted === true);
283
+ }
284
+ applyComposeArgs(plan, localInfer, engineId);
285
+ // The effective auto-rebuild flag for the deploy step. A Get sync that
286
+ // failed under the override threads autoRebuild: false into the deploy,
287
+ // which skips its own git sync (it would hit the same rebase conflict)
288
+ // and launches the checkout's current version; a Get run whose sync
289
+ // passed keeps the request's autoRebuild. It is computed before the
290
+ // command note so the persisted Logs line renders the same pull policy
291
+ // the deploy's compose up runs, and the deploy reads the flag to decide
292
+ // whether a rebuild's missing-binary symlink into /usr/local/bin runs
293
+ // (on) or is written to the card's Logs section (off), and whether its
294
+ // acquisition steps (git sync, image pull, installer) run at all.
295
+ const launchAutoRebuild = get.syncFailed === true ? false : request.autoRebuild;
296
+ const commandNote = launchCommandNote(plan, launchAutoRebuild);
297
+ if (commandNote !== undefined) {
298
+ // Persist before the deploy starts: a slow or hung launch keeps the Get
299
+ // steps and the command readable in the Logs section across a card
300
+ // reload, where the engine output has not landed yet.
301
+ persistStoredLogs(`${run.text()}${commandNote}`);
302
+ localInfer.appendRunLog(runKey, commandNote);
303
+ }
304
+ // The trust gate and the auto-rebuild flag ride into the deploy step.
305
+ // onLog is the deploy's run-log sink: the deploy's mutating commands
306
+ // (image pull, model download) write their `$ <command>` lines through
307
+ // it before their children spawn, so the live launch run reads each
308
+ // command ahead of its output.
309
+ const result = await localInfer.launch(plan, {
310
+ ...(onProgress !== undefined ? { onProgress } : {}),
311
+ allowRunDeps: stored.allowRunDeps,
312
+ ...(launchAutoRebuild !== undefined ? { autoRebuild: launchAutoRebuild } : {}),
313
+ onLog: run.append,
314
+ });
315
+ // The deploy step's build output and the engine's runtime output reach
316
+ // the live buffer at settle (they only exist once the launch returns;
317
+ // the Get-step sink and the command note already streamed through the
318
+ // run bracket). composeLaunchLogs then folds every part into the
319
+ // persisted order (Get steps, build, command, engine), where the hub
320
+ // carries them in arrival order (Get steps, command, build, engine).
321
+ const deployLogs = result.buildLogs ?? '';
322
+ const engineLogs = result.logs ?? '';
323
+ if (deployLogs + engineLogs !== '') {
324
+ localInfer.appendRunLog(runKey, deployLogs + engineLogs);
325
+ }
326
+ // A cancel that landed after the Get steps (the card's provider-form Cancel
327
+ // closed the stream during the deploy or health poll) adds the "Canceled"
328
+ // line, so the persisted run reads as canceled.
329
+ const canceled = signal?.aborted === true;
330
+ composeLaunchLogs(result, run.text(), commandNote, canceled);
331
+ if (canceled) {
332
+ localInfer.appendRunLog(runKey, CANCELED_LINE);
333
+ }
334
+ // The result's logs field already carries the Get-step output via
335
+ // composeLaunchLogs, so the persisted run leads with no runText. Passing
336
+ // runText again would include the Get steps twice in the persisted run.
337
+ persistLaunchOutcome(result, engineId, config, request.providerId, '');
338
+ return result;
339
+ }
340
+ finally {
341
+ clearSudoObserver();
342
+ // The bracket closes on every settle path: healthy, failed, and canceled
343
+ // all clear the key's running flag.
344
+ run.end();
345
+ }
346
+ }
347
+ /**
348
+ * composeLaunchLogs orders a launch's run logs: the Get-step output the
349
+ * launch ran leads, the deploy step's build output follows it, the launch
350
+ * command note follows that (after a successful build, or a direct launch
351
+ * with no build), and the engine's runtime output comes last. The deploy
352
+ * step's buildLogs field is folded into the ordered text and dropped, so the
353
+ * result carries the ordered run in its logs field. A cancel that landed
354
+ * after the Get steps appends the "Canceled" line, so the persisted run reads
355
+ * as canceled.
356
+ */
357
+ function composeLaunchLogs(result, runText, commandNote, canceled) {
358
+ const buildLogs = result.buildLogs ?? '';
359
+ const engineLogs = result.logs ?? '';
360
+ result.logs = runText + buildLogs + (commandNote ?? '') + engineLogs;
361
+ if (result.buildLogs !== undefined) {
362
+ delete result.buildLogs;
363
+ }
364
+ if (canceled) {
365
+ result.logs += CANCELED_LINE;
366
+ }
367
+ }
368
+ /**
369
+ * persistLaunchOutcome persists a settled launch: the run logs to the stored
370
+ * last-run logs and the log file, and the launch record on the target
371
+ * provider entry, so a DSH restart can re-link a surviving engine to its
372
+ * config. The runText argument leads the persisted run only when the result
373
+ * carries no logs of its own (the get-failed path, where the deploy never
374
+ * ran). The success path passes an empty string, because composeLaunchLogs
375
+ * already folded the Get-step output into the result's logs, and leading it
376
+ * again would include the Get steps twice.
377
+ */
378
+ function persistLaunchOutcome(result, engineId, config, providerId, runText) {
379
+ persistLaunchLogs(result, engineId, config, runText);
380
+ persistLaunchRecord(result, engineId, config, providerId);
381
+ }
382
+ /**
383
+ * settleGetFailure settles a launch whose Get steps failed before the deploy
384
+ * starts: it builds the unhealthy result (naming the broken step), persists
385
+ * the run (the Get-step output leads, and a cancel that landed adds the
386
+ * "Canceled" line, so the run reads as canceled), and returns the result so
387
+ * the route answers with it.
388
+ */
389
+ function settleGetFailure(plan, get, engineId, config, providerId, runText, canceled) {
390
+ const result = {
391
+ plan,
392
+ healthy: false,
393
+ detail: get.detail ?? 'a Get step failed',
394
+ };
395
+ persistLaunchOutcome(result, engineId, config, providerId, canceled ? runText + CANCELED_LINE : runText);
396
+ return result;
397
+ }
398
+ /**
399
+ * withoutLaunchRecord returns a provider entry with its launch record (the
400
+ * engine pid and the previous-pid list) removed, so the record is gone from
401
+ * the file, not just ignored, and a recycled pid cannot re-link to a config
402
+ * whose engine is stopped or dead. The stop, death, and re-link paths share
403
+ * it, so the rebuilt entry shape lives in one place.
404
+ */
405
+ function withoutLaunchRecord(entry) {
406
+ return { id: entry.id, engine: entry.engine, config: entry.config, platform: entry.platform };
407
+ }
408
+ /**
409
+ * persistLaunchRecord stores a launch's record on the target provider entry:
410
+ * the engine pid the deploy step started, the start time, the plan's argv,
411
+ * the DSH process pid the launch ran under (only when it identified as DSH),
412
+ * and the previous-pid list the re-link rule prices (the entry's own list
413
+ * plus the current DSH pid, capped by the core builder). A launch whose
414
+ * deploy started no tracked process (a compose up) persists nothing, and a
415
+ * launch with no resolvable entry persists nothing.
416
+ */
417
+ function persistLaunchRecord(result, engineId, config, providerId) {
418
+ if (result.pid === undefined)
419
+ return;
420
+ const stored = readConfig();
421
+ const target = findLaunchTarget(stored.providers, engineId, providerId, config);
422
+ if (target === undefined)
423
+ return;
424
+ const record = launchRecordFromPlan(result.plan, result.pid, result.parentPid, target.launch?.previousPids ?? []);
425
+ const providers = stored.providers.map((entry) => entry.id === target.id ? { ...entry, launch: record } : entry);
426
+ writeStored(stored, { providers });
427
+ }
428
+ /**
429
+ * clearLaunchRecord removes the target provider entry's launch record (the
430
+ * engine pid and the previous-pid list with it), so a stopped engine's pid
431
+ * cannot be re-linked to its config by a recycled pid. A stop with no
432
+ * resolvable entry, or an entry that carries no record, is a no-op.
433
+ */
434
+ function clearLaunchRecord(engineId, config, providerId) {
435
+ const stored = readConfig();
436
+ const target = findLaunchTarget(stored.providers, engineId, providerId, config);
437
+ if (target === undefined || target.launch === undefined)
438
+ return;
439
+ const providers = stored.providers.map((entry) => entry.id === target.id ? withoutLaunchRecord(entry) : entry);
440
+ writeStored(stored, { providers });
441
+ }
442
+ /**
443
+ * handleEngineDeath persists a provider entry's state when its engine process
444
+ * dies outside the plugin's stop path: a crash, a pkill of a plugin-launched
445
+ * child seen through the service's exit watch, or the poll-detected exit of a
446
+ * process adopted after a DSH restart. The entry whose launch record names the
447
+ * dead pid (or the dead engine-and-port, when the report carries no pid match)
448
+ * has its record cleared, so a restart never re-links a dead pid, and the exit
449
+ * line lands in the card's last-run logs beside the engine's own output. An
450
+ * event that matches no entry carrying a record (a stop the route already
451
+ * cleared, an engine the plugin never tracked) is a no-op, which keeps an
452
+ * expected stop quiet. With a run-log hub (the route's observer closure
453
+ * captures the service), the same note line also lands in the dead entry's
454
+ * live buffer: the snapshot is read first, and if the run was NOT already
455
+ * running, the append's lazy reopen is settled with endRun right after, so a
456
+ * late note streams without stranding the key as running.
457
+ */
458
+ export function handleEngineDeath(event, runLogs) {
459
+ const stored = readConfig();
460
+ let matched;
461
+ const providers = stored.providers.map((entry) => {
462
+ const record = entry.launch;
463
+ if (record === undefined)
464
+ return entry;
465
+ const byPid = event.pid !== undefined && record.pid === event.pid;
466
+ const byEnginePort = event.port !== undefined && record.engineId === entry.engine && record.port === event.port;
467
+ if (!byPid && !byEnginePort)
468
+ return entry;
469
+ matched = matched ?? entry;
470
+ return withoutLaunchRecord(entry);
471
+ });
472
+ if (matched === undefined)
473
+ return;
474
+ const signalNote = event.signal !== null && event.signal !== undefined
475
+ ? ` on signal ${event.signal}`
476
+ : ` with exit code ${event.code}`;
477
+ const note = `[local-infer] engine ${event.engineId}` +
478
+ `${event.port !== undefined ? ` (port ${event.port})` : ''}` +
479
+ ` pid ${event.pid ?? 'unknown'} exited${signalNote}` +
480
+ `${event.detail !== undefined ? `: ${event.detail}` : ''}\n`;
481
+ // The note persists through the shared fold (persistProcessLog: the
482
+ // join + capRunLogs tail every appender shares, the per-run log file
483
+ // included) instead of a bespoke lastLogs tail-join; the entry clear rides
484
+ // its own write before it.
485
+ writeStored(stored, { providers });
486
+ persistProcessLog(note);
487
+ if (runLogs === undefined)
488
+ return;
489
+ // The death's run key follows the runLogKey rule over the matched entry
490
+ // (the same resolution the launch wrote the buffer under: the entry id
491
+ // when its config names the model, else the engine id).
492
+ const runKey = runLogKey(stored.providers, event.engineId, undefined, matched.config);
493
+ const wasRunning = runLogs.readRunLog(runKey).running;
494
+ runLogs.appendRunLog(runKey, note);
495
+ if (!wasRunning) {
496
+ runLogs.endRun(runKey);
497
+ }
498
+ }
499
+ /**
500
+ * persistLaunchLogs stores a launch's captured output as the last run and
501
+ * writes it to the configured log file. The Get-step output the launch ran
502
+ * (the optional runText) leads the persisted run, so the Logs section and the
503
+ * log file show the Get steps followed by the launch, not the launch wiping
504
+ * the Get logs. The stored last-run logs ride the shared capRunLogs tail rule
505
+ * (ops/logs.ts), the same cap the append paths and the live hub apply. The
506
+ * log file is only written when the run produced output and the log location
507
+ * is set; the last-run logs are always persisted (an empty string clears a
508
+ * stale run) so the card's Logs section shows the previous run after a
509
+ * reload.
510
+ */
511
+ export function persistLaunchLogs(result, engineId, config, runText = '') {
512
+ const stored = readConfig();
513
+ // The "Launching..." separator leads the persisted run, so the Logs
514
+ // section and the log file read the launch as a fresh process section, the
515
+ // same line the launch's live hub run seeds with (LAUNCH_RUN_SEED).
516
+ const body = runText + (result.logs === undefined ? '' : result.logs);
517
+ const logs = body === '' ? '' : `${LAUNCH_RUN_SEED}${body}`;
518
+ writeStored(stored, { lastLogs: capRunLogs(logs) });
519
+ if (logs === '') {
520
+ return;
521
+ }
522
+ const modelId = typeof config.model === 'string' ? config.model : undefined;
523
+ writeRunFile(stored, logs, engineId, modelId);
524
+ }
525
+ /**
526
+ * stopEngine renders the same plan and runs the service's stop entry point, so
527
+ * the stop step targets the exact container or process the launch started. The
528
+ * stop then clears the target entry's launch record (the engine pid and the
529
+ * previous-pid list with it), so a recycled pid cannot re-link an engine the
530
+ * user just stopped. An engine a DSH restart re-adopted carries no tracked
531
+ * child handle in the adapter, so the stop stamps the record's pid on the
532
+ * plan and the adapter signals it through the host; a stop that fails
533
+ * persists a note (the engine, the model, and the host's message) to the
534
+ * stored last-run logs and the log file, so the card's Logs section names the
535
+ * broken step, and re-throws, so the route answers 500 and the card's stop
536
+ * deadline settles the entry to the red Logs state.
537
+ */
538
+ export async function stopEngine(localInfer, request) {
539
+ const { stored, engineId, config } = resolveStoredRequest(request);
540
+ const adapter = resolveAdapter(localInfer, engineId);
541
+ const hardware = await localInfer.detectHardware();
542
+ const plan = adapter.renderLaunch(config, hardware);
543
+ applyComposeArgs(plan, localInfer, engineId);
544
+ // The re-adopted engine's pid: without it the adapter's stop is a no-op for
545
+ // a process it did not spawn, and the engine the user just clicked Stop on
546
+ // keeps running.
547
+ const target = findLaunchTarget(stored.providers, engineId, request.providerId, config);
548
+ if (target?.launch?.pid !== undefined) {
549
+ plan.pid = target.launch.pid;
550
+ }
551
+ const runKey = runLogKey(stored.providers, engineId, request.providerId, config);
552
+ localInfer.beginRun(runKey, STOP_RUN_SEED);
553
+ try {
554
+ // The stop's log sink is the live run buffer: the adapter writes the
555
+ // `$ <command>` line of every command it runs (compose down, docker stop,
556
+ // a pid signal) through it before the child runs, so the stop's live run
557
+ // reads the command ahead of any note.
558
+ await localInfer.stop(plan, (line) => localInfer.appendRunLog(runKey, line));
559
+ clearLaunchRecord(engineId, config, request.providerId);
560
+ }
561
+ catch (error) {
562
+ persistStopFailure(engineId, config, error, localInfer, runKey);
563
+ throw error;
564
+ }
565
+ finally {
566
+ localInfer.endRun(runKey);
567
+ }
568
+ }
569
+ /**
570
+ * persistStopFailure appends a stop's failure note to the stored last-run logs
571
+ * (the card's Logs section shows it after the summary re-fetch) and writes it
572
+ * to the configured log file, the same per-run file rule the launch step
573
+ * applies, and to the stop's live run-log buffer under runKey (one note text,
574
+ * no third copy). A host that threw without a message reads as "unknown".
575
+ */
576
+ function persistStopFailure(engineId, config, error, localInfer, runKey) {
577
+ const detail = error instanceof Error ? error.message : String(error);
578
+ const modelId = typeof config.model === 'string' ? config.model : undefined;
579
+ const note = `[local-infer] stop failed for ${engineId}` +
580
+ `${modelId !== undefined ? ` (${modelId})` : ''}: ${detail}\n`;
581
+ persistProcessLog(note);
582
+ localInfer.appendRunLog(runKey, note);
583
+ }
584
+ /**
585
+ * relinkStoredProviders prices every stored provider launch record against the
586
+ * live process table through the service's re-link rule and returns the
587
+ * provider list the card should read: an adopted engine is re-marked plugin-
588
+ * launched and tied to its model by the service, and a liveness watcher starts
589
+ * on its port so a later pkill settles the stored state through the death
590
+ * observer without another summary read; a dead engine's record is cleared on
591
+ * disk here (the previous-pid list with it), so a recycled pid cannot re-link
592
+ * to a config whose engine is down. The route calls it on mount and the
593
+ * summary calls it per read, making re-attach independent of the card opening.
594
+ */
595
+ export async function relinkStoredProviders(localInfer, stored) {
596
+ const recorded = stored.providers.filter((provider) => provider.launch !== undefined);
597
+ if (recorded.length === 0)
598
+ return stored.providers;
599
+ const outcomes = await localInfer.reLinkProviders(recorded.map((provider) => ({ id: provider.id, record: provider.launch })));
600
+ if (!recorded.some((provider) => outcomes[provider.id] === 'dead')) {
601
+ return stored.providers;
602
+ }
603
+ const providers = stored.providers.map((provider) => provider.launch !== undefined && outcomes[provider.id] === 'dead'
604
+ ? withoutLaunchRecord(provider)
605
+ : provider);
606
+ writeStored(stored, { providers });
607
+ return providers;
608
+ }
609
+ /**
610
+ * getEngine is the Get step: a thin wrapper over the shared Get-steps routine
611
+ * (the system dependencies, the git sync, the build, the model download, and
612
+ * the chat-template download), streaming every step through log as it lands,
613
+ * so the card appends it to the Logs section live. The dependency install
614
+ * only runs while the Play/Run trust gate is on; with the gate off the deps
615
+ * are re-checked and a missing one aborts the step with the copy/paste note.
616
+ * The optional signal is the card's cancel: an abort interrupts the in-flight
617
+ * hf download child (SIGINT, then SIGTERM). Resume is within-invocation only:
618
+ * the hf CLI (huggingface_hub 2.x) resumes a network blip via HTTP Range inside
619
+ * one `hf download` run, but a killed run re-downloads from zero on the re-run
620
+ * (each run writes a process-unique `.incomplete` temp blob and unlinks it on
621
+ * failure), so an orphaned partial blob lingers until `hf cache prune`. The
622
+ * optional onChunk is the hf download's raw stderr progress sink: the
623
+ * download runner hands each streamed chunk to it as it lands, separate from
624
+ * the log sink, so the route parses the tqdm lines into progress events. The
625
+ * run registers the same sudo observer a launch does: a guarded command
626
+ * waiting for the user's password on the launching CLI writes the core CLI
627
+ * note through log as a GET_EVENT_PREFIX line, the same marker the route's
628
+ * own failure line uses, so the note rides the streamed run and the run text
629
+ * the route persists, and through persistProcessLog so a run still hung on
630
+ * the prompt reads it before it settles. The Get control keeps its throbber,
631
+ * so the observer announces nothing beyond the note line.
632
+ */
633
+ export async function getEngine(localInfer, request, log, host = nodeLifecycleHost, signal, onChunk) {
634
+ const { stored, engineId, config } = resolveStoredRequest(request);
635
+ // Register the request's platform override the same way launchEngine does,
636
+ // so the next launch reads the Engine Directory / Engine Fork the Get used.
637
+ const platform = registerLaunchPlatform(localInfer, request, stored, engineId);
638
+ // The Get run owns the sudo hook for the run's duration, registered under
639
+ // the same runLogKey rule the route brackets the live hub run with (see
640
+ // registerSudoObserver); the finally leaves only this run's slot cleared.
641
+ const clearSudoObserver = registerSudoObserver(localInfer, runLogKey(stored.providers, engineId, undefined, config), `${GET_EVENT_PREFIX}${SUDO_CLI_NOTE}\n`, log);
642
+ try {
643
+ const result = await runGetSteps(localInfer, getStepsRequest(engineId, config, stored, platform, host, signal, onChunk), log);
644
+ return { ok: result.ok, ...(result.detail !== undefined ? { detail: result.detail } : {}) };
645
+ }
646
+ finally {
647
+ clearSudoObserver();
648
+ }
649
+ }
650
+ /**
651
+ * The route's view of which engine platform configs are registered with the
652
+ * service, keyed by engine id. The service registry is the single source the
653
+ * launch step reads, so the route keeps it in sync with the stored config:
654
+ * on route registration, after every save, and when a launch request carries
655
+ * a platform override. A disposer is tracked per engine, so an engine whose
656
+ * stored config is cleared is unregistered rather than left stale.
657
+ */
658
+ const platformDisposers = new Map();
659
+ /**
660
+ * setPlatformConfig registers one engine's platform config with the service,
661
+ * replacing any stored entry. A config with no fields is read as "no platform
662
+ * config", which unregisters the engine so the deploy step falls back to the
663
+ * adapter's default source and cache directory.
664
+ */
665
+ function setPlatformConfig(localInfer, engineId, config) {
666
+ platformDisposers.get(engineId)?.();
667
+ platformDisposers.delete(engineId);
668
+ if (config === undefined) {
669
+ return;
670
+ }
671
+ if (config.engineDirectory === undefined &&
672
+ config.engineFork === undefined &&
673
+ config.buildCommands === undefined &&
674
+ config.dockerTag === undefined) {
675
+ return;
676
+ }
677
+ const platform = { engineId, ...config };
678
+ platformDisposers.set(engineId, localInfer.registerEnginePlatformConfig(platform));
679
+ }
680
+ /**
681
+ * syncPlatformConfigs re-syncs the service registry with the stored platform
682
+ * map: it registers (or re-registers) every engine that carries a config, and
683
+ * unregisters every engine that no longer does, so a launch always reads the
684
+ * platform config the user last saved.
685
+ */
686
+ export function syncPlatformConfigs(localInfer, stored) {
687
+ const wanted = stored.platform;
688
+ for (const engineId of [...platformDisposers.keys()]) {
689
+ if (wanted[engineId] === undefined) {
690
+ setPlatformConfig(localInfer, engineId, undefined);
691
+ }
692
+ }
693
+ for (const [engineId, config] of Object.entries(wanted)) {
694
+ setPlatformConfig(localInfer, engineId, config);
695
+ }
696
+ }
697
+ //# sourceMappingURL=engine.js.map