@hotmeshio/long-tail 0.8.2 → 0.8.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 (194) hide show
  1. package/build/api/escalations/create.js +19 -60
  2. package/build/api/escalations/resolve.js +12 -4
  3. package/build/api/escalations/single.js +8 -4
  4. package/build/api/roles.d.ts +1 -0
  5. package/build/scripts/twin-farm.d.ts +44 -0
  6. package/build/scripts/twin-farm.js +427 -0
  7. package/build/services/escalation/queries.d.ts +13 -0
  8. package/build/services/escalation/queries.js +27 -0
  9. package/build/services/orchestrator/condition.d.ts +18 -12
  10. package/build/services/orchestrator/condition.js +11 -8
  11. package/build/services/role/index.d.ts +1 -2
  12. package/build/services/role/index.js +1 -2
  13. package/build/services/role/schema-validator.d.ts +9 -0
  14. package/build/services/role/schema-validator.js +42 -0
  15. package/build/services/role/sql.d.ts +1 -1
  16. package/build/services/role/sql.js +2 -2
  17. package/build/services/role/types.d.ts +8 -6
  18. package/build/system/mcp-servers/admin/schemas.js +4 -2
  19. package/build/system/mcp-servers/human-queue-schemas.js +6 -2
  20. package/build/system/mcp-servers/human-queue.js +11 -3
  21. package/build/tsconfig.tsbuildinfo +1 -1
  22. package/build/types/config.d.ts +2 -0
  23. package/build/types/startup.d.ts +5 -1
  24. package/dashboard/dist/assets/{AdminDashboard-Nvja-XIJ.js → AdminDashboard-B-JptT54.js} +2 -2
  25. package/dashboard/dist/assets/{AdminDashboard-Nvja-XIJ.js.map → AdminDashboard-B-JptT54.js.map} +1 -1
  26. package/dashboard/dist/assets/{AgentConfigPage-Sf2god7q.js → AgentConfigPage-B1Jc6iJk.js} +2 -2
  27. package/dashboard/dist/assets/{AgentConfigPage-Sf2god7q.js.map → AgentConfigPage-B1Jc6iJk.js.map} +1 -1
  28. package/dashboard/dist/assets/{AgentDetailPage-DtgDeZJR.js → AgentDetailPage-CWpclCOY.js} +2 -2
  29. package/dashboard/dist/assets/{AgentDetailPage-DtgDeZJR.js.map → AgentDetailPage-CWpclCOY.js.map} +1 -1
  30. package/dashboard/dist/assets/{AgentsPage-C6zqP_xC.js → AgentsPage-DyAf5GA2.js} +2 -2
  31. package/dashboard/dist/assets/{AgentsPage-C6zqP_xC.js.map → AgentsPage-DyAf5GA2.js.map} +1 -1
  32. package/dashboard/dist/assets/{AvailableEscalationsPage-D61kC7Is.js → AvailableEscalationsPage-DDwAgklw.js} +2 -2
  33. package/dashboard/dist/assets/{AvailableEscalationsPage-D61kC7Is.js.map → AvailableEscalationsPage-DDwAgklw.js.map} +1 -1
  34. package/dashboard/dist/assets/{BotPicker-ChgljKHj.js → BotPicker-B1RdaNuY.js} +2 -2
  35. package/dashboard/dist/assets/{BotPicker-ChgljKHj.js.map → BotPicker-B1RdaNuY.js.map} +1 -1
  36. package/dashboard/dist/assets/{CapabilitiesPage-DeybKoZM.js → CapabilitiesPage-CxHDnyjX.js} +2 -2
  37. package/dashboard/dist/assets/{CapabilitiesPage-DeybKoZM.js.map → CapabilitiesPage-CxHDnyjX.js.map} +1 -1
  38. package/dashboard/dist/assets/{CollapsibleSection-BPiTuPBk.js → CollapsibleSection-Ds0e8cCO.js} +2 -2
  39. package/dashboard/dist/assets/{CollapsibleSection-BPiTuPBk.js.map → CollapsibleSection-Ds0e8cCO.js.map} +1 -1
  40. package/dashboard/dist/assets/{CredentialsPage-DXOcPWtd.js → CredentialsPage-D8UVO-3A.js} +2 -2
  41. package/dashboard/dist/assets/{CredentialsPage-DXOcPWtd.js.map → CredentialsPage-D8UVO-3A.js.map} +1 -1
  42. package/dashboard/dist/assets/{CronLabel-D3411N8x.js → CronLabel-ZesYXxCH.js} +2 -2
  43. package/dashboard/dist/assets/{CronLabel-D3411N8x.js.map → CronLabel-ZesYXxCH.js.map} +1 -1
  44. package/dashboard/dist/assets/{CustomDurationPicker-WG2tPSbV.js → CustomDurationPicker-OKlwH5a_.js} +2 -2
  45. package/dashboard/dist/assets/{CustomDurationPicker-WG2tPSbV.js.map → CustomDurationPicker-OKlwH5a_.js.map} +1 -1
  46. package/dashboard/dist/assets/{ElapsedCell-BFWEGSD7.js → ElapsedCell-CO8S6vdb.js} +2 -2
  47. package/dashboard/dist/assets/{ElapsedCell-BFWEGSD7.js.map → ElapsedCell-CO8S6vdb.js.map} +1 -1
  48. package/dashboard/dist/assets/{EscalationSchemaPage-D2Y5Yeqn.js → EscalationSchemaPage-Br5sD8-N.js} +2 -2
  49. package/dashboard/dist/assets/{EscalationSchemaPage-D2Y5Yeqn.js.map → EscalationSchemaPage-Br5sD8-N.js.map} +1 -1
  50. package/dashboard/dist/assets/{EscalationsOverview-VlLcX2YM.js → EscalationsOverview-DMpRvVql.js} +2 -2
  51. package/dashboard/dist/assets/{EscalationsOverview-VlLcX2YM.js.map → EscalationsOverview-DMpRvVql.js.map} +1 -1
  52. package/dashboard/dist/assets/{EventTable-3MJw14jp.js → EventTable-BARJeiS2.js} +2 -2
  53. package/dashboard/dist/assets/{EventTable-3MJw14jp.js.map → EventTable-BARJeiS2.js.map} +1 -1
  54. package/dashboard/dist/assets/{GraphInvokePage-QaUUrPnh.js → GraphInvokePage-CTuul-av.js} +2 -2
  55. package/dashboard/dist/assets/{GraphInvokePage-QaUUrPnh.js.map → GraphInvokePage-CTuul-av.js.map} +1 -1
  56. package/dashboard/dist/assets/{HomePage-Br0A3iOp.js → HomePage-BKFpOaHX.js} +2 -2
  57. package/dashboard/dist/assets/{HomePage-Br0A3iOp.js.map → HomePage-BKFpOaHX.js.map} +1 -1
  58. package/dashboard/dist/assets/{ListToolbar-d4MGQefV.js → ListToolbar-Dz2_neVF.js} +2 -2
  59. package/dashboard/dist/assets/{ListToolbar-d4MGQefV.js.map → ListToolbar-Dz2_neVF.js.map} +1 -1
  60. package/dashboard/dist/assets/{McpOverview-CUzJD5Z5.js → McpOverview-Do-vaEqV.js} +2 -2
  61. package/dashboard/dist/assets/{McpOverview-CUzJD5Z5.js.map → McpOverview-Do-vaEqV.js.map} +1 -1
  62. package/dashboard/dist/assets/{McpQueryDetailPage-pYSZmV3M.js → McpQueryDetailPage-HwDGNSbN.js} +2 -2
  63. package/dashboard/dist/assets/{McpQueryDetailPage-pYSZmV3M.js.map → McpQueryDetailPage-HwDGNSbN.js.map} +1 -1
  64. package/dashboard/dist/assets/{McpQueryPage-CA5C5dss.js → McpQueryPage-DxMUpQ7R.js} +2 -2
  65. package/dashboard/dist/assets/{McpQueryPage-CA5C5dss.js.map → McpQueryPage-DxMUpQ7R.js.map} +1 -1
  66. package/dashboard/dist/assets/{McpRunDetailPage-CrRkXwAe.js → McpRunDetailPage-BzzZIXIk.js} +2 -2
  67. package/dashboard/dist/assets/{McpRunDetailPage-CrRkXwAe.js.map → McpRunDetailPage-BzzZIXIk.js.map} +1 -1
  68. package/dashboard/dist/assets/{McpRunsPage-BqRBDbkn.js → McpRunsPage-D3J42d2i.js} +2 -2
  69. package/dashboard/dist/assets/{McpRunsPage-BqRBDbkn.js.map → McpRunsPage-D3J42d2i.js.map} +1 -1
  70. package/dashboard/dist/assets/OperationsPage-BaM-QDhy.js +2 -0
  71. package/dashboard/dist/assets/{OperationsPage--uVJxzmP.js.map → OperationsPage-BaM-QDhy.js.map} +1 -1
  72. package/dashboard/dist/assets/{OperatorDashboard-D-UMrTqI.js → OperatorDashboard-BqJn8wEZ.js} +2 -2
  73. package/dashboard/dist/assets/{OperatorDashboard-D-UMrTqI.js.map → OperatorDashboard-BqJn8wEZ.js.map} +1 -1
  74. package/dashboard/dist/assets/{ProcessDetailPage-BV6D_wuR.js → ProcessDetailPage-C8sT2xuE.js} +2 -2
  75. package/dashboard/dist/assets/{ProcessDetailPage-BV6D_wuR.js.map → ProcessDetailPage-C8sT2xuE.js.map} +1 -1
  76. package/dashboard/dist/assets/{ProcessesListPage-BAalEHpx.js → ProcessesListPage-eA8buUgd.js} +2 -2
  77. package/dashboard/dist/assets/{ProcessesListPage-BAalEHpx.js.map → ProcessesListPage-eA8buUgd.js.map} +1 -1
  78. package/dashboard/dist/assets/{RoleDetailPage-ClE7rUoo.js → RoleDetailPage-aR-ror5v.js} +2 -2
  79. package/dashboard/dist/assets/{RoleDetailPage-ClE7rUoo.js.map → RoleDetailPage-aR-ror5v.js.map} +1 -1
  80. package/dashboard/dist/assets/{RolesPage-DMsgZioQ.js → RolesPage-BQNQxsMY.js} +2 -2
  81. package/dashboard/dist/assets/{RolesPage-DMsgZioQ.js.map → RolesPage-BQNQxsMY.js.map} +1 -1
  82. package/dashboard/dist/assets/{RunAsSelector-VCnEYIwj.js → RunAsSelector-mhusrlYE.js} +2 -2
  83. package/dashboard/dist/assets/{RunAsSelector-VCnEYIwj.js.map → RunAsSelector-mhusrlYE.js.map} +1 -1
  84. package/dashboard/dist/assets/{StickyPagination-3eCY2-_V.js → StickyPagination-CqT9Q1I6.js} +2 -2
  85. package/dashboard/dist/assets/{StickyPagination-3eCY2-_V.js.map → StickyPagination-CqT9Q1I6.js.map} +1 -1
  86. package/dashboard/dist/assets/{StreamMessageDetail-70kDFH4o.js → StreamMessageDetail-jOJaoupp.js} +2 -2
  87. package/dashboard/dist/assets/{StreamMessageDetail-70kDFH4o.js.map → StreamMessageDetail-jOJaoupp.js.map} +1 -1
  88. package/dashboard/dist/assets/{SwimlaneTimeline-BOXRit6t.js → SwimlaneTimeline-BrfmWgAj.js} +2 -2
  89. package/dashboard/dist/assets/{SwimlaneTimeline-BOXRit6t.js.map → SwimlaneTimeline-BrfmWgAj.js.map} +1 -1
  90. package/dashboard/dist/assets/{TaskDetailPage-YxGQE9Qg.js → TaskDetailPage-Cus_JHIC.js} +2 -2
  91. package/dashboard/dist/assets/{TaskDetailPage-YxGQE9Qg.js.map → TaskDetailPage-Cus_JHIC.js.map} +1 -1
  92. package/dashboard/dist/assets/{TasksListPage-DuLJQN7Q.js → TasksListPage-B7GK8AoX.js} +2 -2
  93. package/dashboard/dist/assets/{TasksListPage-DuLJQN7Q.js.map → TasksListPage-B7GK8AoX.js.map} +1 -1
  94. package/dashboard/dist/assets/{TimeAgo-dnc1ZjKc.js → TimeAgo-C606LzT3.js} +2 -2
  95. package/dashboard/dist/assets/{TimeAgo-dnc1ZjKc.js.map → TimeAgo-C606LzT3.js.map} +1 -1
  96. package/dashboard/dist/assets/{TimestampCell-CDR6cTyq.js → TimestampCell-hfh6zvH-.js} +2 -2
  97. package/dashboard/dist/assets/{TimestampCell-CDR6cTyq.js.map → TimestampCell-hfh6zvH-.js.map} +1 -1
  98. package/dashboard/dist/assets/{ToolTestPanel-CfwGdRdz.js → ToolTestPanel-CsPsyvQq.js} +2 -2
  99. package/dashboard/dist/assets/{ToolTestPanel-CfwGdRdz.js.map → ToolTestPanel-CsPsyvQq.js.map} +1 -1
  100. package/dashboard/dist/assets/{TopicDetailPage-f2qaeGFz.js → TopicDetailPage-Diz6r7zr.js} +2 -2
  101. package/dashboard/dist/assets/{TopicDetailPage-f2qaeGFz.js.map → TopicDetailPage-Diz6r7zr.js.map} +1 -1
  102. package/dashboard/dist/assets/{TopicsPage-CwYdVmyK.js → TopicsPage-B6fn7fFh.js} +2 -2
  103. package/dashboard/dist/assets/{TopicsPage-CwYdVmyK.js.map → TopicsPage-B6fn7fFh.js.map} +1 -1
  104. package/dashboard/dist/assets/{UserName-CRE7EZPU.js → UserName-Cc-5QH5q.js} +2 -2
  105. package/dashboard/dist/assets/{UserName-CRE7EZPU.js.map → UserName-Cc-5QH5q.js.map} +1 -1
  106. package/dashboard/dist/assets/{WorkflowExecutionPage-BjNBu-Zt.js → WorkflowExecutionPage-BFq7UdhA.js} +2 -2
  107. package/dashboard/dist/assets/{WorkflowExecutionPage-BjNBu-Zt.js.map → WorkflowExecutionPage-BFq7UdhA.js.map} +1 -1
  108. package/dashboard/dist/assets/{WorkflowsDashboard-DfpvbzNt.js → WorkflowsDashboard-CHCM6hUa.js} +2 -2
  109. package/dashboard/dist/assets/{WorkflowsDashboard-DfpvbzNt.js.map → WorkflowsDashboard-CHCM6hUa.js.map} +1 -1
  110. package/dashboard/dist/assets/{WorkflowsOverview-BwD2lQwj.js → WorkflowsOverview-WeOvl8-_.js} +2 -2
  111. package/dashboard/dist/assets/{WorkflowsOverview-BwD2lQwj.js.map → WorkflowsOverview-WeOvl8-_.js.map} +1 -1
  112. package/dashboard/dist/assets/{YamlWorkflowDetailPage-BhHb7j2s.js → YamlWorkflowDetailPage-6GfhVDpv.js} +2 -2
  113. package/dashboard/dist/assets/{YamlWorkflowDetailPage-BhHb7j2s.js.map → YamlWorkflowDetailPage-6GfhVDpv.js.map} +1 -1
  114. package/dashboard/dist/assets/{YamlWorkflowsPage-SBRpY2dI.js → YamlWorkflowsPage-DqcY3ocP.js} +2 -2
  115. package/dashboard/dist/assets/{YamlWorkflowsPage-SBRpY2dI.js.map → YamlWorkflowsPage-DqcY3ocP.js.map} +1 -1
  116. package/dashboard/dist/assets/{agents-VbkdqWCr.js → agents-DlzvjNxE.js} +2 -2
  117. package/dashboard/dist/assets/{agents-VbkdqWCr.js.map → agents-DlzvjNxE.js.map} +1 -1
  118. package/dashboard/dist/assets/{bots-CsgYcYU_.js → bots-CWMN-KUI.js} +2 -2
  119. package/dashboard/dist/assets/{bots-CsgYcYU_.js.map → bots-CWMN-KUI.js.map} +1 -1
  120. package/dashboard/dist/assets/{capabilities-CMhFyoDl.js → capabilities-_WSZ7cxO.js} +2 -2
  121. package/dashboard/dist/assets/{capabilities-CMhFyoDl.js.map → capabilities-_WSZ7cxO.js.map} +1 -1
  122. package/dashboard/dist/assets/{controlplane-BstXtPbV.js → controlplane-_k9nWZy3.js} +2 -2
  123. package/dashboard/dist/assets/{controlplane-BstXtPbV.js.map → controlplane-_k9nWZy3.js.map} +1 -1
  124. package/dashboard/dist/assets/{escalation-C0BNAeIG.js → escalation-CK8ElYzD.js} +2 -2
  125. package/dashboard/dist/assets/{escalation-C0BNAeIG.js.map → escalation-CK8ElYzD.js.map} +1 -1
  126. package/dashboard/dist/assets/{escalation-columns-DajPgfuV.js → escalation-columns-Dyrf6Tdp.js} +2 -2
  127. package/dashboard/dist/assets/{escalation-columns-DajPgfuV.js.map → escalation-columns-Dyrf6Tdp.js.map} +1 -1
  128. package/dashboard/dist/assets/{index-BWLVqWLU.js → index-BiApvBEz.js} +2 -2
  129. package/dashboard/dist/assets/{index-BWLVqWLU.js.map → index-BiApvBEz.js.map} +1 -1
  130. package/dashboard/dist/assets/{index-D_6JFBiM.js → index-C3h0qGzG.js} +2 -2
  131. package/dashboard/dist/assets/{index-D_6JFBiM.js.map → index-C3h0qGzG.js.map} +1 -1
  132. package/dashboard/dist/assets/{index-fqU_l4dC.js → index-C5Yc3TT7.js} +2 -2
  133. package/dashboard/dist/assets/{index-fqU_l4dC.js.map → index-C5Yc3TT7.js.map} +1 -1
  134. package/dashboard/dist/assets/{index-Cl1vY8Lg.js → index-C7VXLKJs.js} +2 -2
  135. package/dashboard/dist/assets/{index-Cl1vY8Lg.js.map → index-C7VXLKJs.js.map} +1 -1
  136. package/dashboard/dist/assets/{index-CvFQtPWA.js → index-D8GcHxNJ.js} +2 -2
  137. package/dashboard/dist/assets/{index-CvFQtPWA.js.map → index-D8GcHxNJ.js.map} +1 -1
  138. package/dashboard/dist/assets/{index-CSixdp5W.js → index-Dc-Ye2wk.js} +2 -2
  139. package/dashboard/dist/assets/{index-CSixdp5W.js.map → index-Dc-Ye2wk.js.map} +1 -1
  140. package/dashboard/dist/assets/{index-DWlPFH3T.js → index-Dd4Si1Rl.js} +2 -2
  141. package/dashboard/dist/assets/{index-DWlPFH3T.js.map → index-Dd4Si1Rl.js.map} +1 -1
  142. package/dashboard/dist/assets/{index-C8k3Z2su.js → index-DwWP-Z_c.js} +3 -3
  143. package/dashboard/dist/assets/{index-C8k3Z2su.js.map → index-DwWP-Z_c.js.map} +1 -1
  144. package/dashboard/dist/assets/index-Ex5tEte8.js +5 -0
  145. package/dashboard/dist/assets/index-Ex5tEte8.js.map +1 -0
  146. package/dashboard/dist/assets/index-OV1JoB_P.js +6 -0
  147. package/dashboard/dist/assets/index-OV1JoB_P.js.map +1 -0
  148. package/dashboard/dist/assets/{index-BY5m8rhE.js → index-ZBj6Xgim.js} +2 -2
  149. package/dashboard/dist/assets/{index-BY5m8rhE.js.map → index-ZBj6Xgim.js.map} +1 -1
  150. package/dashboard/dist/assets/{index-CXnSqSFH.js → index-kKufOPLo.js} +2 -2
  151. package/dashboard/dist/assets/{index-CXnSqSFH.js.map → index-kKufOPLo.js.map} +1 -1
  152. package/dashboard/dist/assets/{index-CaSMWhLU.js → index-uUYXmvFf.js} +2 -2
  153. package/dashboard/dist/assets/{index-CaSMWhLU.js.map → index-uUYXmvFf.js.map} +1 -1
  154. package/dashboard/dist/assets/{knowledge-Bskl1MbC.js → knowledge-fYoEF4Lx.js} +2 -2
  155. package/dashboard/dist/assets/{knowledge-Bskl1MbC.js.map → knowledge-fYoEF4Lx.js.map} +1 -1
  156. package/dashboard/dist/assets/{mcp-C_qz25AB.js → mcp-1lzlgtS8.js} +2 -2
  157. package/dashboard/dist/assets/{mcp-C_qz25AB.js.map → mcp-1lzlgtS8.js.map} +1 -1
  158. package/dashboard/dist/assets/{mcp-query-CJx58s1A.js → mcp-query-nlrAz7m_.js} +2 -2
  159. package/dashboard/dist/assets/{mcp-query-CJx58s1A.js.map → mcp-query-nlrAz7m_.js.map} +1 -1
  160. package/dashboard/dist/assets/{pipelines-B6GzpgtG.js → pipelines-Ihi7EKXi.js} +2 -2
  161. package/dashboard/dist/assets/{pipelines-B6GzpgtG.js.map → pipelines-Ihi7EKXi.js.map} +1 -1
  162. package/dashboard/dist/assets/{roles-D8lPvFgG.js → roles-D4Wn7kE9.js} +2 -2
  163. package/dashboard/dist/assets/{roles-D8lPvFgG.js.map → roles-D4Wn7kE9.js.map} +1 -1
  164. package/dashboard/dist/assets/{tasks-BWHakSYf.js → tasks-Crk9Y3C4.js} +2 -2
  165. package/dashboard/dist/assets/{tasks-BWHakSYf.js.map → tasks-Crk9Y3C4.js.map} +1 -1
  166. package/dashboard/dist/assets/{topics-0dXzPX8t.js → topics-CtfcCyw9.js} +2 -2
  167. package/dashboard/dist/assets/{topics-0dXzPX8t.js.map → topics-CtfcCyw9.js.map} +1 -1
  168. package/dashboard/dist/assets/{useEventHooks-Dpixlgp-.js → useEventHooks-Dn_vN2xE.js} +2 -2
  169. package/dashboard/dist/assets/{useEventHooks-Dpixlgp-.js.map → useEventHooks-Dn_vN2xE.js.map} +1 -1
  170. package/dashboard/dist/assets/{useNamespace-DQMc77ok.js → useNamespace-Cfs3vKWk.js} +2 -2
  171. package/dashboard/dist/assets/{useNamespace-DQMc77ok.js.map → useNamespace-Cfs3vKWk.js.map} +1 -1
  172. package/dashboard/dist/assets/{useYamlActivityEvents-DO5bHOVE.js → useYamlActivityEvents-ymobxH47.js} +2 -2
  173. package/dashboard/dist/assets/{useYamlActivityEvents-DO5bHOVE.js.map → useYamlActivityEvents-ymobxH47.js.map} +1 -1
  174. package/dashboard/dist/assets/{users-B_LVbnUk.js → users-DngGIiC_.js} +2 -2
  175. package/dashboard/dist/assets/{users-B_LVbnUk.js.map → users-DngGIiC_.js.map} +1 -1
  176. package/dashboard/dist/assets/{workflows-WmmGDAvo.js → workflows-DuUspMAl.js} +2 -2
  177. package/dashboard/dist/assets/{workflows-WmmGDAvo.js.map → workflows-DuUspMAl.js.map} +1 -1
  178. package/dashboard/dist/assets/{yaml-workflows-BS7KHhU1.js → yaml-workflows-DXQCiOsP.js} +2 -2
  179. package/dashboard/dist/assets/{yaml-workflows-BS7KHhU1.js.map → yaml-workflows-DXQCiOsP.js.map} +1 -1
  180. package/dashboard/dist/index.html +1 -1
  181. package/docs/api/http/escalations.md +8 -4
  182. package/docs/api/http/roles.md +1 -1
  183. package/docs/api/http/workflows.md +1 -1
  184. package/docs/api/sdk/escalations.md +3 -3
  185. package/docs/api/sdk/workflows.md +1 -1
  186. package/docs/dashboard.md +2 -2
  187. package/docs/data.md +1 -1
  188. package/docs/hitl-guide.md +2 -3
  189. package/package.json +2 -1
  190. package/dashboard/dist/assets/OperationsPage--uVJxzmP.js +0 -2
  191. package/dashboard/dist/assets/index-BYf0oNSQ.js +0 -6
  192. package/dashboard/dist/assets/index-BYf0oNSQ.js.map +0 -1
  193. package/dashboard/dist/assets/index-BcaQ_hCz.js +0 -9
  194. package/dashboard/dist/assets/index-BcaQ_hCz.js.map +0 -1
@@ -32,34 +32,13 @@ var __importStar = (this && this.__importStar) || (function () {
32
32
  return result;
33
33
  };
34
34
  })();
35
- var __importDefault = (this && this.__importDefault) || function (mod) {
36
- return (mod && mod.__esModule) ? mod : { "default": mod };
37
- };
38
35
  Object.defineProperty(exports, "__esModule", { value: true });
39
36
  exports.createEscalation = createEscalation;
40
- const ajv_1 = __importDefault(require("ajv"));
41
37
  const escalationService = __importStar(require("../../services/escalation"));
42
38
  const roleService = __importStar(require("../../services/role"));
39
+ const schema_validator_1 = require("../../services/role/schema-validator");
43
40
  const escalation_1 = require("../../types/escalation");
44
41
  const helpers_1 = require("./helpers");
45
- // strict: false — role metadata_schemas use the project's house extension
46
- // keywords (x-lt-widget etc., the same style as seeded form_schemas); Ajv 8's
47
- // strict mode throws at compile on any unknown keyword, which would turn every
48
- // metadata-bearing create for that role into a 500.
49
- const ajv = new ajv_1.default({ allErrors: true, strict: false });
50
- // Compiled validators cached per role+version, keyed by the schema's serialized
51
- // form so an admin's schema edit invalidates the stale validator on the next
52
- // create. Pinned versions are immutable, but the same key logic covers both.
53
- const validatorCache = new Map();
54
- function compileRoleValidator(cacheId, schema) {
55
- const key = JSON.stringify(schema);
56
- const cached = validatorCache.get(cacheId);
57
- if (cached && cached.key === key)
58
- return cached.validate;
59
- const validate = ajv.compile(schema);
60
- validatorCache.set(cacheId, { key, validate });
61
- return validate;
62
- }
63
42
  // ── Create ────────────────────────────────────────────────────────────────
64
43
  /**
65
44
  * Create a standalone escalation (not tied to a workflow).
@@ -94,46 +73,26 @@ async function createEscalation(input, auth) {
94
73
  if (denied) {
95
74
  return { status: 403, error: `You must have write access to the "${role}" role or be a superadmin to create escalations for it` };
96
75
  }
97
- // Validate metadata against the role's declared schema (if any). A
98
- // metadata.schema_version pin selects that immutable snapshot; a pin that
99
- // names a missing version is a 400, never a silent fall-through to latest.
100
- if (input.metadata) {
101
- const pinned = input.metadata[escalation_1.ESCALATION_METADATA_KEYS.SCHEMA_VERSION];
102
- if (pinned !== undefined && (!Number.isInteger(pinned) || pinned < 1)) {
103
- return { status: 400, error: `metadata.${escalation_1.ESCALATION_METADATA_KEYS.SCHEMA_VERSION} must be a positive integer` };
104
- }
105
- let schema;
106
- if (pinned !== undefined) {
107
- const snapshot = await roleService.getRoleSchema(role, pinned);
108
- if (!snapshot) {
109
- return { status: 400, error: `Schema version ${pinned} does not exist for role "${role}"` };
110
- }
111
- schema = snapshot.metadata_schema;
76
+ // Validate the caller-supplied metadata against the role's metadata_schema
77
+ // (the queryable-facet contract). An escalation may carry a form-version pin
78
+ // (metadata.schema_version) chosen by the workflow author; that is a system
79
+ // key, not caller data, so it is excluded from validation. No version is
80
+ // stamped here — omitting it means the resolve UI renders the role's latest
81
+ // form at fetch time.
82
+ const metadata = input.metadata ?? {};
83
+ const metadataSchema = await roleService.getRoleMetadataSchema(role);
84
+ if (metadataSchema) {
85
+ let validate;
86
+ try {
87
+ validate = (0, schema_validator_1.compileSchemaValidator)(`${role}:meta`, metadataSchema);
112
88
  }
113
- else {
114
- schema = await roleService.getRoleMetadataSchema(role);
89
+ catch (compileErr) {
90
+ // A broken stored schema is the role admin's bug, not the creator's.
91
+ return { status: 422, error: `metadata_schema for role "${role}" is not a valid JSON Schema: ${compileErr.message}` };
115
92
  }
116
- if (schema) {
117
- let validate;
118
- try {
119
- validate = compileRoleValidator(`${role}@${pinned ?? 'live'}`, schema);
120
- }
121
- catch (compileErr) {
122
- // A broken stored schema is the role admin's bug, not the creator's —
123
- // name it explicitly instead of surfacing an opaque 500.
124
- return {
125
- status: 422,
126
- error: `metadata_schema for role "${role}" is not a valid JSON Schema: ${compileErr.message}`,
127
- };
128
- }
129
- // The pin is a system key, not caller data — strip it so schemas with
130
- // additionalProperties: false still validate the caller's bag.
131
- const { [escalation_1.ESCALATION_METADATA_KEYS.SCHEMA_VERSION]: _pin, ...callerMetadata } = input.metadata;
132
- const valid = validate(callerMetadata);
133
- if (!valid) {
134
- const msgs = (validate.errors ?? []).map((e) => `${e.instancePath || '(root)'} ${e.message}`).join('; ');
135
- return { status: 400, error: `metadata does not match the role schema: ${msgs}` };
136
- }
93
+ const { [escalation_1.ESCALATION_METADATA_KEYS.SCHEMA_VERSION]: _pin, ...callerMetadata } = metadata;
94
+ if (!validate(callerMetadata)) {
95
+ return { status: 400, error: `metadata does not match the role schema: ${(0, schema_validator_1.formatValidationErrors)(validate)}` };
137
96
  }
138
97
  }
139
98
  const escalation = await escalationService.createEscalation({
@@ -60,7 +60,7 @@ const helpers_1 = require("./helpers");
60
60
  */
61
61
  async function resolveEscalation(input, auth) {
62
62
  try {
63
- const { id, resolverPayload, metadata } = input;
63
+ const { id, metadata, resolverPayload } = input;
64
64
  if (!resolverPayload) {
65
65
  return { status: 400, error: 'resolverPayload is required' };
66
66
  }
@@ -82,6 +82,9 @@ async function resolveEscalation(input, auth) {
82
82
  const denied = await (0, helpers_1.assertWriteAccess)(auth.userId, escalation);
83
83
  if (denied)
84
84
  return denied;
85
+ // The resolver payload is stored exactly as submitted — its shape is the
86
+ // workflow's contract, formed by the caller (the React app maps the form to
87
+ // the payload via x-lt-bind before submitting). No server-side transform.
85
88
  // `metadata` (the outcome patch) is never written separately — it rides WITH the
86
89
  // resolverPayload to whichever path performs the resolve, so it merges inside the
87
90
  // single status='pending'-guarded UPDATE (HotMesh `resolve(metadata)`). Paths that
@@ -133,7 +136,7 @@ async function resolveEscalation(input, auth) {
133
136
  */
134
137
  async function resolveBySignalKey(input, auth) {
135
138
  try {
136
- const { signalKey, resolverPayload, metadata } = input;
139
+ const { signalKey, metadata, resolverPayload } = input;
137
140
  if (!signalKey)
138
141
  return { status: 400, error: 'signalKey is required' };
139
142
  if (!resolverPayload)
@@ -147,6 +150,8 @@ async function resolveBySignalKey(input, auth) {
147
150
  const denied = await (0, helpers_1.assertWriteAccess)(auth.userId, escalation);
148
151
  if (denied)
149
152
  return { status: 404, error: 'Escalation not found' };
153
+ // The payload is delivered as the signal the parked workflow's condition()
154
+ // receives — stored exactly as submitted (the caller formed its shape).
150
155
  return resolveViaSignalKey(escalation, resolverPayload, metadata);
151
156
  }
152
157
  catch (err) {
@@ -166,12 +171,15 @@ async function resolveBySignalKey(input, auth) {
166
171
  */
167
172
  async function resolveByIds(input, auth) {
168
173
  try {
169
- const { ids, resolverPayload, metadata } = input;
174
+ const { ids, metadata } = input;
170
175
  if (!Array.isArray(ids) || ids.length === 0) {
171
176
  return { status: 400, error: 'ids must be a non-empty array' };
172
177
  }
173
- if (!resolverPayload)
178
+ if (!input.resolverPayload)
174
179
  return { status: 400, error: 'resolverPayload is required' };
180
+ // One pre-shaped payload for a homogeneous group of bookkeeping rows, stored
181
+ // as submitted — the caller forms the payload's shape.
182
+ const resolverPayload = input.resolverPayload;
175
183
  // Bulk resolve ("ack") is a write verb, gated PER ITEM: write_all roles may
176
184
  // resolve any item in the role; write_self roles may resolve only items assigned
177
185
  // to the caller (same rule single-resolve applies via assertWriteAccess). Global
@@ -51,14 +51,18 @@ const helpers_1 = require("./helpers");
51
51
  */
52
52
  async function getEscalation(input, auth) {
53
53
  try {
54
- const escalation = await escalationService.getEscalation(input.id);
55
- if (!escalation) {
54
+ // One query: the escalation plus its role's form, resolved to the version the
55
+ // row pinned (or the role's latest) and embedded as `form_schema`. The resolve
56
+ // UI renders from this without a second schema call, and the form is never
57
+ // stored on the row — it is JOINed in from the roles tables.
58
+ const detail = await escalationService.getEscalationWithFormSchema(input.id);
59
+ if (!detail) {
56
60
  return { status: 404, error: 'Escalation not found' };
57
61
  }
58
- const denied = await (0, helpers_1.assertReadAccess)(auth.userId, escalation);
62
+ const denied = await (0, helpers_1.assertReadAccess)(auth.userId, detail.escalation);
59
63
  if (denied)
60
64
  return denied;
61
- return { status: 200, data: escalation };
65
+ return { status: 200, data: { ...detail.escalation, form_schema: detail.form_schema } };
62
66
  }
63
67
  catch (err) {
64
68
  return { status: 500, error: err.message };
@@ -83,6 +83,7 @@ export declare function updateRole(input: {
83
83
  role: string;
84
84
  title?: string | null;
85
85
  description?: string | null;
86
+ /** The escalation resolve FORM schema (the JIT UI). Fields may carry x-lt-bind. */
86
87
  form_schema?: Record<string, any> | null;
87
88
  metadata_schema?: Record<string, any> | null;
88
89
  properties?: Record<string, any> | null;
@@ -0,0 +1,44 @@
1
+ /**
2
+ * twin-farm — drive the printer-twin example end to end, effortlessly.
3
+ *
4
+ * Spins up the whole DIGITAL side of the physical/digital twin farm from one
5
+ * command and plays every human part automatically, so a single run exercises
6
+ * the full lifecycle: onboard (register + bind) → advertise → dispatch → print →
7
+ * reconcile → settle, plus the failure detours (autonomous fail → inspect, and
8
+ * filament runout → change → resume). The PHYSICAL side is the app's Bambu
9
+ * backend — `BAMBU_BACKEND=mock` by default (a deterministic in-repo simulation),
10
+ * or `http` against a real Farm Manager Server once the office link is wired.
11
+ *
12
+ * Faults are injected deterministically via each order unit's `simOutcome` (the
13
+ * mock honors it), so the service escalations fire on demand — no racing.
14
+ *
15
+ * Usage:
16
+ * npm run twin:farm
17
+ * FLEET=6 ORDERS=10 UNITS=2 npm run twin:farm
18
+ * FAIL_PCT=30 FAIL_MODE=filament_runout npm run twin:farm # fault → service → recover
19
+ * FLEET=4 ORDERS=4 KEEP=1 npm run twin:farm # leave twins running to poke in the UI
20
+ *
21
+ * Env vars:
22
+ * BASE_URL long-tail API (default http://localhost:3000)
23
+ * FLEET printer twins to launch (default 4)
24
+ * ORDERS orders to place (default 6)
25
+ * UNITS max units per order (default 2; capped to the fleet that
26
+ * shares an order's filament, so every order is satisfiable)
27
+ * FILAMENTS comma-separated filament pool the fleet + orders draw from
28
+ * (default "pla,petg")
29
+ * ARRIVAL_S average seconds between order arrivals (default 2)
30
+ * FAIL_PCT percent of orders with one faulted unit (default 0)
31
+ * FAIL_MODE the injected fault: filament_runout | failed (default filament_runout)
32
+ * POLL_MS servicer/registrar poll interval ms (default 750)
33
+ * BROKER_IDLE idle broker ticks before it self-terminates (default 40)
34
+ * RUN_BROKER launch a broker (default 1; set 0 if one already runs)
35
+ * KEEP 1 = leave twins advertising at the end (default 0 = retire
36
+ * them with a power-down so nothing lingers between runs)
37
+ *
38
+ * Capabilities: each twin is registered with a capability profile (xl/pdac/soft).
39
+ * By default the fleet is fully capable and only filament routes; set FLEET_CAPS
40
+ * to vary it (e.g. "xl,soft|pdac|full|full" — one spec per printer, pipe-separated,
41
+ * each a comma list of capabilities or "full"/"base"). Orders only ever require a
42
+ * capability the fleet can satisfy for the chosen filament and unit count.
43
+ */
44
+ export {};
@@ -0,0 +1,427 @@
1
+ "use strict";
2
+ /**
3
+ * twin-farm — drive the printer-twin example end to end, effortlessly.
4
+ *
5
+ * Spins up the whole DIGITAL side of the physical/digital twin farm from one
6
+ * command and plays every human part automatically, so a single run exercises
7
+ * the full lifecycle: onboard (register + bind) → advertise → dispatch → print →
8
+ * reconcile → settle, plus the failure detours (autonomous fail → inspect, and
9
+ * filament runout → change → resume). The PHYSICAL side is the app's Bambu
10
+ * backend — `BAMBU_BACKEND=mock` by default (a deterministic in-repo simulation),
11
+ * or `http` against a real Farm Manager Server once the office link is wired.
12
+ *
13
+ * Faults are injected deterministically via each order unit's `simOutcome` (the
14
+ * mock honors it), so the service escalations fire on demand — no racing.
15
+ *
16
+ * Usage:
17
+ * npm run twin:farm
18
+ * FLEET=6 ORDERS=10 UNITS=2 npm run twin:farm
19
+ * FAIL_PCT=30 FAIL_MODE=filament_runout npm run twin:farm # fault → service → recover
20
+ * FLEET=4 ORDERS=4 KEEP=1 npm run twin:farm # leave twins running to poke in the UI
21
+ *
22
+ * Env vars:
23
+ * BASE_URL long-tail API (default http://localhost:3000)
24
+ * FLEET printer twins to launch (default 4)
25
+ * ORDERS orders to place (default 6)
26
+ * UNITS max units per order (default 2; capped to the fleet that
27
+ * shares an order's filament, so every order is satisfiable)
28
+ * FILAMENTS comma-separated filament pool the fleet + orders draw from
29
+ * (default "pla,petg")
30
+ * ARRIVAL_S average seconds between order arrivals (default 2)
31
+ * FAIL_PCT percent of orders with one faulted unit (default 0)
32
+ * FAIL_MODE the injected fault: filament_runout | failed (default filament_runout)
33
+ * POLL_MS servicer/registrar poll interval ms (default 750)
34
+ * BROKER_IDLE idle broker ticks before it self-terminates (default 40)
35
+ * RUN_BROKER launch a broker (default 1; set 0 if one already runs)
36
+ * KEEP 1 = leave twins advertising at the end (default 0 = retire
37
+ * them with a power-down so nothing lingers between runs)
38
+ *
39
+ * Capabilities: each twin is registered with a capability profile (xl/pdac/soft).
40
+ * By default the fleet is fully capable and only filament routes; set FLEET_CAPS
41
+ * to vary it (e.g. "xl,soft|pdac|full|full" — one spec per printer, pipe-separated,
42
+ * each a comma list of capabilities or "full"/"base"). Orders only ever require a
43
+ * capability the fleet can satisfy for the chosen filament and unit count.
44
+ */
45
+ Object.defineProperty(exports, "__esModule", { value: true });
46
+ try {
47
+ require('dotenv/config');
48
+ }
49
+ catch { /* dotenv optional */ }
50
+ const BASE_URL = process.env.BASE_URL || 'http://localhost:3000';
51
+ const FLEET = parseInt(process.env.FLEET || '4', 10);
52
+ const ORDERS = parseInt(process.env.ORDERS || '6', 10);
53
+ const UNITS = parseInt(process.env.UNITS || '2', 10);
54
+ const FILAMENTS = (process.env.FILAMENTS || 'pla,petg').split(',').map((f) => f.trim()).filter(Boolean);
55
+ const ARRIVAL_S = parseFloat(process.env.ARRIVAL_S || '2');
56
+ const FAIL_PCT = parseInt(process.env.FAIL_PCT || '0', 10);
57
+ const FAIL_MODE = (process.env.FAIL_MODE || 'filament_runout');
58
+ const POLL_MS = parseInt(process.env.POLL_MS || '750', 10);
59
+ const BROKER_IDLE = parseInt(process.env.BROKER_IDLE || '40', 10);
60
+ const RUN_BROKER = process.env.RUN_BROKER !== '0';
61
+ const KEEP = process.env.KEEP === '1';
62
+ const CAPABILITY_KEYS = ['xl', 'pdac', 'soft'];
63
+ const MODELS = ['Fab-X1', 'Fab-X1C', 'Fab-P2', 'Fab-Mini'];
64
+ let token = '';
65
+ let userId = '';
66
+ let done = false;
67
+ const stats = { registered: 0, dispatched: 0, printed: 0, faulted: 0, serviced: 0, settled: 0 };
68
+ function ts() { return new Date().toISOString().slice(11, 19); }
69
+ function sleep(ms) { return new Promise((r) => setTimeout(r, ms)); }
70
+ function ageSeconds(iso) { return (Date.now() - new Date(iso).getTime()) / 1000; }
71
+ // Deterministic per-item jitter — FNV-1a hash of a key into [0, 1). Stable
72
+ // across polls, so a job's fail/no-fail verdict never flip-flops mid-run.
73
+ function jitter(key, salt = '') {
74
+ let h = 2166136261;
75
+ for (const c of salt + key) {
76
+ h ^= c.charCodeAt(0);
77
+ h = Math.imul(h, 16777619);
78
+ }
79
+ return ((h >>> 0) % 1000) / 1000;
80
+ }
81
+ function spread(base, r) { return base * (0.4 + 1.2 * r); }
82
+ // ── HTTP + auth (mirrors ortho-populate) ──────────────────────────────────────
83
+ async function api(method, path, body) {
84
+ const res = await fetch(`${BASE_URL}${path}`, {
85
+ method,
86
+ headers: { 'Content-Type': 'application/json', ...(token ? { Authorization: `Bearer ${token}` } : {}) },
87
+ body: body !== undefined ? JSON.stringify(body) : undefined,
88
+ });
89
+ const data = await res.json().catch(() => ({}));
90
+ if (!res.ok)
91
+ throw new Error(`${method} ${path} → ${res.status}: ${JSON.stringify(data)}`);
92
+ return data;
93
+ }
94
+ async function login() {
95
+ const auth = await api('POST', '/api/auth/login', { username: 'superadmin', password: 'l0ngt@1l' });
96
+ token = auth.token;
97
+ userId = auth.user?.id || '';
98
+ if (!token) {
99
+ console.error('[auth] Login failed');
100
+ process.exit(1);
101
+ }
102
+ console.log(`[${ts()}] Logged in (userId ${userId.slice(0, 8)}…)`);
103
+ }
104
+ // ── Pace Board role config (idempotent — never clobbers admin tuning) ─────────
105
+ /**
106
+ * Ensure the twin farm's roles are operationalized on the Pace Board. The
107
+ * startup seeder (examples/seed-twin.ts) normally owns this; the driver repeats
108
+ * it so `npm run twin:farm` populates the board even against an app that did not
109
+ * seed the examples. Only UNCONFIGURED roles (no title) are touched — a role an
110
+ * admin has tuned keeps its dials.
111
+ *
112
+ * Topology: print-jobs ─▶ printer-fleet (production line), with print-servicer
113
+ * a side-quest root merging into the fleet.
114
+ */
115
+ const TWIN_ROLE_CONFIG = [
116
+ { role: 'print-jobs', title: 'Print Jobs', parent_role: null, sla_minutes: 5, target_per_hour: 30, priority_threshold_minutes: 5, description: 'Order demand — one print-job escalation per unit, claimed as a set.' },
117
+ { role: 'printer-fleet', title: 'Printer Fleet', parent_role: 'print-jobs', sla_minutes: 5, target_per_hour: 30, priority_threshold_minutes: 5, description: 'Twin availability adverts and in-flight print rows — the machines at work.', upstream_roles: ['print-onboarder', 'print-servicer'] },
118
+ { role: 'print-onboarder', title: 'Print Onboarder', parent_role: null, sla_minutes: 15, target_per_hour: 5, priority_threshold_minutes: 15, description: 'Register + bind a newly unboxed machine.' },
119
+ { role: 'print-servicer', title: 'Print Servicer', parent_role: null, sla_minutes: 10, target_per_hour: 10, priority_threshold_minutes: 10, description: 'Reload filament, inspect failures, restore offline machines.' },
120
+ ];
121
+ async function ensureTwinRoles() {
122
+ const details = await api('GET', '/api/roles/details').catch(() => ({ roles: [] }));
123
+ const configured = new Map((details?.roles ?? []).map((r) => [r.role, r]));
124
+ // Create bare rows first — upstream_roles references must resolve.
125
+ for (const cfg of TWIN_ROLE_CONFIG) {
126
+ if (!configured.has(cfg.role))
127
+ await api('POST', '/api/roles', { role: cfg.role }).catch(() => { });
128
+ }
129
+ let applied = 0;
130
+ for (const cfg of TWIN_ROLE_CONFIG) {
131
+ const row = configured.get(cfg.role);
132
+ if (row?.title)
133
+ continue; // already configured — respect admin tuning
134
+ const { role, ...body } = cfg;
135
+ await api('PATCH', `/api/roles/${role}`, { ...body, ops_visible: true }).then(() => { applied++; }).catch((err) => {
136
+ console.warn(`[${ts()}] ⚠ role ${role}: ${String(err.message).slice(0, 80)}`);
137
+ });
138
+ }
139
+ console.log(`[${ts()}] Pace Board roles ready (print-jobs → printer-fleet, print-servicer side)${applied ? ` — configured ${applied}` : ''}`);
140
+ }
141
+ function parseCapSpec(spec) {
142
+ const s = spec.trim().toLowerCase();
143
+ if (s === 'full' || s === '')
144
+ return { xl: true, pdac: true, soft: true };
145
+ if (s === 'base' || s === 'none')
146
+ return { xl: false, pdac: false, soft: false };
147
+ const set = { xl: false, pdac: false, soft: false };
148
+ for (const k of s.split(',').map((x) => x.trim())) {
149
+ if (CAPABILITY_KEYS.includes(k))
150
+ set[k] = true;
151
+ }
152
+ return set;
153
+ }
154
+ function planFleet(batchTag) {
155
+ const capSpecs = (process.env.FLEET_CAPS || '').split('|').map((s) => s.trim()).filter(Boolean);
156
+ return Array.from({ length: FLEET }, (_, i) => {
157
+ const filament = FILAMENTS[i % FILAMENTS.length];
158
+ const caps = capSpecs.length ? parseCapSpec(capSpecs[i % capSpecs.length]) : { xl: true, pdac: true, soft: true };
159
+ const printerId = `${batchTag}-p${i}`;
160
+ return { printerId, wfId: printerId, filament, caps, model: MODELS[i % MODELS.length], serialNumber: `SN-${batchTag.slice(-6)}-${i}` };
161
+ });
162
+ }
163
+ /** Evenly distribute FAIL_PCT across the order sequence (deterministic). */
164
+ function hasFault(i) {
165
+ return Math.floor(((i + 1) * FAIL_PCT) / 100) > Math.floor((i * FAIL_PCT) / 100);
166
+ }
167
+ function planOrders(batchTag, fleet) {
168
+ return Array.from({ length: ORDERS }, (_, i) => {
169
+ const filament = FILAMENTS[i % FILAMENTS.length];
170
+ const pool = fleet.filter((t) => t.filament === filament);
171
+ const units = Math.max(1, Math.min(UNITS, pool.length));
172
+ // Require a rotating capability only if enough printers of this filament have
173
+ // it — otherwise the order would never place. Proves capability routing when
174
+ // the fleet varies; a no-op (but still-threaded) facet when it's fully capable.
175
+ const cap = CAPABILITY_KEYS[i % CAPABILITY_KEYS.length];
176
+ const capable = pool.filter((t) => t.caps[cap]).length;
177
+ const require = (i % 2 === 0 && capable >= units) ? { [cap]: true } : {};
178
+ const fault = hasFault(i) ? { unit: 0, mode: FAIL_MODE } : undefined;
179
+ return { index: i, wfId: `${batchTag}-o${i}`, orderId: `TWIN-${batchTag.slice(-6)}-${i}`, filament, require, units, fault };
180
+ });
181
+ }
182
+ // ── Launch twins + broker + orders ────────────────────────────────────────────
183
+ async function startTwin(spec) {
184
+ await api('POST', '/api/workflows/printerTwin/invoke', {
185
+ workflowId: spec.wfId,
186
+ data: { printerId: spec.printerId, operatorId: userId },
187
+ metadata: { source: 'twin-farm' },
188
+ });
189
+ }
190
+ async function startBroker(batchTag) {
191
+ const wfId = `${batchTag}-broker`;
192
+ await api('POST', '/api/workflows/twinBroker/invoke', {
193
+ workflowId: wfId,
194
+ data: { brokerId: userId, tickSeconds: 1, idleTickSeconds: 3, maxIdleRuns: BROKER_IDLE },
195
+ metadata: { source: 'twin-farm' },
196
+ });
197
+ return wfId;
198
+ }
199
+ async function placeOrder(spec) {
200
+ await api('POST', '/api/workflows/twinOrder/invoke', {
201
+ workflowId: spec.wfId,
202
+ data: {
203
+ orderId: spec.orderId,
204
+ filament: spec.filament,
205
+ require: spec.require,
206
+ units: Array.from({ length: spec.units }, (_, u) => ({
207
+ gcodeUrl: `https://example.com/${spec.orderId}-u${u}.gcode`,
208
+ ...(spec.fault && spec.fault.unit === u ? { simOutcome: spec.fault.mode } : {}),
209
+ })),
210
+ operatorId: userId,
211
+ },
212
+ metadata: { source: 'twin-farm' },
213
+ });
214
+ }
215
+ // ── Human automation: the print-servicer ──────────────────────────────────────
216
+ /** Resolve registration escalations — the servicer bringing a machine online. */
217
+ async function registerLoop(fleet) {
218
+ while (!done) {
219
+ try {
220
+ const resp = await api('GET', '/api/escalations/available?role=print-onboarder&subtype=registering&limit=100');
221
+ for (const esc of resp?.escalations ?? []) {
222
+ const spec = fleet.get(String(esc.metadata?.printerId ?? ''));
223
+ if (!spec)
224
+ continue;
225
+ await api('POST', `/api/escalations/${esc.id}/resolve`, {
226
+ resolverPayload: {
227
+ serialNumber: spec.serialNumber,
228
+ model: spec.model,
229
+ manufactureDate: '2026-01-15',
230
+ filament: spec.filament,
231
+ certifications: 'CE, RoHS',
232
+ xl: spec.caps.xl, pdac: spec.caps.pdac, soft: spec.caps.soft,
233
+ notes: 'Auto-registered by twin-farm',
234
+ },
235
+ }).then(() => {
236
+ stats.registered++;
237
+ const c = CAPABILITY_KEYS.filter((k) => spec.caps[k]).join('+') || 'base';
238
+ console.log(`[${ts()}] [register] ${spec.printerId} — ${spec.model} ${spec.filament} (${c}) [${stats.registered}/${FLEET}]`);
239
+ }).catch(() => { });
240
+ }
241
+ }
242
+ catch (err) {
243
+ logPollErr('register', err);
244
+ }
245
+ if (!done)
246
+ await sleep(POLL_MS);
247
+ }
248
+ }
249
+ /** The servicer's resolution for each escalation subtype the twin raises. Reloads
250
+ * the SAME filament the machine had (from the escalation metadata) so a service
251
+ * visit restores the machine rather than repurposing it. */
252
+ function servicePayload(esc) {
253
+ const subtype = esc.subtype;
254
+ if (subtype === 'filament_change')
255
+ return { filamentLoaded: esc.metadata?.filament || 'pla', notes: 'Reloaded by twin-farm' };
256
+ if (subtype === 'failure_inspect')
257
+ return { action: 'reset', notes: 'Inspected by twin-farm' };
258
+ if (subtype === 'offline_investigate')
259
+ return { action: 'checked', notes: 'Reconnected by twin-farm' };
260
+ return { action: 'restored', notes: 'Auto-serviced by twin-farm' };
261
+ }
262
+ /** Resolve every servicer escalation (except registration) — filament changes,
263
+ * failure inspections, offline investigations — the way a technician would. */
264
+ async function serviceLoop() {
265
+ while (!done) {
266
+ try {
267
+ const resp = await api('GET', '/api/escalations/available?role=print-servicer&limit=100');
268
+ for (const esc of resp?.escalations ?? []) {
269
+ await api('POST', `/api/escalations/${esc.id}/resolve`, {
270
+ resolverPayload: servicePayload(esc),
271
+ }).then(() => {
272
+ stats.serviced++;
273
+ console.log(`[${ts()}] [service] ${esc.subtype.padEnd(18)} ${esc.metadata?.printerId ?? esc.id.slice(0, 8)} [${stats.serviced}]`);
274
+ }).catch(() => { });
275
+ }
276
+ }
277
+ catch (err) {
278
+ logPollErr('service', err);
279
+ }
280
+ if (!done)
281
+ await sleep(POLL_MS);
282
+ }
283
+ }
284
+ let lastPollErr = '';
285
+ function logPollErr(loop, err) {
286
+ const msg = `[${loop}] ${String(err?.message).slice(0, 80)}`;
287
+ if (msg !== lastPollErr) {
288
+ console.warn(`[${ts()}] ⚠ ${msg}`);
289
+ lastPollErr = msg;
290
+ }
291
+ }
292
+ // ── Order arrival + settlement watch ──────────────────────────────────────────
293
+ async function orderLoop(orders) {
294
+ for (const order of orders) {
295
+ if (done)
296
+ break;
297
+ try {
298
+ await placeOrder(order);
299
+ const req = Object.keys(order.require).length ? ` require:${Object.keys(order.require).join('+')}` : '';
300
+ console.log(`[${ts()}] [order] ${order.index + 1}/${orders.length} ${order.orderId} — ${order.filament} ×${order.units}${req}`);
301
+ }
302
+ catch (err) {
303
+ console.error(`[${ts()}] [order] ${order.orderId} FAILED: ${String(err.message).slice(0, 100)}`);
304
+ }
305
+ await sleep(spread(ARRIVAL_S, jitter(order.wfId, 'arrive')) * 1000);
306
+ }
307
+ }
308
+ /** Poll each order's result; a settled order returns its outcomes. */
309
+ async function settlementWatch(orders) {
310
+ const settled = new Set();
311
+ let stalled = 0;
312
+ // Poll-driven twins add latency per order (poll pickup + print + report), and
313
+ // a fault adds a service round-trip, so the broker needs generous time to
314
+ // converge (it retries placement every tick via release-on-skip). This is a
315
+ // no-progress watchdog, not a deadline — steady settling keeps resetting it.
316
+ const STALL_S = Math.max(60, ORDERS * (ARRIVAL_S + 14));
317
+ while (!done) {
318
+ await sleep(1000);
319
+ let progressed = false;
320
+ for (const order of orders) {
321
+ if (settled.has(order.wfId))
322
+ continue;
323
+ try {
324
+ const resp = await api('GET', `/api/workflows/${order.wfId}/result`);
325
+ const data = resp?.result?.data;
326
+ if (data && data.completedAt !== undefined) {
327
+ settled.add(order.wfId);
328
+ progressed = true;
329
+ stats.settled++;
330
+ const outcomes = data.outcomes ?? [];
331
+ const ok = outcomes.filter((o) => o.outcome === 'success').length;
332
+ const bad = outcomes.length - ok;
333
+ stats.printed += ok;
334
+ stats.faulted += bad;
335
+ stats.dispatched += outcomes.length;
336
+ console.log(`[${ts()}] [settle] ${order.orderId} — ${ok} printed${bad ? `, ${bad} faulted` : ''} [${stats.settled}/${ORDERS}]`);
337
+ }
338
+ }
339
+ catch { /* 202 still running */ }
340
+ }
341
+ if (stats.settled >= ORDERS) {
342
+ done = true;
343
+ break;
344
+ }
345
+ stalled = progressed ? 0 : stalled + 1;
346
+ if (stalled && stalled % 10 === 0)
347
+ console.log(`[${ts()}] … ${stats.settled}/${ORDERS} settled — waiting (${stalled}s quiet)`);
348
+ if (stalled >= STALL_S) {
349
+ console.warn(`[${ts()}] ⚠ stalled ${stalled}s at ${stats.settled}/${ORDERS} — stopping`);
350
+ done = true;
351
+ break;
352
+ }
353
+ }
354
+ }
355
+ // ── Teardown: retire idle twins so nothing lingers ────────────────────────────
356
+ async function retireTwins(fleet) {
357
+ if (KEEP) {
358
+ console.log(`[${ts()}] KEEP=1 — leaving ${fleet.length} twins advertising`);
359
+ return;
360
+ }
361
+ if (stats.settled < ORDERS) {
362
+ console.log(`[${ts()}] ${stats.settled}/${ORDERS} settled — leaving twins up so the broker can finish (rerun with KEEP=1 to keep them)`);
363
+ return;
364
+ }
365
+ await sleep(2000); // let any last twin re-advertise after a service visit
366
+ let retired = 0;
367
+ for (let sweep = 0; sweep < 4 && retired < fleet.length; sweep++) {
368
+ try {
369
+ const resp = await api('GET', '/api/escalations/available?role=printer-fleet&subtype=ready&limit=200');
370
+ for (const esc of resp?.escalations ?? []) {
371
+ try {
372
+ await api('POST', `/api/escalations/${esc.id}/resolve`, { resolverPayload: { powerdown: true } });
373
+ retired++;
374
+ }
375
+ catch { /* claimed or already gone */ }
376
+ }
377
+ }
378
+ catch { /* ignore */ }
379
+ if (retired < fleet.length)
380
+ await sleep(1500);
381
+ }
382
+ console.log(`[${ts()}] Retired ${retired} idle twin${retired === 1 ? '' : 's'}`);
383
+ }
384
+ // ── Main ──────────────────────────────────────────────────────────────────────
385
+ async function main() {
386
+ const runId = Math.floor(Date.now() / 1000).toString();
387
+ const batchTag = `twin-${runId}`;
388
+ const fleet = planFleet(batchTag);
389
+ const fleetById = new Map(fleet.map((t) => [t.printerId, t]));
390
+ console.log(`\n Twin Farm ── ${FLEET} printers, ${ORDERS} orders (≤${UNITS} units), filaments [${FILAMENTS.join(', ')}]`);
391
+ console.log(` arrival=${ARRIVAL_S}s fail=${FAIL_PCT}% (${FAIL_MODE}) broker=${RUN_BROKER ? `on (idle≤${BROKER_IDLE})` : 'external'} keep=${KEEP}`);
392
+ console.log(` Server: ${BASE_URL}\n`);
393
+ await login();
394
+ await ensureTwinRoles();
395
+ // 1. Launch the fleet — each twin onboards (register → bind) then advertises.
396
+ console.log(`\n[${ts()}] Launching ${FLEET} twins …`);
397
+ for (const spec of fleet)
398
+ await startTwin(spec);
399
+ // 2. Register + service loops start now so twins come online as they appear.
400
+ const humanLoops = [registerLoop(fleetById), serviceLoop()];
401
+ // 3. Broker (once the humans are watching).
402
+ if (RUN_BROKER) {
403
+ await startBroker(batchTag);
404
+ console.log(`[${ts()}] Broker running`);
405
+ }
406
+ // 4. Wait for the fleet to register before placing demand (so orders place).
407
+ const registerDeadline = Date.now() + 30_000;
408
+ while (stats.registered < FLEET && Date.now() < registerDeadline && !done)
409
+ await sleep(500);
410
+ if (stats.registered < FLEET)
411
+ console.warn(`[${ts()}] ⚠ only ${stats.registered}/${FLEET} registered — placing orders anyway`);
412
+ // 5. Orders arrive; settlement watch drives the run to completion.
413
+ const orders = planOrders(batchTag, fleet);
414
+ console.log(`\n[${ts()}] Placing ${ORDERS} orders …\n`);
415
+ const startMs = Date.now();
416
+ await Promise.all([orderLoop(orders), settlementWatch(orders), ...humanLoops.map((p) => p.catch(() => { }))]);
417
+ const elapsed = ((Date.now() - startMs) / 1000).toFixed(1);
418
+ await retireTwins(fleet);
419
+ console.log(`\n[${ts()}] Done in ${elapsed}s`);
420
+ console.log(` Registered: ${stats.registered}/${FLEET}`);
421
+ console.log(` Orders: ${stats.settled}/${ORDERS} settled`);
422
+ console.log(` Units: ${stats.printed} printed${stats.faulted ? `, ${stats.faulted} faulted` : ''} (${stats.dispatched} dispatched)`);
423
+ console.log(` Serviced: ${stats.serviced}`);
424
+ console.log(`\n Dashboard: ${BASE_URL}/escalations\n`);
425
+ process.exit(0);
426
+ }
427
+ main().catch((err) => { console.error(err); process.exit(1); });