cozyclay 2.1.1 → 3.0.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 (585) hide show
  1. package/CHANGELOG.md +88 -0
  2. package/README.md +12 -30
  3. package/bin/agent/agent-routes.mjs +43 -116
  4. package/bin/agent/agent-runner.mjs +14 -15
  5. package/bin/agent/cliproxy-provider.mjs +135 -0
  6. package/bin/agent/credential-store.mjs +30 -7
  7. package/bin/agent/pi-tools.mjs +4 -1
  8. package/bin/agent/provider-keys.mjs +32 -0
  9. package/bin/agent/provider-login.mjs +110 -0
  10. package/bin/agent/provider-routes.mjs +99 -0
  11. package/bin/agent/providers.mjs +19 -97
  12. package/bin/agent/script-tool.mjs +17 -4
  13. package/bin/agent/session-store.mjs +2 -2
  14. package/bin/agent/studio-context-budget.mjs +93 -0
  15. package/bin/agent/studio-motion-reference.mjs +23 -0
  16. package/bin/agent/studio-prompt.mjs +1 -1
  17. package/bin/agent/studio-rebase-text.mjs +32 -0
  18. package/bin/agent/studio-rebase.mjs +26 -39
  19. package/bin/agent/studio-tools.mjs +9 -28
  20. package/bin/agent/tool-arguments.mjs +49 -0
  21. package/bin/agent/turn-frames.mjs +66 -0
  22. package/bin/codex-auth.mjs +9 -2
  23. package/bin/cozyclay.mjs +10 -116
  24. package/bin/live/cli.mjs +3 -3
  25. package/bin/mcp-runtime.mjs +2 -12
  26. package/bin/package-signature.mjs +0 -4
  27. package/dist/app/index.html +7 -6
  28. package/dist/assets/EXRLoader-CvVIvGVE.js +1 -0
  29. package/dist/assets/analytics-BWCqdgWA.js +1 -0
  30. package/dist/assets/app-DfTCxaKk.js +5087 -0
  31. package/dist/assets/app-f-fkV5uP.css +1 -0
  32. package/dist/assets/compile-worker-BxKXh07s.js +5 -0
  33. package/dist/assets/fflate.module-KbGj5-H6.js +5 -0
  34. package/dist/assets/{first-shot-handoff-msLI71g1.js → first-shot-handoff-DNO8kv_N.js} +1 -1
  35. package/dist/assets/{landing-ChUvB_zL.js → landing-yfZaJkWc.js} +1 -1
  36. package/dist/assets/scene-asset-sync-U3kS2rvK.js +20 -0
  37. package/dist/assets/scene-asset-sync-_S8NQl5B.css +1 -0
  38. package/dist/assets/workflow-D78eiKTa.js +179 -0
  39. package/dist/cozyclay-package.json +1 -1
  40. package/dist/index.html +3 -3
  41. package/dist/sw.js +3 -6
  42. package/dist/workflow/index.html +5 -4
  43. package/mcp/LIVE-PROTOCOL.md +7 -47
  44. package/mcp/README.md +16 -26
  45. package/mcp/live-hub.mjs +6 -10
  46. package/mcp/package.json +1 -3
  47. package/mcp/tool-handlers.mjs +4 -168
  48. package/package.json +3 -22
  49. package/src/App.jsx +151 -2625
  50. package/src/agent-progress.js +80 -0
  51. package/src/agent-turn-mark.js +30 -0
  52. package/src/analytics.js +1 -19
  53. package/src/app-context.js +12 -2
  54. package/src/app-stage.jsx +2 -34
  55. package/src/asset-pane.jsx +6 -5
  56. package/src/camera-block.js +21 -0
  57. package/src/camera-direction.js +120 -0
  58. package/src/character-kind.js +3 -9
  59. package/src/command-bus.js +10 -8
  60. package/src/commands/cast.js +5 -34
  61. package/src/commands/elements/character.js +1 -1
  62. package/src/commands/elements/motion.js +1 -6
  63. package/src/commands/elements/object.js +6 -2
  64. package/src/commands/elements/shot.js +4 -1
  65. package/src/commands/elements.js +38 -14
  66. package/src/commands/index.js +3 -1
  67. package/src/commands/kits.js +37 -0
  68. package/src/commands/motion-timing.js +57 -0
  69. package/src/commands/motion.js +9 -152
  70. package/src/commands/motion3-compile.js +65 -0
  71. package/src/commands/motion3.js +214 -0
  72. package/src/commands/objects.js +6 -4
  73. package/src/commands/shot.js +55 -13
  74. package/src/commands/stage.js +14 -1
  75. package/src/commands/view.js +4 -1
  76. package/src/document-mirrors.js +42 -0
  77. package/src/document-store.js +4 -0
  78. package/src/domains/cast.js +19 -194
  79. package/src/domains/motion/motion-range-pin-rules.js +1 -1
  80. package/src/domains/motion/take-source.js +53 -0
  81. package/src/domains/motion.js +60 -1838
  82. package/src/domains/scenes.js +12 -55
  83. package/src/domains/shots.js +2 -2
  84. package/src/domains/stage.js +4 -2
  85. package/src/draft-ghost.js +47 -0
  86. package/src/draft-ghost.jsx +129 -0
  87. package/src/dualview.jsx +4 -3
  88. package/src/effects/EffectsLayer.jsx +112 -0
  89. package/src/effects/effect-meshes.js +232 -0
  90. package/src/effects/effect-record.js +103 -0
  91. package/src/effects/emitter.js +59 -0
  92. package/src/effects/simulate.js +261 -0
  93. package/src/effects/state.js +59 -0
  94. package/src/execution-telemetry.js +2 -3
  95. package/src/first-success-guide.jsx +5 -5
  96. package/src/ghost-set.js +257 -0
  97. package/src/held-object-edit.jsx +113 -0
  98. package/src/held-object-pose.js +175 -0
  99. package/src/hierarchy-model.js +1 -1
  100. package/src/hierarchy-panel.jsx +1 -1
  101. package/src/imported-mesh.jsx +103 -0
  102. package/src/kits/held.js +84 -0
  103. package/src/kits/index.js +70 -0
  104. package/src/kits/smoking.js +57 -0
  105. package/src/light-blockers.js +93 -0
  106. package/src/motion/beat-board-model.js +84 -0
  107. package/src/motion/beat-board.css +124 -0
  108. package/src/motion/beat-board.jsx +60 -0
  109. package/src/motion/center-of-mass.js +66 -0
  110. package/src/motion/fix-collisions.js +850 -0
  111. package/src/motion/physics-review.js +118 -0
  112. package/src/{ardy → motion}/physics-support.js +1 -76
  113. package/src/motion/rig-snapshot.js +22 -0
  114. package/src/motion/take-segments.jsx +140 -0
  115. package/src/{ardy → motion}/timeline-coordinates.js +0 -21
  116. package/src/{ardy → motion}/timeline.css +1 -2
  117. package/src/{ardy → motion}/timeline.jsx +42 -413
  118. package/src/motion/waypoints.js +76 -0
  119. package/src/motion3/beats.js +83 -0
  120. package/src/motion3/body.js +158 -0
  121. package/src/motion3/bones.js +47 -0
  122. package/src/motion3/carried.js +47 -0
  123. package/src/motion3/compile-job.js +79 -0
  124. package/src/motion3/compile-worker.js +10 -0
  125. package/src/motion3/compiler/anchors.js +113 -0
  126. package/src/motion3/compiler/arm-orient.js +128 -0
  127. package/src/motion3/compiler/channels.js +125 -0
  128. package/src/motion3/compiler/compile.js +205 -0
  129. package/src/motion3/compiler/gait-rhythm.js +97 -0
  130. package/src/motion3/compiler/grip.js +171 -0
  131. package/src/motion3/compiler/head-steady.js +48 -0
  132. package/src/motion3/compiler/hold.js +134 -0
  133. package/src/motion3/compiler/idle.js +113 -0
  134. package/src/motion3/compiler/limbs.js +197 -0
  135. package/src/motion3/compiler/math.js +72 -0
  136. package/src/motion3/compiler/pose.js +177 -0
  137. package/src/motion3/compiler/quat.js +49 -0
  138. package/src/motion3/compiler/skeleton.js +74 -0
  139. package/src/motion3/compiler/validate-common.js +32 -0
  140. package/src/motion3/compiler/validate-goals.js +118 -0
  141. package/src/motion3/compiler/validate-objects.js +144 -0
  142. package/src/motion3/compiler/validate-parts.js +118 -0
  143. package/src/motion3/compiler/validate.js +122 -0
  144. package/src/motion3/compiler/vocab.js +74 -0
  145. package/src/motion3/compiler/walk-pelvis.js +275 -0
  146. package/src/motion3/compiler/walk.js +207 -0
  147. package/src/motion3/compiler/wrist-clamp.js +32 -0
  148. package/src/motion3/delta.js +47 -0
  149. package/src/motion3/describe.js +177 -0
  150. package/src/motion3/docs.js +27 -0
  151. package/src/motion3/edit-carry.js +21 -0
  152. package/src/motion3/fix/ease.js +16 -0
  153. package/src/motion3/fix/ground.js +37 -0
  154. package/src/motion3/fix/index.js +52 -0
  155. package/src/motion3/fix/push.js +71 -0
  156. package/src/motion3/fix/reach.js +46 -0
  157. package/src/motion3/fix/retime.js +79 -0
  158. package/src/motion3/fix/self.js +211 -0
  159. package/src/motion3/fix/slide.js +70 -0
  160. package/src/motion3/fix/turn.js +51 -0
  161. package/src/motion3/follow.js +39 -0
  162. package/src/motion3/free-motion.js +204 -0
  163. package/src/motion3/grip-choice.js +130 -0
  164. package/src/motion3/hold-plan.js +157 -0
  165. package/src/motion3/json-patch.js +77 -0
  166. package/src/motion3/key-aim.js +105 -0
  167. package/src/motion3/key-edit.js +82 -0
  168. package/src/motion3/lenient.js +127 -0
  169. package/src/motion3/object-keys.js +236 -0
  170. package/src/motion3/object-pose.js +182 -0
  171. package/src/motion3/object-tracks.js +12 -0
  172. package/src/motion3/people.js +82 -0
  173. package/src/motion3/pipeline.js +102 -0
  174. package/src/motion3/playback.js +51 -0
  175. package/src/motion3/probes.js +91 -0
  176. package/src/motion3/qa/body-checks.js +34 -0
  177. package/src/motion3/qa/check-names.js +14 -0
  178. package/src/motion3/qa/checks.js +125 -0
  179. package/src/motion3/qa/contact-checks.js +51 -0
  180. package/src/motion3/qa/explain.js +80 -0
  181. package/src/motion3/qa/fmt.js +5 -0
  182. package/src/motion3/qa/foot-checks.js +72 -0
  183. package/src/motion3/qa/geometry.js +77 -0
  184. package/src/motion3/qa/goals.js +191 -0
  185. package/src/motion3/qa/ground.js +60 -0
  186. package/src/motion3/qa/hints-contact.js +195 -0
  187. package/src/motion3/qa/hints-goal.js +160 -0
  188. package/src/motion3/qa/hints-knee.js +24 -0
  189. package/src/motion3/qa/hints-motion.js +209 -0
  190. package/src/motion3/qa/hints-object.js +91 -0
  191. package/src/motion3/qa/index.js +44 -0
  192. package/src/motion3/qa/key-checks.js +82 -0
  193. package/src/motion3/qa/near.js +96 -0
  194. package/src/motion3/qa/object-checks.js +135 -0
  195. package/src/motion3/qa/patch.js +161 -0
  196. package/src/motion3/qa/people-checks.js +37 -0
  197. package/src/motion3/qa/severity.js +38 -0
  198. package/src/motion3/qa/spec-checks.js +65 -0
  199. package/src/motion3/qa/style.js +228 -0
  200. package/src/motion3/qa/wrist-checks.js +69 -0
  201. package/src/motion3/qa/wrist-obs.js +60 -0
  202. package/src/motion3/refs.js +129 -0
  203. package/src/motion3/repair-tune.js +121 -0
  204. package/src/motion3/repair.js +83 -0
  205. package/src/motion3/report.js +67 -0
  206. package/src/motion3/rigs/index.js +14 -0
  207. package/src/motion3/rigs/x-bot-tpose-body.json +1 -0
  208. package/src/motion3/rigs/x-bot-tpose.json +1 -0
  209. package/src/motion3/rigs/y-bot-tpose-body.json +1 -0
  210. package/src/motion3/rigs/y-bot-tpose.json +1 -0
  211. package/src/motion3/scene.js +60 -0
  212. package/src/motion3/schema/brief.js +89 -0
  213. package/src/motion3/schema/fields.js +44 -0
  214. package/src/motion3/schema/spec-examples.js +46 -0
  215. package/src/motion3/schema/topics.js +113 -0
  216. package/src/motion3/schema/types.js +149 -0
  217. package/src/motion3/search.js +117 -0
  218. package/src/motion3/second-hand.js +112 -0
  219. package/src/motion3/settle.js +136 -0
  220. package/src/motion3/skills/basic.js +83 -0
  221. package/src/motion3/skills/book.js +76 -0
  222. package/src/motion3/skills/car.js +145 -0
  223. package/src/motion3/skills/flip.js +200 -0
  224. package/src/motion3/skills/frames-auto.js +29 -0
  225. package/src/motion3/skills/impact.js +109 -0
  226. package/src/motion3/skills/index.js +85 -0
  227. package/src/motion3/skills/info.js +80 -0
  228. package/src/motion3/skills/smoke-kit.js +109 -0
  229. package/src/motion3/skills/smoke.js +160 -0
  230. package/src/motion3/spec-store.js +59 -0
  231. package/src/motion3/take-edit.js +155 -0
  232. package/src/motion3/take.js +63 -0
  233. package/src/motion3/timeline.js +127 -0
  234. package/src/motion3/views.js +121 -0
  235. package/src/motion3/walk-plan.js +119 -0
  236. package/src/object-catalog.jsx +7 -1
  237. package/src/object-colors.js +98 -0
  238. package/src/object-gizmo-parts.js +115 -0
  239. package/src/object-gizmo.jsx +37 -122
  240. package/src/object-glow.js +139 -0
  241. package/src/object-hidden.js +33 -0
  242. package/src/object-look.js +196 -0
  243. package/src/object-size.js +10 -0
  244. package/src/object-travel.js +71 -6
  245. package/src/panels/CameraPanel.jsx +7 -7
  246. package/src/panels/HdrEnvironmentControls.jsx +59 -0
  247. package/src/panels/LightPanel.jsx +25 -4
  248. package/src/panels/ObjectLookPanel.jsx +88 -0
  249. package/src/panels/PosePanel.jsx +2 -17
  250. package/src/panels/RigControlPanel.jsx +5 -53
  251. package/src/panels/SubjectBox.jsx +4 -1
  252. package/src/panels/details.css +14 -0
  253. package/src/posestudio.jsx +4 -27
  254. package/src/project-browser.jsx +3 -4
  255. package/src/project-resources.js +18 -31
  256. package/src/project.js +13 -104
  257. package/src/props.jsx +25 -280
  258. package/src/qa-label.js +25 -0
  259. package/src/rail-dot-drag.js +23 -0
  260. package/src/render-passes.js +10 -0
  261. package/src/resource-status.jsx +1 -0
  262. package/src/result-modal.jsx +4 -19
  263. package/src/room.jsx +20 -12
  264. package/src/scene-assets.js +8 -6
  265. package/src/scene-light-place.js +72 -0
  266. package/src/scene-lights.jsx +148 -0
  267. package/src/scene-objects.js +31 -139
  268. package/src/scenes.js +15 -105
  269. package/src/semantic-edit.js +2 -5
  270. package/src/set-pieces.jsx +183 -0
  271. package/src/settings-menu.jsx +3 -11
  272. package/src/shell/BottomDock.jsx +9 -51
  273. package/src/shell/DetailsSlot.jsx +16 -200
  274. package/src/shell/GhostsMenuGroup.jsx +74 -0
  275. package/src/shell/LibrarySlot.jsx +1 -3
  276. package/src/shell/MenuBar.jsx +3 -3
  277. package/src/shell/PreferencesDialog.jsx +15 -264
  278. package/src/shell/PreferencesSlot.jsx +2 -8
  279. package/src/shell/TopBar.jsx +5 -84
  280. package/src/shell/ViewportToolbar.jsx +10 -70
  281. package/src/shell/agent-glass.css +19 -6
  282. package/src/shell/details-selection.js +42 -0
  283. package/src/shell/ghosts-menu.css +22 -0
  284. package/src/shell/glass-regions.css +29 -3
  285. package/src/shell/preferences.css +6 -1
  286. package/src/shot-authoring.js +4 -7
  287. package/src/soft-shadows.js +37 -0
  288. package/src/stage-attach-frames.js +12 -12
  289. package/src/stage-canvas-rigs.jsx +3 -3
  290. package/src/stage-capture.jsx +42 -8
  291. package/src/stage-characters.jsx +22 -216
  292. package/src/stage-editor-gizmos.jsx +6 -6
  293. package/src/stage-environment.js +93 -0
  294. package/src/stage-hdr-builtins.js +107 -0
  295. package/src/stage-hdr-image.js +116 -0
  296. package/src/stage-hdr-library.js +70 -0
  297. package/src/stage-hdr-map.js +94 -0
  298. package/src/stage-hdr.js +130 -0
  299. package/src/stage-lighting.js +149 -0
  300. package/src/stage-occlusion.js +210 -0
  301. package/src/stage-sky.jsx +159 -0
  302. package/src/stage-startup-defaults.js +5 -9
  303. package/src/studio-actions.js +16 -14
  304. package/src/studio-agent-commands.js +19 -16
  305. package/src/studio-agent-motion.js +9 -9
  306. package/src/studio-agent-protocol.js +47 -77
  307. package/src/studio-app-binding.js +27 -47
  308. package/src/studio-asset-list.js +17 -0
  309. package/src/studio-contact-sheet.js +37 -5
  310. package/src/studio-elements.js +27 -7
  311. package/src/studio-entity-bounds.js +18 -0
  312. package/src/studio-error-codes.js +15 -0
  313. package/src/studio-export-framing.js +152 -0
  314. package/src/studio-lighting-check.js +103 -0
  315. package/src/studio-motion-visual.js +150 -0
  316. package/src/studio-schema-errors.js +80 -0
  317. package/src/studio-support-gap.js +74 -0
  318. package/src/styles.css +1 -19
  319. package/src/timeline-defaults.js +5 -0
  320. package/src/timeline-extent.js +8 -20
  321. package/src/tutorial-walk.js +17 -0
  322. package/src/ui-scrub.js +1 -1
  323. package/src/use-shot-camera-tracks.js +12 -0
  324. package/src/view-ghosts.js +97 -0
  325. package/src/view-ghosts.jsx +49 -0
  326. package/src/workflow/AgentPanel.jsx +47 -166
  327. package/src/workflow/AgentProgressCard.jsx +109 -0
  328. package/src/workflow/AgentReplyText.jsx +37 -0
  329. package/src/workflow/AgentTurnSteps.jsx +84 -0
  330. package/src/workflow/LoginPanel.jsx +140 -0
  331. package/src/workflow/WorkflowBuilder.jsx +7 -23
  332. package/src/workflow/agent-client.js +54 -51
  333. package/src/workflow/agent-panel.css +71 -1
  334. package/src/workflow/agent-progress-card.css +167 -0
  335. package/src/workflow/agent-progress-view.js +47 -0
  336. package/src/workflow/agent-turn-steps.css +113 -0
  337. package/src/workflow/agent-turn.js +191 -0
  338. package/src/workflow/login-client.js +34 -0
  339. package/src/workflow/motion-input.js +18 -31
  340. package/src/workflow/node-schema.js +1 -1
  341. package/src/workflow/reply-markdown.js +69 -0
  342. package/src/workflow/scene-asset-sync.js +8 -8
  343. package/tools/agent-session-stats.mjs +61 -0
  344. package/tools/dev-full.mjs +5 -56
  345. package/tools/eslint-baseline.json +12 -13
  346. package/tools/knip-baseline.json +0 -179
  347. package/tools/loc-baseline.json +31 -81
  348. package/tools/motion3/extract-body.mjs +103 -0
  349. package/tools/motion3/extract-rig.mjs +85 -0
  350. package/tools/process-supervisor.mjs +0 -88
  351. package/tools/run-tests.mjs +9 -125
  352. package/bin/agent/motion-runtime.mjs +0 -76
  353. package/dist/ardy/cskel27-rest.json +0 -557
  354. package/dist/assets/analytics-C9pMpW7s.js +0 -1
  355. package/dist/assets/app-B_2XB9rw.css +0 -1
  356. package/dist/assets/app-DAuvKPP8.js +0 -4920
  357. package/dist/assets/config-Bx7pYsD9.js +0 -1
  358. package/dist/assets/config-DeoFTYzb.css +0 -1
  359. package/dist/assets/demo-CvKuBgy5.js +0 -1
  360. package/dist/assets/scene-asset-sync-DKkpjKBV.js +0 -19
  361. package/dist/assets/scene-asset-sync-DjJ_cJ2R.css +0 -1
  362. package/dist/assets/ticket-BPk2Ni95.js +0 -1
  363. package/dist/assets/workflow-jJRBR94Z.js +0 -179
  364. package/dist/demo/walk-then-stop.npz +0 -0
  365. package/mcp/ardy-prompts.mjs +0 -218
  366. package/src/ardy/auto-fix-panel.css +0 -108
  367. package/src/ardy/auto-physics.js +0 -1194
  368. package/src/ardy/client.js +0 -153
  369. package/src/ardy/convert.js +0 -313
  370. package/src/ardy/cskel27-neutral.js +0 -39
  371. package/src/ardy/cskel27.js +0 -77
  372. package/src/ardy/export.js +0 -165
  373. package/src/ardy/fix-collisions.js +0 -2257
  374. package/src/ardy/foot-lock.js +0 -230
  375. package/src/ardy/motion-calibration.js +0 -118
  376. package/src/ardy/motion-edit.js +0 -213
  377. package/src/ardy/motion-url.js +0 -55
  378. package/src/ardy/mp4-duration.js +0 -102
  379. package/src/ardy/npz.js +0 -595
  380. package/src/ardy/physics-panel.css +0 -21
  381. package/src/ardy/physics-panel.jsx +0 -89
  382. package/src/ardy/physics-refine.js +0 -111
  383. package/src/ardy/physics-review.js +0 -567
  384. package/src/ardy/physics-temporal.js +0 -54
  385. package/src/ardy/platform-fit-panel.css +0 -64
  386. package/src/ardy/platform-fit-panel.jsx +0 -41
  387. package/src/ardy/platform-fit.js +0 -326
  388. package/src/ardy/playback.js +0 -711
  389. package/src/ardy/pose-pin.js +0 -97
  390. package/src/ardy/prompt-clips.js +0 -30
  391. package/src/ardy/retime.js +0 -211
  392. package/src/ardy/root-drop.js +0 -369
  393. package/src/ardy/timeline-resize.js +0 -13
  394. package/src/ardy/to-cskel27.js +0 -225
  395. package/src/ardy/trim.js +0 -33
  396. package/src/ardy/waypoints.js +0 -369
  397. package/src/ardy-bridge-contract.js +0 -281
  398. package/src/domains/motion/motion-video-capture.js +0 -664
  399. package/src/i2v-motion-client.js +0 -77
  400. package/src/i2v-motion-studio.jsx +0 -180
  401. package/src/line-edit.js +0 -1348
  402. package/src/motion/generation.js +0 -105
  403. package/src/motion-readiness-ui.jsx +0 -67
  404. package/src/motion-readiness.js +0 -40
  405. package/src/motion-resources.js +0 -177
  406. package/src/motion-store.js +0 -65
  407. package/src/motion-trail.js +0 -601
  408. package/src/multimodel-ingest.js +0 -394
  409. package/src/panels/PromptBlocksPanel.jsx +0 -444
  410. package/src/panels/VideoCapturePanel.jsx +0 -181
  411. package/src/pose-extract/browser.js +0 -42
  412. package/src/pose-extract/detector.js +0 -100
  413. package/src/pose-extract/fit.js +0 -227
  414. package/src/pose-extract/image-frame.js +0 -51
  415. package/src/pose-extract/index.js +0 -58
  416. package/src/pose-extract/keyframes.js +0 -74
  417. package/src/pose-extract/landmarks.js +0 -162
  418. package/src/pose-extract/mirror.js +0 -153
  419. package/src/pose-extract/take.js +0 -131
  420. package/src/pose-extract/video-frames.js +0 -91
  421. package/src/take-recipe.js +0 -165
  422. package/src/trail-key-conflicts.js +0 -39
  423. package/src/trail-pick.js +0 -59
  424. package/src/use-i2v-capture-modal.js +0 -15
  425. package/tools/ardy/BRIDGE.md +0 -37
  426. package/tools/ardy/GVHMR-NPZ.md +0 -68
  427. package/tools/ardy/README.md +0 -124
  428. package/tools/ardy/__pycache__/cclay_gvhmr_worker.cpython-313.pyc +0 -0
  429. package/tools/ardy/artifacts.mjs +0 -34
  430. package/tools/ardy/bridge.mjs +0 -1732
  431. package/tools/ardy/bvh-cskel27.mjs +0 -1265
  432. package/tools/ardy/cclay_constrained_generate.py +0 -561
  433. package/tools/ardy/cclay_gvhmr_worker.py +0 -264
  434. package/tools/ardy/cclay_motion_edit.py +0 -510
  435. package/tools/ardy/cclay_sequence_generate.py +0 -705
  436. package/tools/ardy/cclay_worker.py +0 -304
  437. package/tools/ardy/dump-npz.py +0 -205
  438. package/tools/ardy/extract-rest.mjs +0 -299
  439. package/tools/ardy/extract.mjs +0 -507
  440. package/tools/ardy/footage.mjs +0 -467
  441. package/tools/ardy/gvhmr-floor.mjs +0 -102
  442. package/tools/ardy/gvhmr-to-cskel27.mjs +0 -21
  443. package/tools/ardy/gvhmr_fastpath.py +0 -168
  444. package/tools/ardy/gvhmr_trajectory.py +0 -240
  445. package/tools/ardy/merge-text-encoder.py +0 -160
  446. package/tools/ardy/mocap-metrics.mjs +0 -200
  447. package/tools/ardy/mocap-quality-gate.mjs +0 -110
  448. package/tools/ardy/motion-stabilize.mjs +0 -348
  449. package/tools/ardy/npz.mjs +0 -414
  450. package/tools/ardy/pose-to-npz.mjs +0 -106
  451. package/tools/ardy/prompt-limits.mjs +0 -5
  452. package/tools/ardy/run-edit-on-box.sh +0 -73
  453. package/tools/ardy/run-local.mjs +0 -233
  454. package/tools/ardy/run-on-box.sh +0 -593
  455. package/tools/ardy/run-sequence-on-box.sh +0 -177
  456. package/tools/ardy/runners/gvhmr-worker.mjs +0 -206
  457. package/tools/ardy/runners/index.mjs +0 -18
  458. package/tools/ardy/runners/local.mjs +0 -455
  459. package/tools/ardy/runners/proc.mjs +0 -125
  460. package/tools/ardy/runners/remote.mjs +0 -252
  461. package/tools/ardy/setup-local.mjs +0 -189
  462. package/tools/ardy/setup-text-encoder-on-box.sh +0 -93
  463. package/tools/ardy/setup-text-encoder.py +0 -306
  464. package/tools/ardy/smpl-cskel27.mjs +0 -178
  465. package/tools/ardy/visual-qa.mjs +0 -196
  466. package/tools/ardy/vq-car.mjs +0 -52
  467. package/tools/bench/EXP3.md +0 -61
  468. package/tools/bench/cclay_bench_extract_incam.py +0 -125
  469. package/tools/bench/cclay_bench_extract_obs.py +0 -204
  470. package/tools/bench/cclay_bench_runner.py +0 -42
  471. package/tools/bench/cube-contact.mjs +0 -162
  472. package/tools/bench/exp3.mjs +0 -152
  473. package/tools/bench/extract-bench-lib.mjs +0 -216
  474. package/tools/bench/extract-bench.mjs +0 -302
  475. package/tools/bench/fal-generate.mjs +0 -88
  476. package/tools/bench/fit/README.md +0 -189
  477. package/tools/bench/fit/camera.mjs +0 -59
  478. package/tools/bench/fit/contact.mjs +0 -419
  479. package/tools/bench/fit/footlock.mjs +0 -234
  480. package/tools/bench/fit/motion.mjs +0 -85
  481. package/tools/bench/fit/pin.mjs +0 -27
  482. package/tools/bench/fit/remote.mjs +0 -105
  483. package/tools/bench/fit-bench.mjs +0 -126
  484. package/tools/bench/fit-sanity.mjs +0 -45
  485. package/tools/bench/metrics.mjs +0 -313
  486. package/tools/bench/obs/depth.mjs +0 -258
  487. package/tools/bench/obs/extrinsics.mjs +0 -175
  488. package/tools/bench/obs/fit_mannequin_betas.py +0 -340
  489. package/tools/bench/obs/ground.mjs +0 -294
  490. package/tools/bench/obs/heading.mjs +0 -151
  491. package/tools/bench/obs/ladder.mjs +0 -555
  492. package/tools/bench/obs/mannequin-betas.json +0 -124
  493. package/tools/bench/obs/remote.mjs +0 -83
  494. package/tools/bench/obs/rest_joints.py +0 -52
  495. package/tools/bench/obs/ybot-targets.mjs +0 -49
  496. package/tools/bench/obs-bench.mjs +0 -520
  497. package/tools/bench/score.mjs +0 -343
  498. package/tools/bench/summarize.mjs +0 -86
  499. package/tools/demo-worker/README.md +0 -137
  500. package/tools/demo-worker/api-client.mjs +0 -233
  501. package/tools/demo-worker/generate.mjs +0 -301
  502. package/tools/demo-worker/index.mjs +0 -466
  503. package/tools/demo-worker/sign-core.mjs +0 -112
  504. package/tools/demo-worker/sign.mjs +0 -161
  505. package/tools/gt-render/browser.mjs +0 -182
  506. package/tools/gt-render/camera-math.mjs +0 -276
  507. package/tools/gt-render/page.mjs +0 -267
  508. package/tools/gt-render/render.mjs +0 -514
  509. package/tools/gt-render/scene-box.mjs +0 -31
  510. package/tools/gt-render/take-transform.mjs +0 -87
  511. package/tools/kimodo/compare-seam.mjs +0 -108
  512. package/tools/kimodo/constraints.mjs +0 -244
  513. package/tools/kimodo/edit.mjs +0 -259
  514. package/tools/kimodo/effector-constraints.mjs +0 -200
  515. package/tools/kimodo/generate.mjs +0 -646
  516. package/tools/kimodo/local-output.mjs +0 -265
  517. package/tools/kimodo/measure-pose.mjs +0 -65
  518. package/tools/kimodo/measure-preserve.mjs +0 -899
  519. package/tools/kimodo/measure-waypoints.mjs +0 -55
  520. package/tools/kimodo/mlx-generate.py +0 -111
  521. package/tools/kimodo/pose-constraints.mjs +0 -248
  522. package/tools/kimodo/preserve-mask.mjs +0 -543
  523. package/tools/kimodo/read-npz.mjs +0 -189
  524. package/tools/kimodo/run-edit-on-box.mjs +0 -155
  525. package/tools/kimodo/run-sequence-on-box.mjs +0 -178
  526. package/tools/kimodo/runner.mjs +0 -284
  527. package/tools/kimodo/seam-batch.mjs +0 -77
  528. package/tools/kimodo/setup-local.mjs +0 -30
  529. package/tools/kimodo/setup-on-box.sh +0 -339
  530. package/tools/kimodo/soma77-to-cskel27.mjs +0 -305
  531. package/tools/morphgs/assets/gen_truth.py +0 -39
  532. package/tools/morphgs/assets/mesh_ori_rig.txt +0 -27
  533. package/tools/morphgs/assets/playback-check.mjs +0 -30
  534. package/tools/morphgs/demo-gate.sh +0 -26
  535. package/tools/morphgs/fbx2morphgs.mjs +0 -68
  536. package/tools/morphgs/morphgs-to-cskel27.mjs +0 -105
  537. package/tools/morphgs/patches/preprocess_src-none-mode.patch +0 -76
  538. package/tools/morphgs/setup-on-cluster.sh +0 -126
  539. package/tools/projflow/driver.py +0 -1069
  540. package/tools/projflow/generate.mjs +0 -805
  541. package/tools/projflow/hml22-to-cskel27.mjs +0 -735
  542. package/tools/projflow/line-edit-job.mjs +0 -547
  543. package/tools/projflow/replay.mjs +0 -452
  544. package/tools/projflow/runner.mjs +0 -269
  545. package/tools/projflow/service.mjs +0 -742
  546. package/tools/track/DESIGN.md +0 -513
  547. package/tools/track/backfill-provenance.mjs +0 -116
  548. package/tools/track/budget.mjs +0 -8
  549. package/tools/track/check-rig.mjs +0 -209
  550. package/tools/track/diagnostics.schema.json +0 -60
  551. package/tools/track/export-rig.mjs +0 -256
  552. package/tools/track/fallback.mjs +0 -7
  553. package/tools/track/fk-parity-fixture.mjs +0 -161
  554. package/tools/track/gate.mjs +0 -349
  555. package/tools/track/masks.mjs +0 -318
  556. package/tools/track/metrics.mjs +0 -201
  557. package/tools/track/publish-obs.mjs +0 -108
  558. package/tools/track/py/check_env.py +0 -61
  559. package/tools/track/py/eval_lr.py +0 -229
  560. package/tools/track/py/lr_viterbi.py +0 -294
  561. package/tools/track/py/masks.py +0 -405
  562. package/tools/track/py/objective.py +0 -382
  563. package/tools/track/py/rig.py +0 -508
  564. package/tools/track/py/scene.py +0 -326
  565. package/tools/track/py/test_joint_indices.py +0 -123
  566. package/tools/track/py/test_lr_viterbi.py +0 -195
  567. package/tools/track/py/test_masks.py +0 -229
  568. package/tools/track/py/test_rig.py +0 -288
  569. package/tools/track/py/test_scene.py +0 -354
  570. package/tools/track/py/test_track.py +0 -328
  571. package/tools/track/py/track.py +0 -411
  572. package/tools/track/remote.mjs +0 -140
  573. package/tools/track/rig-dump.mjs +0 -227
  574. package/tools/track/run-box-tests.mjs +0 -14
  575. package/tools/track/run-box.mjs +0 -168
  576. package/tools/track/setup-box.sh +0 -59
  577. package/tools/track/study-2d.mjs +0 -728
  578. /package/src/{ardy → motion}/collision-blockers.js +0 -0
  579. /package/src/{ardy → motion}/ground.js +0 -0
  580. /package/src/{ardy → motion}/ik-drag.js +0 -0
  581. /package/src/{ardy → motion}/ik-key-json.js +0 -0
  582. /package/src/{ardy → motion}/ik.js +0 -0
  583. /package/src/{ardy → motion}/physics-surface.js +0 -0
  584. /package/src/{ardy → motion}/playback-clock.js +0 -0
  585. /package/src/{ardy → motion}/range-pin.js +0 -0
@@ -1,742 +0,0 @@
1
- /**
2
- * service.mjs — the resident (warm) ProjFlow child, contract C11.
3
- *
4
- * WHY THIS EXISTS. Gate GP3 measured a line edit at 7.6-8.6 s wall, of which
5
- * 3.9 s is loading the same 625 MB checkpoint the previous edit already loaded
6
- * and 1.3 s is the actual sample. The detail loop ("drag, look, drag again") is
7
- * ProjFlow's job now, so the load has to be paid once per SESSION rather than
8
- * once per drag. That is the entire idea: one `ssh <host> python driver.py
9
- * --serve` child, owned by this module, fed one NDJSON request per line.
10
- *
11
- * WHAT THIS MODULE IS NOT. It is not a scheduler, not a pool and not a server.
12
- * There is no port, no tunnel and nothing listening on the box — the transport
13
- * is the ssh child's own stdin/stdout, which means the resident inherits ssh's
14
- * authentication and dies with the pipe. One child, one request in flight, a
15
- * queue for the rest.
16
- *
17
- * THE COLD PATH IS THE CONTRACT OF RECORD. Every failure here — a dead child, a
18
- * response whose id does not match the request, a timeout, a driver error
19
- * envelope — turns into a `ResidentError`, and generate.mjs answers a
20
- * ResidentError by running the cold spawn path it always had. So the worst a
21
- * broken resident can do is make an edit as slow as it is today. That is why
22
- * the failure paths below are aggressive rather than forgiving: there is no
23
- * situation in which retrying inside the warm path beats falling back.
24
- *
25
- * TWO KINDS OF FAILURE, DELIBERATELY TREATED DIFFERENTLY:
26
- *
27
- * TRANSPORT failures (child died, id mismatch, unsolicited line, timeout)
28
- * mean the stream can no longer be trusted to pair responses with
29
- * requests. The child is killed and restarted with backoff.
30
- * ENVELOPE failures ({"ok": false} — a bad track, a frame range past the end
31
- * of the source) mean the driver understood us perfectly and refused. The
32
- * child is HEALTHY and stays up; the caller still falls back cold, where
33
- * the same request produces the same named error from the path the
34
- * contract is written against.
35
- *
36
- * RESTART POLICY. 1 s, 5 s, 25 s, capped, and only while a session looks
37
- * recoverable: after MAX_CONSECUTIVE_FAILURES the background loop gives up and
38
- * the service goes idle, so a box that is simply gone does not leave a retry
39
- * loop spawning ssh forever. The next edit starts it again on demand, which is
40
- * the right trigger — someone is at the keyboard.
41
- *
42
- * ENV
43
- * CCLAY_PROJFLOW_RESIDENT=0 disable the resident entirely (cold only)
44
- * CCLAY_PROJFLOW_RESIDENT_READY_MS how long to wait for the model load
45
- * CCLAY_PROJFLOW_RESIDENT_REQ_MS per-request ceiling on the warm path
46
- */
47
-
48
- import { spawn } from "node:child_process";
49
- import { randomBytes } from "node:crypto";
50
- import { dirname, join } from "node:path";
51
- import { fileURLToPath } from "node:url";
52
-
53
- const HERE = dirname(fileURLToPath(import.meta.url));
54
- const DRIVER = join(HERE, "driver.py");
55
-
56
- /** The same ssh options generate.mjs uses for the cold path, plus the keepalive
57
- * that matters more here: this child is expected to sit idle between edits. */
58
- const SSH_OPTS = [
59
- "-o", "BatchMode=yes",
60
- "-o", "ConnectTimeout=10",
61
- "-o", "ServerAliveInterval=30",
62
- "-o", "ServerAliveCountMax=6",
63
- ];
64
-
65
- /** Bumped in lockstep with driver.py's PROTOCOL_VERSION. A resident left over
66
- * from an earlier build answers with the old number and is refused rather than
67
- * trusted, which is the whole reason the field exists.
68
- *
69
- * NOT bumped for `seamPin` (2026-08-29), and the reason is worth stating because
70
- * it looks like an omission. The version guards FRAMING and field MEANING: an
71
- * old resident handed the new field ignores it, a new resident handed a request
72
- * without it applies its own default, and in both directions every existing
73
- * field still means exactly what it meant. Bumping would have been the safer
74
- * instinct and was measurably wrong — a long-lived wrapper process holds this
75
- * constant in memory while it re-ships driver.py from DISK on every resident
76
- * start, so a bump makes the freshly shipped driver announce a number its own
77
- * launcher refuses, and the supervisor crash-loops the box until the wrapper
78
- * itself is restarted. Bump this only when a stale resident would produce a
79
- * WRONG answer rather than an older one, and expect to restart every wrapper. */
80
- export const PROTOCOL_VERSION = 1;
81
-
82
- /** 1 s, 5 s, 25 s, then 25 s forever. Short enough that a transient ssh blip
83
- * costs one cold edit, long enough that a box being rebooted is not hammered. */
84
- export const BACKOFF_MS = Object.freeze([1_000, 5_000, 25_000]);
85
- export const MAX_CONSECUTIVE_FAILURES = 6;
86
-
87
- /** scp + ssh + a 3.9 s model load, with room for a cold page cache. */
88
- const DEFAULT_READY_MS = 45_000;
89
- /** A 100-step edit samples in ~1.3 s; the ceiling exists to bound a hung child,
90
- * not to be a realistic budget. */
91
- const DEFAULT_REQUEST_MS = 120_000;
92
- const PING_MS = 10_000;
93
-
94
- /**
95
- * A failure that means "use the cold path".
96
- *
97
- * `kind` is the only field callers branch on and it exists for one decision:
98
- * whether the child is still trustworthy. `transport` kills and restarts it;
99
- * `envelope` and `disabled` leave it alone.
100
- */
101
- export class ResidentError extends Error {
102
- constructor(message, { kind = "transport", cause } = {}) {
103
- super(message);
104
- this.name = "ResidentError";
105
- this.kind = kind;
106
- if (cause !== undefined) this.cause = cause;
107
- }
108
- }
109
-
110
- /** How long to wait before the Nth consecutive restart (N counted from 1).
111
- * `schedule` exists so a service and this function cannot end up with two
112
- * spellings of the same clamp; only tests ever pass one. */
113
- export function backoffDelay(failures, schedule = BACKOFF_MS) {
114
- const index = Math.max(0, Math.floor(failures) - 1);
115
- return schedule[Math.min(index, schedule.length - 1)];
116
- }
117
-
118
- /** The resident is opt-OUT: it is the fast path and the cold path is always
119
- * underneath it, so the default is on and the env var is an escape hatch for
120
- * bisecting a bad session. */
121
- export function residentEnabled(env = process.env) {
122
- const value = env.CCLAY_PROJFLOW_RESIDENT;
123
- return !(value === "0" || value === "false" || value === "off");
124
- }
125
-
126
- function positiveInt(value, fallback) {
127
- const number = Number(value);
128
- return Number.isFinite(number) && number > 0 ? Math.floor(number) : fallback;
129
- }
130
-
131
- /* ------------------------------------------------------------------------- */
132
- /* the transport */
133
- /* ------------------------------------------------------------------------- */
134
-
135
- function runOnce(argv, timeoutMs) {
136
- return new Promise((resolve) => {
137
- const child = spawn(argv[0], argv.slice(1), { stdio: ["ignore", "pipe", "pipe"] });
138
- let stdout = "";
139
- let stderr = "";
140
- const timer = setTimeout(() => child.kill("SIGKILL"), timeoutMs);
141
- child.stdout.on("data", (chunk) => (stdout += chunk));
142
- child.stderr.on("data", (chunk) => (stderr += chunk));
143
- child.on("error", (error) => {
144
- clearTimeout(timer);
145
- resolve({ code: -1, stdout, stderr: `${stderr}${error.message}` });
146
- });
147
- child.on("close", (code) => {
148
- clearTimeout(timer);
149
- resolve({ code, stdout, stderr });
150
- });
151
- });
152
- }
153
-
154
- /**
155
- * Ship the CURRENT driver.py and start it in serve mode.
156
- *
157
- * The deploy is the cold path's mechanism, unchanged: scp the file that is in
158
- * the tree right now, to a path this process generated. Nothing is installed on
159
- * the box and nothing survives the session — the remote copy is removed when the
160
- * service stops, and if the local process dies without stopping cleanly, a
161
- * single file in /tmp is the whole residue.
162
- *
163
- * `env HOME=... exec python` rather than a bare assignment: `exec` guarantees
164
- * the ssh channel owns the python process directly, so when this end of the
165
- * pipe closes, sshd's SIGHUP lands on the driver rather than on a shell that
166
- * would leave it orphaned holding 800 MiB of VRAM.
167
- */
168
- async function defaultStartChild({ host, python, repo, boxHome, driverPath, onLine }) {
169
- const remoteDriver = `/tmp/cclay-projflow-serve-${Date.now()}-${randomBytes(4).toString("hex")}.py`;
170
- const pushed = await runOnce(["scp", ...SSH_OPTS, driverPath, `${host}:${remoteDriver}`], 60_000);
171
- if (pushed.code !== 0) {
172
- throw new ResidentError(
173
- `could not copy the serve driver to ${host} (exit ${pushed.code}): ${pushed.stderr.trim()}`
174
- );
175
- }
176
- const remote = `exec env HOME=${boxHome} ${python} ${remoteDriver} --serve --repo ${JSON.stringify(repo)}`;
177
- onLine?.(`projflow-resident: starting ${host}:${remoteDriver}`);
178
- const child = spawn("ssh", [...SSH_OPTS, host, remote], { stdio: ["pipe", "pipe", "pipe"] });
179
- child.cleanupRemote = async () => {
180
- await runOnce(["ssh", ...SSH_OPTS, host, `rm -f ${remoteDriver}`], 20_000);
181
- };
182
- return child;
183
- }
184
-
185
- /* ------------------------------------------------------------------------- */
186
- /* the service */
187
- /* ------------------------------------------------------------------------- */
188
-
189
- /**
190
- * One resident child and everything that can go wrong with it.
191
- *
192
- * @param {object} config
193
- * @param {string} config.host ssh destination
194
- * @param {string} config.python the scout venv's python on the box
195
- * @param {string} config.repo the ProjFlow clone on the box
196
- * @param {string} config.boxHome HOME override (keeps CLIP's cache in the scout dir)
197
- * @param {Function} [config.startChild] injected for tests: returns a child
198
- * process-like object with piped stdio. The default scps driver.py and spawns
199
- * `ssh <host> python driver.py --serve`.
200
- */
201
- export function createResidentService({
202
- host,
203
- python,
204
- repo,
205
- boxHome,
206
- driverPath = DRIVER,
207
- startChild = defaultStartChild,
208
- // The schedule is a parameter for one reason: the restart state machine is
209
- // worth testing and 1 + 5 + 25 seconds is not a test. Production never passes
210
- // it.
211
- backoff = BACKOFF_MS,
212
- readyTimeoutMs = positiveInt(process.env.CCLAY_PROJFLOW_RESIDENT_READY_MS, DEFAULT_READY_MS),
213
- requestTimeoutMs = positiveInt(process.env.CCLAY_PROJFLOW_RESIDENT_REQ_MS, DEFAULT_REQUEST_MS),
214
- onLine,
215
- registerExitHook = true,
216
- } = {}) {
217
- if (!host) throw new Error("createResidentService: host is required");
218
-
219
- let child = null;
220
- let state = "idle"; // idle | starting | ready | backoff | stopped
221
- let readyPromise = null;
222
- let readyInfo = null;
223
- let failures = 0;
224
- let restartTimer = null;
225
- let pending = null; // the single in-flight request
226
- let readyWaiters = [];
227
- let queue = Promise.resolve();
228
- let counter = 0;
229
- let stdoutBuffer = "";
230
-
231
- /**
232
- * Does the warm child count as work this process is waiting for?
233
- *
234
- * It must, while a request is in flight or the model is loading — otherwise
235
- * node would exit out from under an awaited edit, because the only pending
236
- * handle is a pipe. It must NOT while the child is merely warm, or every
237
- * short-lived process that ever ran an edit would hang forever holding an
238
- * idle ssh open. So the child is ref'd exactly for the duration of the work
239
- * and unref'd the rest of the time; the exit hook does the killing.
240
- */
241
- function keepAlive(on) {
242
- if (!child) return;
243
- const handles = [child, child.stdin, child.stdout, child.stderr];
244
- for (const handle of handles) {
245
- try {
246
- if (on) handle?.ref?.();
247
- else handle?.unref?.();
248
- } catch {
249
- // A stream that is already destroyed has nothing to ref.
250
- }
251
- }
252
- }
253
-
254
- const say = (text) => {
255
- try {
256
- (pending?.onLine || onLine)?.(text);
257
- } catch {
258
- // A status sink that throws must not take the service with it.
259
- }
260
- };
261
-
262
- /** Kill the child and forget it. `reason` is null for an orderly stop. */
263
- function tearDown(reason) {
264
- const dying = child;
265
- child = null;
266
- readyPromise = null;
267
- readyInfo = null;
268
- stdoutBuffer = "";
269
- const error = new ResidentError(reason || "the resident was stopped", { kind: "transport" });
270
- if (pending) {
271
- const failed = pending;
272
- pending = null;
273
- clearTimeout(failed.timer);
274
- failed.reject(error);
275
- }
276
- const waiting = readyWaiters;
277
- readyWaiters = [];
278
- for (const waiter of waiting) waiter.reject(error);
279
- if (dying) {
280
- dying.removeAllListeners?.("close");
281
- dying.removeAllListeners?.("error");
282
- try {
283
- dying.stdin?.end?.();
284
- } catch {
285
- // The pipe may already be gone; killing is what matters.
286
- }
287
- try {
288
- dying.kill?.("SIGKILL");
289
- } catch {
290
- // Same.
291
- }
292
- // Best-effort: the remote /tmp copy of the driver. A failure here is
293
- // one stale file, never a failed edit, so it is never awaited.
294
- Promise.resolve(dying.cleanupRemote?.()).catch(() => {});
295
- }
296
- }
297
-
298
- /** A transport failure: drop the child, count it, and schedule a restart so
299
- * the NEXT edit is warm even though this one goes cold. */
300
- function fail(reason) {
301
- if (state === "stopped") return;
302
- tearDown(reason);
303
- failures += 1;
304
- if (failures >= MAX_CONSECUTIVE_FAILURES) {
305
- // The box is not coming back on its own. Go idle rather than keep an
306
- // ssh retry loop alive; the next request restarts on demand.
307
- state = "idle";
308
- say(`projflow-resident: giving up after ${failures} failed starts (${reason}); edits stay cold`);
309
- return;
310
- }
311
- const delay = backoffDelay(failures, backoff);
312
- state = "backoff";
313
- say(`projflow-resident: ${reason}; restarting in ${delay / 1000}s`);
314
- clearTimeout(restartTimer);
315
- restartTimer = setTimeout(() => {
316
- restartTimer = null;
317
- if (state !== "backoff") return;
318
- state = "idle";
319
- // Fire and forget: a background restart that fails just schedules the
320
- // next one, and nobody is awaiting this.
321
- start().catch(() => {});
322
- }, delay);
323
- // The timer must never hold the process open — a warm model is a
324
- // convenience, not a reason for `node` to refuse to exit.
325
- restartTimer.unref?.();
326
- }
327
-
328
- function handleMessage(message) {
329
- if (message && message.type === "ready") {
330
- if (readyInfo) {
331
- fail("the resident announced itself twice");
332
- return;
333
- }
334
- readyInfo = message;
335
- const waiting = readyWaiters;
336
- readyWaiters = [];
337
- for (const waiter of waiting) waiter.resolve(message);
338
- return;
339
- }
340
- if (!pending) {
341
- // Nothing is in flight, so this line cannot be an answer to anything.
342
- // The two sides no longer agree about framing.
343
- fail(`the resident sent an unsolicited response (id ${JSON.stringify(message?.id)})`);
344
- return;
345
- }
346
- if (message?.id !== pending.id) {
347
- // THE stale-response guard: pairing this with the in-flight request
348
- // would hand the caller another edit's motion.
349
- const wanted = pending.id;
350
- fail(`response id ${JSON.stringify(message?.id)} does not match request ${JSON.stringify(wanted)}`);
351
- return;
352
- }
353
- const settled = pending;
354
- pending = null;
355
- clearTimeout(settled.timer);
356
- keepAlive(false);
357
- if (message.ok) {
358
- settled.resolve(message.result);
359
- return;
360
- }
361
- // The driver refused the request. It is still healthy, so it stays up and
362
- // only this request falls back.
363
- const detail = message.error?.message || "the resident refused the request";
364
- settled.reject(new ResidentError(detail, { kind: "envelope" }));
365
- }
366
-
367
- function attach(spawned) {
368
- spawned.stdout?.setEncoding?.("utf8");
369
- spawned.stdout?.on("data", (chunk) => {
370
- stdoutBuffer += chunk;
371
- let newline = stdoutBuffer.indexOf("\n");
372
- while (newline >= 0) {
373
- const text = stdoutBuffer.slice(0, newline).trim();
374
- stdoutBuffer = stdoutBuffer.slice(newline + 1);
375
- if (text) {
376
- let message;
377
- try {
378
- message = JSON.parse(text);
379
- } catch {
380
- // driver.py points fd 1 at stderr before it imports anything,
381
- // so a non-JSON line on stdout means something wrote past that
382
- // guard and the stream is no longer a protocol.
383
- fail(`the resident wrote a non-protocol line to stdout: ${text.slice(0, 120)}`);
384
- return;
385
- }
386
- handleMessage(message);
387
- if (!child) return; // handleMessage tore the child down
388
- }
389
- newline = stdoutBuffer.indexOf("\n");
390
- }
391
- });
392
- spawned.stderr?.setEncoding?.("utf8");
393
- spawned.stderr?.on("data", (chunk) => {
394
- for (const text of String(chunk).split("\n")) if (text.trim()) say(text);
395
- });
396
- spawned.on("error", (error) => fail(`the resident child errored: ${error.message}`));
397
- spawned.on("close", (code, signal) => {
398
- if (state === "stopped") return;
399
- fail(`the resident exited (code ${code}${signal ? `, ${signal}` : ""})`);
400
- });
401
- }
402
-
403
- /**
404
- * Start the child and resolve once it has announced a compatible protocol.
405
- *
406
- * Memoised, and the memo is CLEARED by `fail` (through tearDown), so a start
407
- * that failed is never handed to the next caller as a cached rejection — the
408
- * next edit gets a fresh attempt or a fast "in backoff" refusal, both of
409
- * which are honest answers.
410
- */
411
- function start() {
412
- if (state === "stopped") return Promise.reject(new ResidentError("the resident is stopped"));
413
- // Backoff means "we already know this is broken, and the clock has not run
414
- // out". Starting anyway would spend the backoff's whole purpose — and it
415
- // would add a start attempt to an edit that should already be running
416
- // cold. Only the restart timer moves the state out of backoff.
417
- if (state === "backoff") {
418
- return Promise.reject(new ResidentError("the resident is restarting; this edit goes cold"));
419
- }
420
- if (readyPromise) return readyPromise;
421
- state = "starting";
422
- const attempt = (async () => {
423
- const spawned = await startChild({ host, python, repo, boxHome, driverPath, onLine: say });
424
- child = spawned;
425
- attach(spawned);
426
- keepAlive(true); // the model load is work this process is waiting on
427
- const announced = readyInfo ?? await new Promise((resolve, reject) => {
428
- const timer = setTimeout(
429
- () => fail(`the resident did not become ready within ${readyTimeoutMs} ms`),
430
- readyTimeoutMs
431
- );
432
- // NOT unref'd, unlike the restart timer below: this one is the
433
- // promise's own guarantee that it settles, and a process whose only
434
- // remaining work is an awaited start must not exit out from under it.
435
- readyWaiters.push({
436
- resolve: (message) => {
437
- clearTimeout(timer);
438
- resolve(message);
439
- },
440
- reject: (error) => {
441
- clearTimeout(timer);
442
- reject(error);
443
- },
444
- });
445
- });
446
- if (Number(announced.protocol) !== PROTOCOL_VERSION) {
447
- throw new ResidentError(
448
- `the resident speaks protocol ${announced.protocol}, this build speaks ${PROTOCOL_VERSION}`
449
- );
450
- }
451
- state = "ready";
452
- failures = 0;
453
- if (!pending) keepAlive(false);
454
- say(
455
- `projflow-resident: ready on ${announced.device ?? "?"} in ${announced.loadSeconds ?? "?"}s ` +
456
- `(protocol ${announced.protocol})`
457
- );
458
- return announced;
459
- })().catch((error) => {
460
- const wrapped = error instanceof ResidentError
461
- ? error
462
- : new ResidentError(`the resident failed to start: ${error.message}`, { cause: error });
463
- // `fail` is idempotent enough for this: when the child died on its own
464
- // it has already run, and a second call only costs another backoff
465
- // slot on a service that is not going to be used until it restarts.
466
- if (state === "starting" || state === "ready") fail(wrapped.message);
467
- throw wrapped;
468
- });
469
- readyPromise = attempt;
470
- return attempt;
471
- }
472
-
473
- function send(payload, timeoutMs, lineSink) {
474
- return new Promise((resolve, reject) => {
475
- if (!child || state !== "ready") {
476
- reject(new ResidentError("the resident is not ready"));
477
- return;
478
- }
479
- counter += 1;
480
- const id = `r${counter}`;
481
- const timer = setTimeout(() => {
482
- // A timeout is a TRANSPORT failure, not a slow answer we can wait
483
- // out: if the response arrives later it would be paired with the
484
- // next request, which is exactly the bug the ids exist to prevent.
485
- fail(`request ${id} timed out after ${timeoutMs} ms`);
486
- }, timeoutMs);
487
- // Ref'd on purpose (see the ready timeout): this timer is what makes an
488
- // awaited edit always settle, warm child or not.
489
- pending = { id, resolve, reject, timer, onLine: lineSink };
490
- keepAlive(true);
491
- try {
492
- child.stdin.write(`${JSON.stringify({ id, ...payload })}\n`);
493
- } catch (error) {
494
- fail(`could not write to the resident: ${error.message}`);
495
- }
496
- });
497
- }
498
-
499
- /**
500
- * Queue one request against the child, starting it if necessary.
501
- *
502
- * Serialised on purpose. The bridge sends one line edit at a time today, but
503
- * "today" is not a guarantee, and a second writer would interleave lines on
504
- * one stdin.
505
- */
506
- function request(payload, { timeoutMs = requestTimeoutMs, onLine: lineSink } = {}) {
507
- if (state === "stopped") return Promise.reject(new ResidentError("the resident is stopped"));
508
- // A child that is in backoff is a child we already know is broken. Waiting
509
- // for it would add its backoff to an edit that could already be running
510
- // cold.
511
- if (state === "backoff") {
512
- return Promise.reject(new ResidentError("the resident is restarting; this edit goes cold", { kind: "transport" }));
513
- }
514
- const run = async () => {
515
- await start();
516
- return send(payload, timeoutMs, lineSink);
517
- };
518
- // `.then(run, run)` rather than `.then(run)`: one failed request must not
519
- // poison the queue for the next one.
520
- const result = queue.then(run, run);
521
- queue = result.then(() => {}, () => {});
522
- return result;
523
- }
524
-
525
- function stop() {
526
- state = "stopped";
527
- clearTimeout(restartTimer);
528
- restartTimer = null;
529
- tearDown(null);
530
- LIVE.delete(service);
531
- }
532
-
533
- const service = {
534
- key: JSON.stringify({ host, python, repo, boxHome }),
535
- host,
536
- request,
537
- start,
538
- stop,
539
- state: () => state,
540
- failures: () => failures,
541
- isReady: () => state === "ready" && Boolean(child),
542
- info: () => readyInfo,
543
- /** A ping is the only way to claim a resident is up: `state === "ready"`
544
- * says the child announced itself, an answered ping says it is still
545
- * there. probeHealth reports the second, never the first. */
546
- ping: (timeoutMs = PING_MS) => request({ type: "ping" }, { timeoutMs }),
547
- };
548
-
549
- if (registerExitHook) armExitCleanup(service);
550
- return service;
551
- }
552
-
553
- /* ------------------------------------------------------------------------- */
554
- /* process lifetime */
555
- /* ------------------------------------------------------------------------- */
556
-
557
- const LIVE = new Set();
558
- let hooksArmed = false;
559
-
560
- /**
561
- * Kill every resident when this process goes away.
562
- *
563
- * `exit` covers the normal case. The signal handlers are more careful than they
564
- * look: adding a SIGTERM listener SUPPRESSES node's default termination, so a
565
- * handler installed by a library would silently make the bridge unkillable.
566
- * When nobody else is listening we kill the children, remove ourselves and
567
- * re-raise the signal so the default behaviour still happens; when the host
568
- * process has its own handler we only do the cleanup and let it decide.
569
- */
570
- function armExitCleanup(service) {
571
- LIVE.add(service);
572
- if (hooksArmed) return;
573
- hooksArmed = true;
574
- const killAll = () => {
575
- for (const live of LIVE) {
576
- try {
577
- live.stop();
578
- } catch {
579
- // Nothing useful to do on the way out.
580
- }
581
- }
582
- LIVE.clear();
583
- };
584
- process.once("exit", killAll);
585
- for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"]) {
586
- const hostHandles = process.listenerCount(signal) > 0;
587
- const handler = () => {
588
- killAll();
589
- if (!hostHandles) {
590
- process.removeListener(signal, handler);
591
- process.kill(process.pid, signal);
592
- }
593
- };
594
- process.on(signal, handler);
595
- }
596
- }
597
-
598
- /* ------------------------------------------------------------------------- */
599
- /* the shared instance */
600
- /* ------------------------------------------------------------------------- */
601
-
602
- let shared = null;
603
-
604
- /** The process-wide resident for one box configuration, created on demand.
605
- * Changing the box (or the venv, or the repo) replaces it: a warm child pointed
606
- * at the wrong repo is worse than no child at all. */
607
- export function getResidentService(config) {
608
- const key = JSON.stringify({
609
- host: config.host,
610
- python: config.python,
611
- repo: config.repo,
612
- boxHome: config.boxHome,
613
- });
614
- if (shared && shared.key !== key) {
615
- shared.stop();
616
- shared = null;
617
- }
618
- if (!shared) shared = createResidentService(config);
619
- return shared;
620
- }
621
-
622
- /** The existing resident, or null. Used by probeHealth, which must never START
623
- * a model load just to answer a health question. */
624
- export function peekResidentService(config) {
625
- if (!shared) return null;
626
- if (!config) return shared;
627
- const key = JSON.stringify({
628
- host: config.host,
629
- python: config.python,
630
- repo: config.repo,
631
- boxHome: config.boxHome,
632
- });
633
- return shared.key === key ? shared : null;
634
- }
635
-
636
- export function stopResidentService() {
637
- if (shared) shared.stop();
638
- shared = null;
639
- }
640
-
641
- /* ------------------------------------------------------------------------- */
642
- /* base64 float32 arrays */
643
- /* ------------------------------------------------------------------------- */
644
-
645
- /** A Float32Array as the {shape, dtype, data} blob driver.py decodes. Little
646
- * endian is the wire format and every machine either side of this is; the
647
- * dtype name travels so the driver can refuse rather than reinterpret. */
648
- export function encodeFloat32(data, shape) {
649
- if (!(data instanceof Float32Array)) throw new TypeError("encodeFloat32: data must be a Float32Array");
650
- const count = shape.reduce((total, dim) => total * dim, 1);
651
- if (count !== data.length) {
652
- throw new Error(`encodeFloat32: shape (${shape.join(",")}) needs ${count} values, got ${data.length}`);
653
- }
654
- return {
655
- shape: [...shape],
656
- dtype: "float32",
657
- data: Buffer.from(data.buffer, data.byteOffset, data.byteLength).toString("base64"),
658
- };
659
- }
660
-
661
- /** The inverse, with the shape checked rather than trusted: a payload whose
662
- * byte count disagrees with its own shape is a corrupt response, not a motion. */
663
- export function decodeFloat32(blob) {
664
- if (!blob || typeof blob !== "object") throw new ResidentError("the resident returned no array");
665
- const shape = Array.isArray(blob.shape) ? blob.shape.map(Number) : null;
666
- if (!shape || shape.some((dim) => !Number.isInteger(dim) || dim < 0)) {
667
- throw new ResidentError(`the resident returned a bad shape ${JSON.stringify(blob.shape)}`);
668
- }
669
- if (blob.dtype && blob.dtype !== "float32") {
670
- throw new ResidentError(`the resident returned dtype ${blob.dtype}; this path is float32 only`);
671
- }
672
- const bytes = Buffer.from(String(blob.data || ""), "base64");
673
- const count = shape.reduce((total, dim) => total * dim, 1);
674
- if (bytes.length !== count * 4) {
675
- throw new ResidentError(
676
- `the resident returned ${bytes.length} bytes for shape (${shape.join(",")}), expected ${count * 4}`
677
- );
678
- }
679
- // A copy, not a view: Buffer.from(base64) has no alignment guarantee and a
680
- // Float32Array view over an odd byteOffset throws.
681
- const data = new Float32Array(count);
682
- for (let index = 0; index < count; index += 1) data[index] = bytes.readFloatLE(index * 4);
683
- return { data, shape };
684
- }
685
-
686
- /* ------------------------------------------------------------------------- */
687
- /* one warm line edit */
688
- /* ------------------------------------------------------------------------- */
689
-
690
- /**
691
- * Run one line edit on the warm child.
692
- *
693
- * Everything a one-shot invocation carries travels in the request, because the
694
- * resident has no per-run argv: the C6 line document, the source motion as a
695
- * base64 float32 blob, the step count (or the preview flag), the ridge, the
696
- * preserve knobs, the seed and the cfg. What comes back is the same
697
- * `(T,22,3)` motion and the same meta the one-shot writes to
698
- * `<out>.meta.json`.
699
- *
700
- * @throws {ResidentError} on ANY failure. The caller's answer to that is the
701
- * cold path — never a retry here.
702
- */
703
- export async function residentLineEdit({
704
- service,
705
- source,
706
- sourceShape,
707
- line,
708
- steps,
709
- preview = false,
710
- ridge,
711
- preserveStride,
712
- preserveMargin,
713
- seamPin,
714
- seed,
715
- cfg,
716
- timeoutMs,
717
- onLine,
718
- } = {}) {
719
- if (!service) throw new ResidentError("residentLineEdit: no resident service");
720
- const payload = {
721
- type: "lineEdit",
722
- line,
723
- source: encodeFloat32(source, sourceShape),
724
- preview: Boolean(preview),
725
- steps: Number(steps),
726
- ridge: Number(ridge),
727
- preserveStride: Number(preserveStride),
728
- preserveMargin: Number(preserveMargin),
729
- };
730
- if (seamPin !== undefined) payload.seamPin = Number(seamPin);
731
- // Omitted rather than defaulted on this side: driver.py owns the defaults
732
- // (0 and 3.0) and two places spelling them is how they drift.
733
- if (seed !== undefined) payload.seed = Number(seed);
734
- if (cfg !== undefined) payload.cfg = Number(cfg);
735
-
736
- const result = await service.request(payload, { timeoutMs, onLine });
737
- const { data, shape } = decodeFloat32(result);
738
- if (!result.meta || typeof result.meta !== "object") {
739
- throw new ResidentError("the resident returned a motion with no metadata");
740
- }
741
- return { positions: data, shape, meta: result.meta };
742
- }