@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.
- package/build/api/escalations/create.js +19 -60
- package/build/api/escalations/resolve.js +12 -4
- package/build/api/escalations/single.js +8 -4
- package/build/api/roles.d.ts +1 -0
- package/build/scripts/twin-farm.d.ts +44 -0
- package/build/scripts/twin-farm.js +427 -0
- package/build/services/escalation/queries.d.ts +13 -0
- package/build/services/escalation/queries.js +27 -0
- package/build/services/orchestrator/condition.d.ts +18 -12
- package/build/services/orchestrator/condition.js +11 -8
- package/build/services/role/index.d.ts +1 -2
- package/build/services/role/index.js +1 -2
- package/build/services/role/schema-validator.d.ts +9 -0
- package/build/services/role/schema-validator.js +42 -0
- package/build/services/role/sql.d.ts +1 -1
- package/build/services/role/sql.js +2 -2
- package/build/services/role/types.d.ts +8 -6
- package/build/system/mcp-servers/admin/schemas.js +4 -2
- package/build/system/mcp-servers/human-queue-schemas.js +6 -2
- package/build/system/mcp-servers/human-queue.js +11 -3
- package/build/tsconfig.tsbuildinfo +1 -1
- package/build/types/config.d.ts +2 -0
- package/build/types/startup.d.ts +5 -1
- package/dashboard/dist/assets/{AdminDashboard-Nvja-XIJ.js → AdminDashboard-B-JptT54.js} +2 -2
- package/dashboard/dist/assets/{AdminDashboard-Nvja-XIJ.js.map → AdminDashboard-B-JptT54.js.map} +1 -1
- package/dashboard/dist/assets/{AgentConfigPage-Sf2god7q.js → AgentConfigPage-B1Jc6iJk.js} +2 -2
- package/dashboard/dist/assets/{AgentConfigPage-Sf2god7q.js.map → AgentConfigPage-B1Jc6iJk.js.map} +1 -1
- package/dashboard/dist/assets/{AgentDetailPage-DtgDeZJR.js → AgentDetailPage-CWpclCOY.js} +2 -2
- package/dashboard/dist/assets/{AgentDetailPage-DtgDeZJR.js.map → AgentDetailPage-CWpclCOY.js.map} +1 -1
- package/dashboard/dist/assets/{AgentsPage-C6zqP_xC.js → AgentsPage-DyAf5GA2.js} +2 -2
- package/dashboard/dist/assets/{AgentsPage-C6zqP_xC.js.map → AgentsPage-DyAf5GA2.js.map} +1 -1
- package/dashboard/dist/assets/{AvailableEscalationsPage-D61kC7Is.js → AvailableEscalationsPage-DDwAgklw.js} +2 -2
- package/dashboard/dist/assets/{AvailableEscalationsPage-D61kC7Is.js.map → AvailableEscalationsPage-DDwAgklw.js.map} +1 -1
- package/dashboard/dist/assets/{BotPicker-ChgljKHj.js → BotPicker-B1RdaNuY.js} +2 -2
- package/dashboard/dist/assets/{BotPicker-ChgljKHj.js.map → BotPicker-B1RdaNuY.js.map} +1 -1
- package/dashboard/dist/assets/{CapabilitiesPage-DeybKoZM.js → CapabilitiesPage-CxHDnyjX.js} +2 -2
- package/dashboard/dist/assets/{CapabilitiesPage-DeybKoZM.js.map → CapabilitiesPage-CxHDnyjX.js.map} +1 -1
- package/dashboard/dist/assets/{CollapsibleSection-BPiTuPBk.js → CollapsibleSection-Ds0e8cCO.js} +2 -2
- package/dashboard/dist/assets/{CollapsibleSection-BPiTuPBk.js.map → CollapsibleSection-Ds0e8cCO.js.map} +1 -1
- package/dashboard/dist/assets/{CredentialsPage-DXOcPWtd.js → CredentialsPage-D8UVO-3A.js} +2 -2
- package/dashboard/dist/assets/{CredentialsPage-DXOcPWtd.js.map → CredentialsPage-D8UVO-3A.js.map} +1 -1
- package/dashboard/dist/assets/{CronLabel-D3411N8x.js → CronLabel-ZesYXxCH.js} +2 -2
- package/dashboard/dist/assets/{CronLabel-D3411N8x.js.map → CronLabel-ZesYXxCH.js.map} +1 -1
- package/dashboard/dist/assets/{CustomDurationPicker-WG2tPSbV.js → CustomDurationPicker-OKlwH5a_.js} +2 -2
- package/dashboard/dist/assets/{CustomDurationPicker-WG2tPSbV.js.map → CustomDurationPicker-OKlwH5a_.js.map} +1 -1
- package/dashboard/dist/assets/{ElapsedCell-BFWEGSD7.js → ElapsedCell-CO8S6vdb.js} +2 -2
- package/dashboard/dist/assets/{ElapsedCell-BFWEGSD7.js.map → ElapsedCell-CO8S6vdb.js.map} +1 -1
- package/dashboard/dist/assets/{EscalationSchemaPage-D2Y5Yeqn.js → EscalationSchemaPage-Br5sD8-N.js} +2 -2
- package/dashboard/dist/assets/{EscalationSchemaPage-D2Y5Yeqn.js.map → EscalationSchemaPage-Br5sD8-N.js.map} +1 -1
- package/dashboard/dist/assets/{EscalationsOverview-VlLcX2YM.js → EscalationsOverview-DMpRvVql.js} +2 -2
- package/dashboard/dist/assets/{EscalationsOverview-VlLcX2YM.js.map → EscalationsOverview-DMpRvVql.js.map} +1 -1
- package/dashboard/dist/assets/{EventTable-3MJw14jp.js → EventTable-BARJeiS2.js} +2 -2
- package/dashboard/dist/assets/{EventTable-3MJw14jp.js.map → EventTable-BARJeiS2.js.map} +1 -1
- package/dashboard/dist/assets/{GraphInvokePage-QaUUrPnh.js → GraphInvokePage-CTuul-av.js} +2 -2
- package/dashboard/dist/assets/{GraphInvokePage-QaUUrPnh.js.map → GraphInvokePage-CTuul-av.js.map} +1 -1
- package/dashboard/dist/assets/{HomePage-Br0A3iOp.js → HomePage-BKFpOaHX.js} +2 -2
- package/dashboard/dist/assets/{HomePage-Br0A3iOp.js.map → HomePage-BKFpOaHX.js.map} +1 -1
- package/dashboard/dist/assets/{ListToolbar-d4MGQefV.js → ListToolbar-Dz2_neVF.js} +2 -2
- package/dashboard/dist/assets/{ListToolbar-d4MGQefV.js.map → ListToolbar-Dz2_neVF.js.map} +1 -1
- package/dashboard/dist/assets/{McpOverview-CUzJD5Z5.js → McpOverview-Do-vaEqV.js} +2 -2
- package/dashboard/dist/assets/{McpOverview-CUzJD5Z5.js.map → McpOverview-Do-vaEqV.js.map} +1 -1
- package/dashboard/dist/assets/{McpQueryDetailPage-pYSZmV3M.js → McpQueryDetailPage-HwDGNSbN.js} +2 -2
- package/dashboard/dist/assets/{McpQueryDetailPage-pYSZmV3M.js.map → McpQueryDetailPage-HwDGNSbN.js.map} +1 -1
- package/dashboard/dist/assets/{McpQueryPage-CA5C5dss.js → McpQueryPage-DxMUpQ7R.js} +2 -2
- package/dashboard/dist/assets/{McpQueryPage-CA5C5dss.js.map → McpQueryPage-DxMUpQ7R.js.map} +1 -1
- package/dashboard/dist/assets/{McpRunDetailPage-CrRkXwAe.js → McpRunDetailPage-BzzZIXIk.js} +2 -2
- package/dashboard/dist/assets/{McpRunDetailPage-CrRkXwAe.js.map → McpRunDetailPage-BzzZIXIk.js.map} +1 -1
- package/dashboard/dist/assets/{McpRunsPage-BqRBDbkn.js → McpRunsPage-D3J42d2i.js} +2 -2
- package/dashboard/dist/assets/{McpRunsPage-BqRBDbkn.js.map → McpRunsPage-D3J42d2i.js.map} +1 -1
- package/dashboard/dist/assets/OperationsPage-BaM-QDhy.js +2 -0
- package/dashboard/dist/assets/{OperationsPage--uVJxzmP.js.map → OperationsPage-BaM-QDhy.js.map} +1 -1
- package/dashboard/dist/assets/{OperatorDashboard-D-UMrTqI.js → OperatorDashboard-BqJn8wEZ.js} +2 -2
- package/dashboard/dist/assets/{OperatorDashboard-D-UMrTqI.js.map → OperatorDashboard-BqJn8wEZ.js.map} +1 -1
- package/dashboard/dist/assets/{ProcessDetailPage-BV6D_wuR.js → ProcessDetailPage-C8sT2xuE.js} +2 -2
- package/dashboard/dist/assets/{ProcessDetailPage-BV6D_wuR.js.map → ProcessDetailPage-C8sT2xuE.js.map} +1 -1
- package/dashboard/dist/assets/{ProcessesListPage-BAalEHpx.js → ProcessesListPage-eA8buUgd.js} +2 -2
- package/dashboard/dist/assets/{ProcessesListPage-BAalEHpx.js.map → ProcessesListPage-eA8buUgd.js.map} +1 -1
- package/dashboard/dist/assets/{RoleDetailPage-ClE7rUoo.js → RoleDetailPage-aR-ror5v.js} +2 -2
- package/dashboard/dist/assets/{RoleDetailPage-ClE7rUoo.js.map → RoleDetailPage-aR-ror5v.js.map} +1 -1
- package/dashboard/dist/assets/{RolesPage-DMsgZioQ.js → RolesPage-BQNQxsMY.js} +2 -2
- package/dashboard/dist/assets/{RolesPage-DMsgZioQ.js.map → RolesPage-BQNQxsMY.js.map} +1 -1
- package/dashboard/dist/assets/{RunAsSelector-VCnEYIwj.js → RunAsSelector-mhusrlYE.js} +2 -2
- package/dashboard/dist/assets/{RunAsSelector-VCnEYIwj.js.map → RunAsSelector-mhusrlYE.js.map} +1 -1
- package/dashboard/dist/assets/{StickyPagination-3eCY2-_V.js → StickyPagination-CqT9Q1I6.js} +2 -2
- package/dashboard/dist/assets/{StickyPagination-3eCY2-_V.js.map → StickyPagination-CqT9Q1I6.js.map} +1 -1
- package/dashboard/dist/assets/{StreamMessageDetail-70kDFH4o.js → StreamMessageDetail-jOJaoupp.js} +2 -2
- package/dashboard/dist/assets/{StreamMessageDetail-70kDFH4o.js.map → StreamMessageDetail-jOJaoupp.js.map} +1 -1
- package/dashboard/dist/assets/{SwimlaneTimeline-BOXRit6t.js → SwimlaneTimeline-BrfmWgAj.js} +2 -2
- package/dashboard/dist/assets/{SwimlaneTimeline-BOXRit6t.js.map → SwimlaneTimeline-BrfmWgAj.js.map} +1 -1
- package/dashboard/dist/assets/{TaskDetailPage-YxGQE9Qg.js → TaskDetailPage-Cus_JHIC.js} +2 -2
- package/dashboard/dist/assets/{TaskDetailPage-YxGQE9Qg.js.map → TaskDetailPage-Cus_JHIC.js.map} +1 -1
- package/dashboard/dist/assets/{TasksListPage-DuLJQN7Q.js → TasksListPage-B7GK8AoX.js} +2 -2
- package/dashboard/dist/assets/{TasksListPage-DuLJQN7Q.js.map → TasksListPage-B7GK8AoX.js.map} +1 -1
- package/dashboard/dist/assets/{TimeAgo-dnc1ZjKc.js → TimeAgo-C606LzT3.js} +2 -2
- package/dashboard/dist/assets/{TimeAgo-dnc1ZjKc.js.map → TimeAgo-C606LzT3.js.map} +1 -1
- package/dashboard/dist/assets/{TimestampCell-CDR6cTyq.js → TimestampCell-hfh6zvH-.js} +2 -2
- package/dashboard/dist/assets/{TimestampCell-CDR6cTyq.js.map → TimestampCell-hfh6zvH-.js.map} +1 -1
- package/dashboard/dist/assets/{ToolTestPanel-CfwGdRdz.js → ToolTestPanel-CsPsyvQq.js} +2 -2
- package/dashboard/dist/assets/{ToolTestPanel-CfwGdRdz.js.map → ToolTestPanel-CsPsyvQq.js.map} +1 -1
- package/dashboard/dist/assets/{TopicDetailPage-f2qaeGFz.js → TopicDetailPage-Diz6r7zr.js} +2 -2
- package/dashboard/dist/assets/{TopicDetailPage-f2qaeGFz.js.map → TopicDetailPage-Diz6r7zr.js.map} +1 -1
- package/dashboard/dist/assets/{TopicsPage-CwYdVmyK.js → TopicsPage-B6fn7fFh.js} +2 -2
- package/dashboard/dist/assets/{TopicsPage-CwYdVmyK.js.map → TopicsPage-B6fn7fFh.js.map} +1 -1
- package/dashboard/dist/assets/{UserName-CRE7EZPU.js → UserName-Cc-5QH5q.js} +2 -2
- package/dashboard/dist/assets/{UserName-CRE7EZPU.js.map → UserName-Cc-5QH5q.js.map} +1 -1
- package/dashboard/dist/assets/{WorkflowExecutionPage-BjNBu-Zt.js → WorkflowExecutionPage-BFq7UdhA.js} +2 -2
- package/dashboard/dist/assets/{WorkflowExecutionPage-BjNBu-Zt.js.map → WorkflowExecutionPage-BFq7UdhA.js.map} +1 -1
- package/dashboard/dist/assets/{WorkflowsDashboard-DfpvbzNt.js → WorkflowsDashboard-CHCM6hUa.js} +2 -2
- package/dashboard/dist/assets/{WorkflowsDashboard-DfpvbzNt.js.map → WorkflowsDashboard-CHCM6hUa.js.map} +1 -1
- package/dashboard/dist/assets/{WorkflowsOverview-BwD2lQwj.js → WorkflowsOverview-WeOvl8-_.js} +2 -2
- package/dashboard/dist/assets/{WorkflowsOverview-BwD2lQwj.js.map → WorkflowsOverview-WeOvl8-_.js.map} +1 -1
- package/dashboard/dist/assets/{YamlWorkflowDetailPage-BhHb7j2s.js → YamlWorkflowDetailPage-6GfhVDpv.js} +2 -2
- package/dashboard/dist/assets/{YamlWorkflowDetailPage-BhHb7j2s.js.map → YamlWorkflowDetailPage-6GfhVDpv.js.map} +1 -1
- package/dashboard/dist/assets/{YamlWorkflowsPage-SBRpY2dI.js → YamlWorkflowsPage-DqcY3ocP.js} +2 -2
- package/dashboard/dist/assets/{YamlWorkflowsPage-SBRpY2dI.js.map → YamlWorkflowsPage-DqcY3ocP.js.map} +1 -1
- package/dashboard/dist/assets/{agents-VbkdqWCr.js → agents-DlzvjNxE.js} +2 -2
- package/dashboard/dist/assets/{agents-VbkdqWCr.js.map → agents-DlzvjNxE.js.map} +1 -1
- package/dashboard/dist/assets/{bots-CsgYcYU_.js → bots-CWMN-KUI.js} +2 -2
- package/dashboard/dist/assets/{bots-CsgYcYU_.js.map → bots-CWMN-KUI.js.map} +1 -1
- package/dashboard/dist/assets/{capabilities-CMhFyoDl.js → capabilities-_WSZ7cxO.js} +2 -2
- package/dashboard/dist/assets/{capabilities-CMhFyoDl.js.map → capabilities-_WSZ7cxO.js.map} +1 -1
- package/dashboard/dist/assets/{controlplane-BstXtPbV.js → controlplane-_k9nWZy3.js} +2 -2
- package/dashboard/dist/assets/{controlplane-BstXtPbV.js.map → controlplane-_k9nWZy3.js.map} +1 -1
- package/dashboard/dist/assets/{escalation-C0BNAeIG.js → escalation-CK8ElYzD.js} +2 -2
- package/dashboard/dist/assets/{escalation-C0BNAeIG.js.map → escalation-CK8ElYzD.js.map} +1 -1
- package/dashboard/dist/assets/{escalation-columns-DajPgfuV.js → escalation-columns-Dyrf6Tdp.js} +2 -2
- package/dashboard/dist/assets/{escalation-columns-DajPgfuV.js.map → escalation-columns-Dyrf6Tdp.js.map} +1 -1
- package/dashboard/dist/assets/{index-BWLVqWLU.js → index-BiApvBEz.js} +2 -2
- package/dashboard/dist/assets/{index-BWLVqWLU.js.map → index-BiApvBEz.js.map} +1 -1
- package/dashboard/dist/assets/{index-D_6JFBiM.js → index-C3h0qGzG.js} +2 -2
- package/dashboard/dist/assets/{index-D_6JFBiM.js.map → index-C3h0qGzG.js.map} +1 -1
- package/dashboard/dist/assets/{index-fqU_l4dC.js → index-C5Yc3TT7.js} +2 -2
- package/dashboard/dist/assets/{index-fqU_l4dC.js.map → index-C5Yc3TT7.js.map} +1 -1
- package/dashboard/dist/assets/{index-Cl1vY8Lg.js → index-C7VXLKJs.js} +2 -2
- package/dashboard/dist/assets/{index-Cl1vY8Lg.js.map → index-C7VXLKJs.js.map} +1 -1
- package/dashboard/dist/assets/{index-CvFQtPWA.js → index-D8GcHxNJ.js} +2 -2
- package/dashboard/dist/assets/{index-CvFQtPWA.js.map → index-D8GcHxNJ.js.map} +1 -1
- package/dashboard/dist/assets/{index-CSixdp5W.js → index-Dc-Ye2wk.js} +2 -2
- package/dashboard/dist/assets/{index-CSixdp5W.js.map → index-Dc-Ye2wk.js.map} +1 -1
- package/dashboard/dist/assets/{index-DWlPFH3T.js → index-Dd4Si1Rl.js} +2 -2
- package/dashboard/dist/assets/{index-DWlPFH3T.js.map → index-Dd4Si1Rl.js.map} +1 -1
- package/dashboard/dist/assets/{index-C8k3Z2su.js → index-DwWP-Z_c.js} +3 -3
- package/dashboard/dist/assets/{index-C8k3Z2su.js.map → index-DwWP-Z_c.js.map} +1 -1
- package/dashboard/dist/assets/index-Ex5tEte8.js +5 -0
- package/dashboard/dist/assets/index-Ex5tEte8.js.map +1 -0
- package/dashboard/dist/assets/index-OV1JoB_P.js +6 -0
- package/dashboard/dist/assets/index-OV1JoB_P.js.map +1 -0
- package/dashboard/dist/assets/{index-BY5m8rhE.js → index-ZBj6Xgim.js} +2 -2
- package/dashboard/dist/assets/{index-BY5m8rhE.js.map → index-ZBj6Xgim.js.map} +1 -1
- package/dashboard/dist/assets/{index-CXnSqSFH.js → index-kKufOPLo.js} +2 -2
- package/dashboard/dist/assets/{index-CXnSqSFH.js.map → index-kKufOPLo.js.map} +1 -1
- package/dashboard/dist/assets/{index-CaSMWhLU.js → index-uUYXmvFf.js} +2 -2
- package/dashboard/dist/assets/{index-CaSMWhLU.js.map → index-uUYXmvFf.js.map} +1 -1
- package/dashboard/dist/assets/{knowledge-Bskl1MbC.js → knowledge-fYoEF4Lx.js} +2 -2
- package/dashboard/dist/assets/{knowledge-Bskl1MbC.js.map → knowledge-fYoEF4Lx.js.map} +1 -1
- package/dashboard/dist/assets/{mcp-C_qz25AB.js → mcp-1lzlgtS8.js} +2 -2
- package/dashboard/dist/assets/{mcp-C_qz25AB.js.map → mcp-1lzlgtS8.js.map} +1 -1
- package/dashboard/dist/assets/{mcp-query-CJx58s1A.js → mcp-query-nlrAz7m_.js} +2 -2
- package/dashboard/dist/assets/{mcp-query-CJx58s1A.js.map → mcp-query-nlrAz7m_.js.map} +1 -1
- package/dashboard/dist/assets/{pipelines-B6GzpgtG.js → pipelines-Ihi7EKXi.js} +2 -2
- package/dashboard/dist/assets/{pipelines-B6GzpgtG.js.map → pipelines-Ihi7EKXi.js.map} +1 -1
- package/dashboard/dist/assets/{roles-D8lPvFgG.js → roles-D4Wn7kE9.js} +2 -2
- package/dashboard/dist/assets/{roles-D8lPvFgG.js.map → roles-D4Wn7kE9.js.map} +1 -1
- package/dashboard/dist/assets/{tasks-BWHakSYf.js → tasks-Crk9Y3C4.js} +2 -2
- package/dashboard/dist/assets/{tasks-BWHakSYf.js.map → tasks-Crk9Y3C4.js.map} +1 -1
- package/dashboard/dist/assets/{topics-0dXzPX8t.js → topics-CtfcCyw9.js} +2 -2
- package/dashboard/dist/assets/{topics-0dXzPX8t.js.map → topics-CtfcCyw9.js.map} +1 -1
- package/dashboard/dist/assets/{useEventHooks-Dpixlgp-.js → useEventHooks-Dn_vN2xE.js} +2 -2
- package/dashboard/dist/assets/{useEventHooks-Dpixlgp-.js.map → useEventHooks-Dn_vN2xE.js.map} +1 -1
- package/dashboard/dist/assets/{useNamespace-DQMc77ok.js → useNamespace-Cfs3vKWk.js} +2 -2
- package/dashboard/dist/assets/{useNamespace-DQMc77ok.js.map → useNamespace-Cfs3vKWk.js.map} +1 -1
- package/dashboard/dist/assets/{useYamlActivityEvents-DO5bHOVE.js → useYamlActivityEvents-ymobxH47.js} +2 -2
- package/dashboard/dist/assets/{useYamlActivityEvents-DO5bHOVE.js.map → useYamlActivityEvents-ymobxH47.js.map} +1 -1
- package/dashboard/dist/assets/{users-B_LVbnUk.js → users-DngGIiC_.js} +2 -2
- package/dashboard/dist/assets/{users-B_LVbnUk.js.map → users-DngGIiC_.js.map} +1 -1
- package/dashboard/dist/assets/{workflows-WmmGDAvo.js → workflows-DuUspMAl.js} +2 -2
- package/dashboard/dist/assets/{workflows-WmmGDAvo.js.map → workflows-DuUspMAl.js.map} +1 -1
- package/dashboard/dist/assets/{yaml-workflows-BS7KHhU1.js → yaml-workflows-DXQCiOsP.js} +2 -2
- package/dashboard/dist/assets/{yaml-workflows-BS7KHhU1.js.map → yaml-workflows-DXQCiOsP.js.map} +1 -1
- package/dashboard/dist/index.html +1 -1
- package/docs/api/http/escalations.md +8 -4
- package/docs/api/http/roles.md +1 -1
- package/docs/api/http/workflows.md +1 -1
- package/docs/api/sdk/escalations.md +3 -3
- package/docs/api/sdk/workflows.md +1 -1
- package/docs/dashboard.md +2 -2
- package/docs/data.md +1 -1
- package/docs/hitl-guide.md +2 -3
- package/package.json +2 -1
- package/dashboard/dist/assets/OperationsPage--uVJxzmP.js +0 -2
- package/dashboard/dist/assets/index-BYf0oNSQ.js +0 -6
- package/dashboard/dist/assets/index-BYf0oNSQ.js.map +0 -1
- package/dashboard/dist/assets/index-BcaQ_hCz.js +0 -9
- 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
|
|
98
|
-
//
|
|
99
|
-
//
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
114
|
-
schema
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
-
|
|
55
|
-
|
|
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 };
|
package/build/api/roles.d.ts
CHANGED
|
@@ -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); });
|