@hotmeshio/long-tail 0.6.2 → 0.7.0
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 +45 -0
- package/build/api/escalations/index.d.ts +1 -1
- package/build/api/escalations/index.js +2 -1
- package/build/api/escalations/list.d.ts +3 -0
- package/build/api/escalations/list.js +14 -0
- package/build/api/roles.d.ts +17 -0
- package/build/api/roles.js +46 -0
- package/build/bin/ltc.js +1 -0
- package/build/lib/cli/commands/escalations.d.ts +1 -0
- package/build/lib/cli/commands/escalations.js +2 -0
- package/build/lib/db/migrate.js +22 -2
- package/build/lib/db/schemas/014_roles_v2.sql +19 -0
- package/build/lib/db/schemas/015_role_ops.sql +50 -0
- package/build/lib/db/schemas/016_station_metrics_indexes.sql +13 -0
- package/build/lib/events/system-events.js +3 -0
- package/build/modules/auth.d.ts +9 -0
- package/build/modules/auth.js +38 -1
- package/build/routes/escalations/list.js +10 -0
- package/build/routes/roles.js +16 -5
- package/build/scripts/ortho-populate.d.ts +37 -0
- package/build/scripts/ortho-populate.js +228 -0
- package/build/sdk/index.d.ts +4 -0
- package/build/sdk/index.js +2 -0
- package/build/services/escalation/client.js +93 -2
- package/build/services/escalation/crud.js +18 -9
- package/build/services/escalation/metrics-cache.d.ts +14 -0
- package/build/services/escalation/metrics-cache.js +53 -0
- package/build/services/escalation/queries.d.ts +14 -2
- package/build/services/escalation/queries.js +121 -11
- package/build/services/escalation/sql.d.ts +29 -2
- package/build/services/escalation/sql.js +156 -27
- package/build/services/escalation/types.d.ts +17 -0
- package/build/services/escalation/types.js +1 -0
- package/build/services/mcp/external-server.js +5 -1
- package/build/services/orchestrator/condition.d.ts +20 -0
- package/build/services/orchestrator/condition.js +20 -0
- package/build/services/role/index.d.ts +22 -3
- package/build/services/role/index.js +50 -2
- package/build/services/role/sql.d.ts +11 -1
- package/build/services/role/sql.js +38 -1
- package/build/services/role/types.d.ts +32 -0
- package/build/system/mcp-servers/admin/escalations.js +8 -7
- package/build/system/mcp-servers/admin/index.js +5 -3
- package/build/system/mcp-servers/admin/ortho.d.ts +10 -0
- package/build/system/mcp-servers/admin/ortho.js +242 -0
- package/build/system/mcp-servers/admin/schemas.d.ts +40 -3
- package/build/system/mcp-servers/admin/schemas.js +17 -4
- package/build/system/mcp-servers/admin/users.js +31 -0
- package/build/system/seed/tool-manifests-admin.d.ts +4016 -712
- package/build/system/seed/tool-manifests-admin.js +14 -1
- package/build/tsconfig.tsbuildinfo +1 -1
- package/build/types/escalation.d.ts +3 -1
- package/dashboard/dist/assets/{AdminDashboard-C5gfVEeL.js → AdminDashboard-Bg1HgukI.js} +2 -2
- package/dashboard/dist/assets/{AdminDashboard-C5gfVEeL.js.map → AdminDashboard-Bg1HgukI.js.map} +1 -1
- package/dashboard/dist/assets/{AgentConfigPage-CWEnuiGN.js → AgentConfigPage-HBe_YDU_.js} +6 -6
- package/dashboard/dist/assets/{AgentConfigPage-CWEnuiGN.js.map → AgentConfigPage-HBe_YDU_.js.map} +1 -1
- package/dashboard/dist/assets/{AgentDetailPage-B6IE27iK.js → AgentDetailPage-BsaiYbwL.js} +4 -4
- package/dashboard/dist/assets/{AgentDetailPage-B6IE27iK.js.map → AgentDetailPage-BsaiYbwL.js.map} +1 -1
- package/dashboard/dist/assets/AgentsPage-DhN2lIBh.js +2 -0
- package/dashboard/dist/assets/{AgentsPage-Blhh2RsK.js.map → AgentsPage-DhN2lIBh.js.map} +1 -1
- package/dashboard/dist/assets/AvailableEscalationsPage-_xcWvlIz.js +2 -0
- package/dashboard/dist/assets/AvailableEscalationsPage-_xcWvlIz.js.map +1 -0
- package/dashboard/dist/assets/{BotPicker-DSozotGk.js → BotPicker-DY6iUVC9.js} +2 -2
- package/dashboard/dist/assets/{BotPicker-DSozotGk.js.map → BotPicker-DY6iUVC9.js.map} +1 -1
- package/dashboard/dist/assets/{CapabilitiesPage-CrS_DTA1.js → CapabilitiesPage-w1owqsmN.js} +2 -2
- package/dashboard/dist/assets/{CapabilitiesPage-CrS_DTA1.js.map → CapabilitiesPage-w1owqsmN.js.map} +1 -1
- package/dashboard/dist/assets/{CollapsibleSection-1i6bOQ1L.js → CollapsibleSection-DIMGjlJ-.js} +2 -2
- package/dashboard/dist/assets/{CollapsibleSection-1i6bOQ1L.js.map → CollapsibleSection-DIMGjlJ-.js.map} +1 -1
- package/dashboard/dist/assets/{CredentialsPage-BxJio06E.js → CredentialsPage-D1OmMHux.js} +2 -2
- package/dashboard/dist/assets/{CredentialsPage-BxJio06E.js.map → CredentialsPage-D1OmMHux.js.map} +1 -1
- package/dashboard/dist/assets/{CronLabel-BAfIHHz4.js → CronLabel-CQy3DrUQ.js} +2 -2
- package/dashboard/dist/assets/{CronLabel-BAfIHHz4.js.map → CronLabel-CQy3DrUQ.js.map} +1 -1
- package/dashboard/dist/assets/{CustomDurationPicker-BLF0NFJK.js → CustomDurationPicker-BoBkl9tw.js} +2 -2
- package/dashboard/dist/assets/{CustomDurationPicker-BLF0NFJK.js.map → CustomDurationPicker-BoBkl9tw.js.map} +1 -1
- package/dashboard/dist/assets/{DropZone-CEx_wk-g.js → DropZone-QXbiuU0b.js} +2 -2
- package/dashboard/dist/assets/{DropZone-CEx_wk-g.js.map → DropZone-QXbiuU0b.js.map} +1 -1
- package/dashboard/dist/assets/{ElapsedCell-nKGJvznH.js → ElapsedCell-Donjm9gT.js} +2 -2
- package/dashboard/dist/assets/{ElapsedCell-nKGJvznH.js.map → ElapsedCell-Donjm9gT.js.map} +1 -1
- package/dashboard/dist/assets/{EscalationsOverview-bNNVA6R2.js → EscalationsOverview-CvHIJ_GG.js} +2 -2
- package/dashboard/dist/assets/{EscalationsOverview-bNNVA6R2.js.map → EscalationsOverview-CvHIJ_GG.js.map} +1 -1
- package/dashboard/dist/assets/{EventTable-BCuDdqlU.js → EventTable-DpAXMmT3.js} +2 -2
- package/dashboard/dist/assets/{EventTable-BCuDdqlU.js.map → EventTable-DpAXMmT3.js.map} +1 -1
- package/dashboard/dist/assets/{GraphInvokePage-BPAvqHls.js → GraphInvokePage-De1XVx5d.js} +2 -2
- package/dashboard/dist/assets/{GraphInvokePage-BPAvqHls.js.map → GraphInvokePage-De1XVx5d.js.map} +1 -1
- package/dashboard/dist/assets/HomePage-BYR5VXXw.js +2 -0
- package/dashboard/dist/assets/HomePage-BYR5VXXw.js.map +1 -0
- package/dashboard/dist/assets/{ListToolbar-DIJZfEgu.js → ListToolbar-Bx7jGzPA.js} +2 -2
- package/dashboard/dist/assets/{ListToolbar-DIJZfEgu.js.map → ListToolbar-Bx7jGzPA.js.map} +1 -1
- package/dashboard/dist/assets/{McpOverview-DCfSCfPg.js → McpOverview-DXRKcJN0.js} +2 -2
- package/dashboard/dist/assets/{McpOverview-DCfSCfPg.js.map → McpOverview-DXRKcJN0.js.map} +1 -1
- package/dashboard/dist/assets/{McpQueryDetailPage-mb7rmtNe.js → McpQueryDetailPage-CmW15w5e.js} +2 -2
- package/dashboard/dist/assets/{McpQueryDetailPage-mb7rmtNe.js.map → McpQueryDetailPage-CmW15w5e.js.map} +1 -1
- package/dashboard/dist/assets/{McpQueryPage-D9b1T0I3.js → McpQueryPage-BE6I011z.js} +2 -2
- package/dashboard/dist/assets/{McpQueryPage-D9b1T0I3.js.map → McpQueryPage-BE6I011z.js.map} +1 -1
- package/dashboard/dist/assets/{McpRunDetailPage-Bz2UyuNm.js → McpRunDetailPage-Dsa3G3XQ.js} +2 -2
- package/dashboard/dist/assets/{McpRunDetailPage-Bz2UyuNm.js.map → McpRunDetailPage-Dsa3G3XQ.js.map} +1 -1
- package/dashboard/dist/assets/{McpRunsPage-Dw7w_ZO-.js → McpRunsPage-uTbym66Y.js} +2 -2
- package/dashboard/dist/assets/{McpRunsPage-Dw7w_ZO-.js.map → McpRunsPage-uTbym66Y.js.map} +1 -1
- package/dashboard/dist/assets/{NamespacePill-D27vqDXy.js → NamespacePill-BGf0jHwE.js} +2 -2
- package/dashboard/dist/assets/{NamespacePill-D27vqDXy.js.map → NamespacePill-BGf0jHwE.js.map} +1 -1
- package/dashboard/dist/assets/OperationsPage-CAmiFHI6.js +2 -0
- package/dashboard/dist/assets/OperationsPage-CAmiFHI6.js.map +1 -0
- package/dashboard/dist/assets/{OperatorDashboard-CTzSDW3N.js → OperatorDashboard-i8cxaDlk.js} +2 -2
- package/dashboard/dist/assets/{OperatorDashboard-CTzSDW3N.js.map → OperatorDashboard-i8cxaDlk.js.map} +1 -1
- package/dashboard/dist/assets/{PageHeader-B_1zTx2q.js → PageHeader-BZJsEVOC.js} +2 -2
- package/dashboard/dist/assets/{PageHeader-B_1zTx2q.js.map → PageHeader-BZJsEVOC.js.map} +1 -1
- package/dashboard/dist/assets/{PageHeaderWithStats-D-cPg7UD.js → PageHeaderWithStats-BWuD7s9c.js} +2 -2
- package/dashboard/dist/assets/{PageHeaderWithStats-D-cPg7UD.js.map → PageHeaderWithStats-BWuD7s9c.js.map} +1 -1
- package/dashboard/dist/assets/{ProcessDetailPage-olG2CS-h.js → ProcessDetailPage-CP7GZhgs.js} +2 -2
- package/dashboard/dist/assets/{ProcessDetailPage-olG2CS-h.js.map → ProcessDetailPage-CP7GZhgs.js.map} +1 -1
- package/dashboard/dist/assets/{ProcessesListPage-CUZSL06Y.js → ProcessesListPage-btsU6hBR.js} +2 -2
- package/dashboard/dist/assets/{ProcessesListPage-CUZSL06Y.js.map → ProcessesListPage-btsU6hBR.js.map} +1 -1
- package/dashboard/dist/assets/RoleDetailPage-BB_e330f.js +16 -0
- package/dashboard/dist/assets/RoleDetailPage-BB_e330f.js.map +1 -0
- package/dashboard/dist/assets/{RolePill-B-V-31lC.js → RolePill-Pb9TKSQN.js} +2 -2
- package/dashboard/dist/assets/{RolePill-B-V-31lC.js.map → RolePill-Pb9TKSQN.js.map} +1 -1
- package/dashboard/dist/assets/RolesPage-1DBoEMsb.js +2 -0
- package/dashboard/dist/assets/RolesPage-1DBoEMsb.js.map +1 -0
- package/dashboard/dist/assets/{RunAsSelector-BoLHF4fy.js → RunAsSelector-Cp6j3FEY.js} +2 -2
- package/dashboard/dist/assets/{RunAsSelector-BoLHF4fy.js.map → RunAsSelector-Cp6j3FEY.js.map} +1 -1
- package/dashboard/dist/assets/StatusBadge-D9MeQHrh.js +2 -0
- package/dashboard/dist/assets/StatusBadge-D9MeQHrh.js.map +1 -0
- package/dashboard/dist/assets/{StickyPagination-BEMY1L-H.js → StickyPagination-Pr8kawv8.js} +2 -2
- package/dashboard/dist/assets/{StickyPagination-BEMY1L-H.js.map → StickyPagination-Pr8kawv8.js.map} +1 -1
- package/dashboard/dist/assets/StreamMessageDetail-BqmQ-N3y.js +2 -0
- package/dashboard/dist/assets/{StreamMessageDetail-CVRVWXcU.js.map → StreamMessageDetail-BqmQ-N3y.js.map} +1 -1
- package/dashboard/dist/assets/{SwimlaneTimeline-DDpU0-Jn.js → SwimlaneTimeline-y1YmXrsj.js} +2 -2
- package/dashboard/dist/assets/{SwimlaneTimeline-DDpU0-Jn.js.map → SwimlaneTimeline-y1YmXrsj.js.map} +1 -1
- package/dashboard/dist/assets/{TagInput-BHVfG307.js → TagInput-Df4myuYF.js} +2 -2
- package/dashboard/dist/assets/{TagInput-BHVfG307.js.map → TagInput-Df4myuYF.js.map} +1 -1
- package/dashboard/dist/assets/{TaskDetailPage-B_xQtJMg.js → TaskDetailPage-CT5rXHK5.js} +2 -2
- package/dashboard/dist/assets/{TaskDetailPage-B_xQtJMg.js.map → TaskDetailPage-CT5rXHK5.js.map} +1 -1
- package/dashboard/dist/assets/{TaskQueuePill-CGlxXqWt.js → TaskQueuePill-YalYdCNo.js} +2 -2
- package/dashboard/dist/assets/{TaskQueuePill-CGlxXqWt.js.map → TaskQueuePill-YalYdCNo.js.map} +1 -1
- package/dashboard/dist/assets/{TasksListPage-CsrdIOKk.js → TasksListPage-DOy6JlKS.js} +2 -2
- package/dashboard/dist/assets/{TasksListPage-CsrdIOKk.js.map → TasksListPage-DOy6JlKS.js.map} +1 -1
- package/dashboard/dist/assets/{TimeAgo-DmicaxjT.js → TimeAgo-BAQwZSpq.js} +2 -2
- package/dashboard/dist/assets/{TimeAgo-DmicaxjT.js.map → TimeAgo-BAQwZSpq.js.map} +1 -1
- package/dashboard/dist/assets/{TimestampCell-DeK89eRV.js → TimestampCell-BK_jufns.js} +2 -2
- package/dashboard/dist/assets/{TimestampCell-DeK89eRV.js.map → TimestampCell-BK_jufns.js.map} +1 -1
- package/dashboard/dist/assets/{ToolPill-NV7PxSpA.js → ToolPill-VGpT6Quy.js} +2 -2
- package/dashboard/dist/assets/{ToolPill-NV7PxSpA.js.map → ToolPill-VGpT6Quy.js.map} +1 -1
- package/dashboard/dist/assets/{ToolTestPanel-D3s2n9-9.js → ToolTestPanel-5PMQyUCe.js} +2 -2
- package/dashboard/dist/assets/{ToolTestPanel-D3s2n9-9.js.map → ToolTestPanel-5PMQyUCe.js.map} +1 -1
- package/dashboard/dist/assets/{TopicDetailPage-qyv7UWT_.js → TopicDetailPage-Cn1ShfR8.js} +3 -3
- package/dashboard/dist/assets/{TopicDetailPage-qyv7UWT_.js.map → TopicDetailPage-Cn1ShfR8.js.map} +1 -1
- package/dashboard/dist/assets/{TopicsPage-CEUfMoed.js → TopicsPage-D67tjS8L.js} +2 -2
- package/dashboard/dist/assets/{TopicsPage-CEUfMoed.js.map → TopicsPage-D67tjS8L.js.map} +1 -1
- package/dashboard/dist/assets/{UserName-Bn_BxI8K.js → UserName-Dsu1Jk4N.js} +2 -2
- package/dashboard/dist/assets/{UserName-Bn_BxI8K.js.map → UserName-Dsu1Jk4N.js.map} +1 -1
- package/dashboard/dist/assets/{WorkflowExecutionPage-DAk2RqpB.js → WorkflowExecutionPage-BwQoOmUE.js} +2 -2
- package/dashboard/dist/assets/{WorkflowExecutionPage-DAk2RqpB.js.map → WorkflowExecutionPage-BwQoOmUE.js.map} +1 -1
- package/dashboard/dist/assets/{WorkflowPill-DDR0B6DN.js → WorkflowPill-pPUqmx6Q.js} +2 -2
- package/dashboard/dist/assets/{WorkflowPill-DDR0B6DN.js.map → WorkflowPill-pPUqmx6Q.js.map} +1 -1
- package/dashboard/dist/assets/{WorkflowsDashboard-DfBgM80t.js → WorkflowsDashboard-D3z7COgH.js} +2 -2
- package/dashboard/dist/assets/{WorkflowsDashboard-DfBgM80t.js.map → WorkflowsDashboard-D3z7COgH.js.map} +1 -1
- package/dashboard/dist/assets/{WorkflowsOverview--Dkoo9Jq.js → WorkflowsOverview-C15JcxyY.js} +2 -2
- package/dashboard/dist/assets/{WorkflowsOverview--Dkoo9Jq.js.map → WorkflowsOverview-C15JcxyY.js.map} +1 -1
- package/dashboard/dist/assets/{YamlWorkflowDetailPage-B6HYbfFj.js → YamlWorkflowDetailPage-BYq3t7E-.js} +2 -2
- package/dashboard/dist/assets/{YamlWorkflowDetailPage-B6HYbfFj.js.map → YamlWorkflowDetailPage-BYq3t7E-.js.map} +1 -1
- package/dashboard/dist/assets/YamlWorkflowsPage-B44WgwE_.js +2 -0
- package/dashboard/dist/assets/{YamlWorkflowsPage-CJE-PxC0.js.map → YamlWorkflowsPage-B44WgwE_.js.map} +1 -1
- package/dashboard/dist/assets/{agents-DUciUZpt.js → agents-BRmmK6Y9.js} +2 -2
- package/dashboard/dist/assets/{agents-DUciUZpt.js.map → agents-BRmmK6Y9.js.map} +1 -1
- package/dashboard/dist/assets/{bots-BnyqCfuC.js → bots-BD75mzGG.js} +2 -2
- package/dashboard/dist/assets/{bots-BnyqCfuC.js.map → bots-BD75mzGG.js.map} +1 -1
- package/dashboard/dist/assets/{capabilities-CkTWWT0q.js → capabilities-DDgC_Kds.js} +2 -2
- package/dashboard/dist/assets/{capabilities-CkTWWT0q.js.map → capabilities-DDgC_Kds.js.map} +1 -1
- package/dashboard/dist/assets/controlplane-BJh12lMv.js +2 -0
- package/dashboard/dist/assets/controlplane-BJh12lMv.js.map +1 -0
- package/dashboard/dist/assets/{escalation-De9JnTKI.js → escalation-DIJt2sgn.js} +2 -2
- package/dashboard/dist/assets/{escalation-De9JnTKI.js.map → escalation-DIJt2sgn.js.map} +1 -1
- package/dashboard/dist/assets/{escalation-columns-Df7Pluzg.js → escalation-columns-zl6E94Up.js} +2 -2
- package/dashboard/dist/assets/{escalation-columns-Df7Pluzg.js.map → escalation-columns-zl6E94Up.js.map} +1 -1
- package/dashboard/dist/assets/{index-Cqjf-yVP.js → index-B0kmiSmt.js} +2 -2
- package/dashboard/dist/assets/{index-Cqjf-yVP.js.map → index-B0kmiSmt.js.map} +1 -1
- package/dashboard/dist/assets/index-BRtKQtQZ.css +1 -0
- package/dashboard/dist/assets/{index-BxdiKgnH.js → index-BUbzfEA9.js} +2 -2
- package/dashboard/dist/assets/{index-BxdiKgnH.js.map → index-BUbzfEA9.js.map} +1 -1
- package/dashboard/dist/assets/{index-DpAS4-DA.js → index-BhznDv-4.js} +2 -2
- package/dashboard/dist/assets/{index-DpAS4-DA.js.map → index-BhznDv-4.js.map} +1 -1
- package/dashboard/dist/assets/{index-BAhkWcOq.js → index-CDPBXB4e.js} +4 -4
- package/dashboard/dist/assets/{index-BAhkWcOq.js.map → index-CDPBXB4e.js.map} +1 -1
- package/dashboard/dist/assets/{index-CgD9fA9P.js → index-CV4lEd4I.js} +2 -2
- package/dashboard/dist/assets/{index-CgD9fA9P.js.map → index-CV4lEd4I.js.map} +1 -1
- package/dashboard/dist/assets/{index-DjxTV3cn.js → index-CknKy4RA.js} +2 -2
- package/dashboard/dist/assets/{index-DjxTV3cn.js.map → index-CknKy4RA.js.map} +1 -1
- package/dashboard/dist/assets/{index-DNugmBs7.js → index-CpgV4Kkp.js} +2 -2
- package/dashboard/dist/assets/{index-DNugmBs7.js.map → index-CpgV4Kkp.js.map} +1 -1
- package/dashboard/dist/assets/{index-B1kc6itM.js → index-CtDf8vvQ.js} +24 -24
- package/dashboard/dist/assets/index-CtDf8vvQ.js.map +1 -0
- package/dashboard/dist/assets/{index-BzMsh6Ui.js → index-DU7YumVJ.js} +2 -2
- package/dashboard/dist/assets/{index-BzMsh6Ui.js.map → index-DU7YumVJ.js.map} +1 -1
- package/dashboard/dist/assets/{index-BsIaUbKD.js → index-DVVIeBBk.js} +2 -2
- package/dashboard/dist/assets/{index-BsIaUbKD.js.map → index-DVVIeBBk.js.map} +1 -1
- package/dashboard/dist/assets/index-DccCwyZl.js +6 -0
- package/dashboard/dist/assets/{index-CN0efCE9.js.map → index-DccCwyZl.js.map} +1 -1
- package/dashboard/dist/assets/{index-7hqtQI43.js → index-DrUkTcn-.js} +2 -2
- package/dashboard/dist/assets/{index-7hqtQI43.js.map → index-DrUkTcn-.js.map} +1 -1
- package/dashboard/dist/assets/{index-B07xLlyz.js → index-j-C7g_Ej.js} +2 -2
- package/dashboard/dist/assets/{index-B07xLlyz.js.map → index-j-C7g_Ej.js.map} +1 -1
- package/dashboard/dist/assets/{knowledge-D4SvdNv5.js → knowledge-pNDWACXB.js} +2 -2
- package/dashboard/dist/assets/{knowledge-D4SvdNv5.js.map → knowledge-pNDWACXB.js.map} +1 -1
- package/dashboard/dist/assets/{mcp-query-CLP9EnnU.js → mcp-query-CmNjvxTG.js} +2 -2
- package/dashboard/dist/assets/{mcp-query-CLP9EnnU.js.map → mcp-query-CmNjvxTG.js.map} +1 -1
- package/dashboard/dist/assets/{mcp-DVxt4T9J.js → mcp-xddLBwUW.js} +2 -2
- package/dashboard/dist/assets/{mcp-DVxt4T9J.js.map → mcp-xddLBwUW.js.map} +1 -1
- package/dashboard/dist/assets/pipelines-B05sd2nB.js +2 -0
- package/dashboard/dist/assets/pipelines-B05sd2nB.js.map +1 -0
- package/dashboard/dist/assets/roles-bRgaMcdy.js +2 -0
- package/dashboard/dist/assets/roles-bRgaMcdy.js.map +1 -0
- package/dashboard/dist/assets/tasks-Bm1OKrJA.js +2 -0
- package/dashboard/dist/assets/{tasks-1eTuP02d.js.map → tasks-Bm1OKrJA.js.map} +1 -1
- package/dashboard/dist/assets/{topics-B3p8K0PW.js → topics-BVIqtmd4.js} +2 -2
- package/dashboard/dist/assets/{topics-B3p8K0PW.js.map → topics-BVIqtmd4.js.map} +1 -1
- package/dashboard/dist/assets/useEventHooks-_qCzU74s.js +2 -0
- package/dashboard/dist/assets/useEventHooks-_qCzU74s.js.map +1 -0
- package/dashboard/dist/assets/{useNamespace-Dx81_JM0.js → useNamespace-CDnCSFpG.js} +2 -2
- package/dashboard/dist/assets/{useNamespace-Dx81_JM0.js.map → useNamespace-CDnCSFpG.js.map} +1 -1
- package/dashboard/dist/assets/{useYamlActivityEvents-D5T3TbE7.js → useYamlActivityEvents-zrHMhaFM.js} +2 -2
- package/dashboard/dist/assets/{useYamlActivityEvents-D5T3TbE7.js.map → useYamlActivityEvents-zrHMhaFM.js.map} +1 -1
- package/dashboard/dist/assets/{users-BEMysNdR.js → users-BPBMZg27.js} +2 -2
- package/dashboard/dist/assets/{users-BEMysNdR.js.map → users-BPBMZg27.js.map} +1 -1
- package/dashboard/dist/assets/{vendor-icons--Ivmp-RX.js → vendor-icons-C1zVP_dv.js} +125 -115
- package/dashboard/dist/assets/vendor-icons-C1zVP_dv.js.map +1 -0
- package/dashboard/dist/assets/workflows-CT6nTL1I.js +2 -0
- package/dashboard/dist/assets/workflows-CT6nTL1I.js.map +1 -0
- package/dashboard/dist/assets/{yaml-workflows-4rJ5v5rJ.js → yaml-workflows-CdfjkOKU.js} +2 -2
- package/dashboard/dist/assets/{yaml-workflows-4rJ5v5rJ.js.map → yaml-workflows-CdfjkOKU.js.map} +1 -1
- package/dashboard/dist/index.html +3 -3
- package/docs/api/http/escalations.md +82 -6
- package/docs/api/http/roles.md +82 -3
- package/docs/api/mcp/admin.md +127 -1
- package/docs/api/sdk/escalations.md +65 -4
- package/docs/api/sdk/roles.md +81 -0
- package/docs/cli.md +2 -0
- package/docs/dashboard.md +17 -2
- package/docs/hitl-guide.md +12 -3
- package/docs/operations.md +113 -0
- package/package.json +3 -2
- package/dashboard/dist/assets/AgentsPage-Blhh2RsK.js +0 -2
- package/dashboard/dist/assets/AvailableEscalationsPage-ClwAle76.js +0 -2
- package/dashboard/dist/assets/AvailableEscalationsPage-ClwAle76.js.map +0 -1
- package/dashboard/dist/assets/HomePage-BZ1YTrjR.js +0 -2
- package/dashboard/dist/assets/HomePage-BZ1YTrjR.js.map +0 -1
- package/dashboard/dist/assets/RolesPage-DLnaW6iD.js +0 -2
- package/dashboard/dist/assets/RolesPage-DLnaW6iD.js.map +0 -1
- package/dashboard/dist/assets/StatusBadge-Bl3FXVmF.js +0 -2
- package/dashboard/dist/assets/StatusBadge-Bl3FXVmF.js.map +0 -1
- package/dashboard/dist/assets/StreamMessageDetail-CVRVWXcU.js +0 -2
- package/dashboard/dist/assets/YamlWorkflowsPage-CJE-PxC0.js +0 -2
- package/dashboard/dist/assets/controlplane-D9HaFZsC.js +0 -2
- package/dashboard/dist/assets/controlplane-D9HaFZsC.js.map +0 -1
- package/dashboard/dist/assets/index-B1kc6itM.js.map +0 -1
- package/dashboard/dist/assets/index-CN0efCE9.js +0 -6
- package/dashboard/dist/assets/index-nO0cim3_.css +0 -1
- package/dashboard/dist/assets/pipelines-C_x0LTBN.js +0 -2
- package/dashboard/dist/assets/pipelines-C_x0LTBN.js.map +0 -1
- package/dashboard/dist/assets/roles-QaGFZQ58.js +0 -2
- package/dashboard/dist/assets/roles-QaGFZQ58.js.map +0 -1
- package/dashboard/dist/assets/tasks-1eTuP02d.js +0 -2
- package/dashboard/dist/assets/useEventHooks-BNZky4xM.js +0 -2
- package/dashboard/dist/assets/useEventHooks-BNZky4xM.js.map +0 -1
- package/dashboard/dist/assets/vendor-icons--Ivmp-RX.js.map +0 -1
- package/dashboard/dist/assets/workflows-C4rjvQbI.js +0 -2
- package/dashboard/dist/assets/workflows-C4rjvQbI.js.map +0 -1
|
@@ -6,12 +6,16 @@ exports.getEscalationStats = getEscalationStats;
|
|
|
6
6
|
exports.listDistinctTypes = listDistinctTypes;
|
|
7
7
|
exports.listEscalations = listEscalations;
|
|
8
8
|
exports.listAvailableEscalations = listAvailableEscalations;
|
|
9
|
+
exports.invalidateEscalationAggregates = invalidateEscalationAggregates;
|
|
10
|
+
exports.resetStationMetricsCache = resetStationMetricsCache;
|
|
11
|
+
exports.getStationMetrics = getStationMetrics;
|
|
9
12
|
const db_1 = require("../../lib/db");
|
|
10
13
|
const client_1 = require("./client");
|
|
11
14
|
const map_1 = require("./map");
|
|
12
15
|
const facet_sql_1 = require("./facet-sql");
|
|
13
16
|
const types_1 = require("./types");
|
|
14
17
|
const sql_1 = require("./sql");
|
|
18
|
+
const metrics_cache_1 = require("./metrics-cache");
|
|
15
19
|
/**
|
|
16
20
|
* Default sort is priority ASC, created_at ASC. A user-chosen `sort_by` maps to
|
|
17
21
|
* a single column (DESC unless `order='asc'`), matching the legacy behavior.
|
|
@@ -36,6 +40,13 @@ function buildSearchOrderBy(sortBy, order) {
|
|
|
36
40
|
}
|
|
37
41
|
return 'priority ASC, created_at ASC';
|
|
38
42
|
}
|
|
43
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
44
|
+
/**
|
|
45
|
+
* The id arm of the correlation search binds a uuid param only when the term
|
|
46
|
+
* parses as one — comparing `id::text = $term` casts the uuid column and has
|
|
47
|
+
* no index path, which drags the whole OR into a full history scan.
|
|
48
|
+
*/
|
|
49
|
+
const asUuidOrNull = (term) => term && UUID_RE.test(term) ? term : null;
|
|
39
50
|
/**
|
|
40
51
|
* Server-side free-text search over the `lt_escalations` view. Runs when a
|
|
41
52
|
* caller supplies a non-empty `search` term — the SDK `client.list()` cannot do
|
|
@@ -62,6 +73,7 @@ async function searchEscalations(params) {
|
|
|
62
73
|
params.selfRoles && params.selfRoles.length ? params.selfRoles : null,
|
|
63
74
|
params.meUserId || null,
|
|
64
75
|
params.metadata ? JSON.stringify(params.metadata) : null,
|
|
76
|
+
asUuidOrNull(params.search),
|
|
65
77
|
];
|
|
66
78
|
const orderBy = buildSearchOrderBy(params.sort_by, params.order);
|
|
67
79
|
const [rows, countRows] = await Promise.all([
|
|
@@ -78,7 +90,8 @@ async function searchEscalations(params) {
|
|
|
78
90
|
* predicate (global → none; else `role ∈ allRoles OR (role ∈ selfRoles AND
|
|
79
91
|
* assigned_to = me)`) with the full FacetQuery language (`buildFacetWhere`:
|
|
80
92
|
* facets `@>`, block, numeric range, exists, status, available) plus the extra
|
|
81
|
-
* top-level filters (type/subtype/priority/assigned_to) and
|
|
93
|
+
* top-level filters (type/subtype/priority/assigned_to) and an exact correlation-id
|
|
94
|
+
* match (id/workflow_id/origin_id), then orders
|
|
82
95
|
* by `buildFacetOrder` (columns + `metadata.<key>`). Every value is bound; the
|
|
83
96
|
* COUNT shares the WHERE so totals stay correct across pages — no client-side
|
|
84
97
|
* filtering. Reads go through the `public.lt_escalations` view.
|
|
@@ -110,13 +123,14 @@ async function searchEscalationsFaceted(opts) {
|
|
|
110
123
|
clauses.push(`priority = $${params.push(opts.priority)}`);
|
|
111
124
|
if (opts.assigned_to)
|
|
112
125
|
clauses.push(`assigned_to = $${params.push(opts.assigned_to)}`);
|
|
113
|
-
// 4.
|
|
126
|
+
// 4. Correlation-id match (exact) — same fields as the list search path.
|
|
127
|
+
// Equality keeps it index-served (origin_id / workflow_id btree, id pk); the
|
|
128
|
+
// id arm binds a pre-parsed uuid so the PK can serve it, and metadata is
|
|
129
|
+
// searched precisely via the facets above, never a full-table text scan.
|
|
114
130
|
if (opts.search) {
|
|
115
131
|
const i = params.push(opts.search);
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
OR workflow_id ILIKE '%' || $${i} || '%' OR origin_id ILIKE '%' || $${i} || '%'
|
|
119
|
-
OR id::text ILIKE '%' || $${i} || '%' OR metadata::text ILIKE '%' || $${i} || '%')`);
|
|
132
|
+
const u = params.push(asUuidOrNull(opts.search));
|
|
133
|
+
clauses.push(`(origin_id = $${i} OR workflow_id = $${i} OR ($${u}::uuid IS NOT NULL AND id = $${u}::uuid))`);
|
|
120
134
|
}
|
|
121
135
|
const where = clauses.length ? `WHERE ${clauses.join('\n AND ')}` : '';
|
|
122
136
|
const orderBy = (0, facet_sql_1.buildFacetOrder)(opts.facet.orderBy);
|
|
@@ -152,13 +166,46 @@ async function listFacetKeys(opts) {
|
|
|
152
166
|
FROM public.lt_escalations
|
|
153
167
|
WHERE ${clauses.join('\n AND ')}
|
|
154
168
|
ORDER BY key`, params);
|
|
155
|
-
|
|
169
|
+
const actualKeys = new Set(rows.map((r) => r.key));
|
|
170
|
+
// Also surface keys declared in metadata_schema for visible roles — so the
|
|
171
|
+
// autocomplete offers expected keys even before any data has been created.
|
|
172
|
+
const roleFilter = opts.global
|
|
173
|
+
? null
|
|
174
|
+
: [...(opts.visibleRoles ?? []), ...(opts.selfRoles ?? [])];
|
|
175
|
+
if (roleFilter === null || roleFilter.length > 0) {
|
|
176
|
+
const schemaParam = roleFilter === null ? null : roleFilter;
|
|
177
|
+
const { rows: schemaRows } = await pool.query(`SELECT metadata_schema FROM lt_roles
|
|
178
|
+
WHERE metadata_schema IS NOT NULL
|
|
179
|
+
AND ($1::text[] IS NULL OR role = ANY($1))`, [schemaParam]);
|
|
180
|
+
for (const row of schemaRows) {
|
|
181
|
+
const schema = row.metadata_schema;
|
|
182
|
+
if (schema?.properties && typeof schema.properties === 'object') {
|
|
183
|
+
for (const key of Object.keys(schema.properties)) {
|
|
184
|
+
actualKeys.add(key);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return [...actualKeys].sort();
|
|
156
190
|
}
|
|
191
|
+
// Escalation stats back the home + overview surfaces and refresh on every
|
|
192
|
+
// escalation event. As of hotmesh 0.25.0 the SDK stats query reads three
|
|
193
|
+
// bounded, index-only sources (pending backlog, created window, resolved
|
|
194
|
+
// window), so it is cheap enough to run per-request — this short single-flight
|
|
195
|
+
// window exists purely to collapse the refetch burst a busy escalation stream
|
|
196
|
+
// triggers (every event → every open dashboard refetches at once). Local
|
|
197
|
+
// writes clear the cache (see crud.ts publishEscalationChange), so a refetch
|
|
198
|
+
// on this container always observes its own write.
|
|
199
|
+
const ESCALATION_STATS_CACHE_TTL_MS = 2_500;
|
|
200
|
+
const escalationStatsCache = new metrics_cache_1.TtlCache(ESCALATION_STATS_CACHE_TTL_MS);
|
|
157
201
|
async function getEscalationStats(visibleRoles, period) {
|
|
158
|
-
const
|
|
159
|
-
return
|
|
160
|
-
|
|
161
|
-
|
|
202
|
+
const key = `${period ?? '24h'}::${visibleRoles ? [...visibleRoles].sort().join(',') : 'ALL'}`;
|
|
203
|
+
return escalationStatsCache.resolve(key, async () => {
|
|
204
|
+
const client = await (0, client_1.escalations)();
|
|
205
|
+
return client.stats({
|
|
206
|
+
roles: visibleRoles,
|
|
207
|
+
period: period,
|
|
208
|
+
});
|
|
162
209
|
});
|
|
163
210
|
}
|
|
164
211
|
async function listDistinctTypes() {
|
|
@@ -266,3 +313,66 @@ async function listAvailableEscalations(filters) {
|
|
|
266
313
|
]);
|
|
267
314
|
return { escalations: (0, map_1.toEscalationRecords)(rows), total };
|
|
268
315
|
}
|
|
316
|
+
// Period metrics (percentiles + throughput) are expensive and slow-changing, so
|
|
317
|
+
// they are cached ~30s and shared, via single-flight, across the socket-driven
|
|
318
|
+
// refresh burst and all concurrent viewers. Live counts are never cached — they
|
|
319
|
+
// refresh on every event. Per-container cache; 30-60s staleness is acceptable.
|
|
320
|
+
const STATION_PERIOD_CACHE_TTL_MS = 30_000;
|
|
321
|
+
const stationPeriodCache = new metrics_cache_1.TtlCache(STATION_PERIOD_CACHE_TTL_MS);
|
|
322
|
+
/**
|
|
323
|
+
* Drop cached aggregates (period metrics + escalation stats). Called by the
|
|
324
|
+
* write paths in crud.ts alongside each escalation event publish: the event
|
|
325
|
+
* tells dashboards "data changed", so the very next fetch on this container
|
|
326
|
+
* must recompute rather than serve the pre-change aggregate for up to a TTL.
|
|
327
|
+
* Cross-container staleness within the TTL remains and is acceptable.
|
|
328
|
+
*/
|
|
329
|
+
function invalidateEscalationAggregates() {
|
|
330
|
+
stationPeriodCache.clear();
|
|
331
|
+
escalationStatsCache.clear();
|
|
332
|
+
}
|
|
333
|
+
/** Test hook: drop cached period metrics AND escalation stats so tests observe fresh rows. */
|
|
334
|
+
function resetStationMetricsCache() {
|
|
335
|
+
invalidateEscalationAggregates();
|
|
336
|
+
}
|
|
337
|
+
const toNum = (v) => (v != null ? Number(v) : null);
|
|
338
|
+
async function getStationMetrics(visibleRoles, period) {
|
|
339
|
+
await (0, client_1.ensureEscalationCompatView)();
|
|
340
|
+
// hasOwnProperty: a caller-supplied period like 'constructor' must fall back
|
|
341
|
+
// to 24h, not resolve to an inherited Object.prototype member.
|
|
342
|
+
const intervalStr = period && Object.prototype.hasOwnProperty.call(types_1.VALID_PERIODS, period)
|
|
343
|
+
? types_1.VALID_PERIODS[period]
|
|
344
|
+
: '24 hours';
|
|
345
|
+
const roles = visibleRoles ?? null;
|
|
346
|
+
const pool = (0, db_1.getPool)();
|
|
347
|
+
// Live counts: always fresh (cheap — bounded pending working set).
|
|
348
|
+
// Period metrics: cached ~30s, single-flight (expensive percentile sort).
|
|
349
|
+
const cacheKey = `${period ?? '24h'}::${roles ? [...roles].sort().join(',') : 'ALL'}`;
|
|
350
|
+
const [countsResult, periodRows] = await Promise.all([
|
|
351
|
+
pool.query(sql_1.STATION_LIVE_COUNTS_SQL, [roles]),
|
|
352
|
+
stationPeriodCache.resolve(cacheKey, async () => (await pool.query(sql_1.STATION_PERIOD_METRICS_SQL, [roles, intervalStr])).rows),
|
|
353
|
+
]);
|
|
354
|
+
const periodByRole = new Map(periodRows.map((r) => [r.role, r]));
|
|
355
|
+
return countsResult.rows.map((c) => {
|
|
356
|
+
const p = periodByRole.get(c.role);
|
|
357
|
+
return {
|
|
358
|
+
role: c.role,
|
|
359
|
+
pending: Number(c.pending ?? 0),
|
|
360
|
+
claimed: Number(c.claimed ?? 0),
|
|
361
|
+
resolved: Number(p?.resolved ?? 0),
|
|
362
|
+
in_arrears: Number(c.in_arrears ?? 0),
|
|
363
|
+
throughput_pct: p?.throughput_pct != null ? Number(p.throughput_pct) : null,
|
|
364
|
+
wait: {
|
|
365
|
+
p99: toNum(p?.p99_wait_min),
|
|
366
|
+
p50: toNum(p?.p50_wait_min),
|
|
367
|
+
avg: toNum(p?.avg_wait_min),
|
|
368
|
+
max: toNum(p?.max_wait_min),
|
|
369
|
+
},
|
|
370
|
+
work: {
|
|
371
|
+
p99: toNum(p?.p99_work_min),
|
|
372
|
+
p50: toNum(p?.p50_work_min),
|
|
373
|
+
avg: toNum(p?.avg_work_min),
|
|
374
|
+
max: toNum(p?.max_work_min),
|
|
375
|
+
},
|
|
376
|
+
};
|
|
377
|
+
});
|
|
378
|
+
}
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
* table. Clears both `assigned_until` and `claim_expires_at`.
|
|
9
9
|
*/
|
|
10
10
|
export declare const RELEASE_EXPIRED_CLAIMS = "UPDATE public.hmsh_escalations\nSET assigned_to = NULL,\n assigned_until = NULL,\n claimed_at = NULL,\n claim_expires_at = NULL,\n updated_at = NOW()\nWHERE status = 'pending'\n AND assigned_to IS NOT NULL\n AND assigned_until <= NOW()";
|
|
11
|
-
/** Count matching the search WHERE (params $1–$
|
|
12
|
-
export declare const COUNT_SEARCH_ESCALATIONS = "SELECT COUNT(*)::int AS total\n FROM public.lt_escalations\n WHERE ($1::text IS NULL OR status = $1)\n AND ($2::text IS NULL OR role = $2)\n AND (\n ($3::text[] IS NULL AND $10::text[] IS NULL)\n OR ($3::text[] IS NOT NULL AND role = ANY($3))\n OR ($10::text[] IS NOT NULL AND role = ANY($10) AND assigned_to = $11)\n )\n AND ($4::text IS NULL OR type = $4)\n AND ($5::text IS NULL OR subtype = $5)\n AND ($6::int IS NULL OR priority = $6)\n AND ($7::text IS NULL OR assigned_to = $7)\n AND ($8::boolean IS NULL OR available = $8)\n AND ($12::jsonb IS NULL OR metadata @> $12)\n AND ($9::text IS NULL OR
|
|
11
|
+
/** Count matching the search WHERE (params $1–$13). */
|
|
12
|
+
export declare const COUNT_SEARCH_ESCALATIONS = "SELECT COUNT(*)::int AS total\n FROM public.lt_escalations\n WHERE ($1::text IS NULL OR status = $1)\n AND ($2::text IS NULL OR role = $2)\n AND (\n ($3::text[] IS NULL AND $10::text[] IS NULL)\n OR ($3::text[] IS NOT NULL AND role = ANY($3))\n OR ($10::text[] IS NOT NULL AND role = ANY($10) AND assigned_to = $11)\n )\n AND ($4::text IS NULL OR type = $4)\n AND ($5::text IS NULL OR subtype = $5)\n AND ($6::int IS NULL OR priority = $6)\n AND ($7::text IS NULL OR assigned_to = $7)\n AND ($8::boolean IS NULL OR available = $8)\n AND ($12::jsonb IS NULL OR metadata @> $12)\n AND ($9::text IS NULL OR origin_id = $9 OR workflow_id = $9\n OR ($13::uuid IS NOT NULL AND id = $13::uuid))";
|
|
13
13
|
/**
|
|
14
14
|
* Build the search SELECT. `orderBy` is composed from a whitelist by the caller
|
|
15
15
|
* (never raw user input), so it is safe to interpolate; everything else is bound.
|
|
@@ -44,3 +44,30 @@ export declare function searchEscalationsQuery(orderBy: string): string;
|
|
|
44
44
|
* already assigned to them ($2). assigned_to is indexed, so the self-branch scales.
|
|
45
45
|
*/
|
|
46
46
|
export declare const RESOLVE_BY_METADATA_ATOMIC = "WITH target AS MATERIALIZED (\n SELECT *\n FROM public.hmsh_escalations\n WHERE metadata @> $1::jsonb\n AND status = 'pending'\n AND (\n $5::text[] IS NULL\n OR role = ANY($5)\n OR (role = ANY($6::text[]) AND assigned_to = $2)\n )\n ORDER BY priority ASC, created_at ASC\n LIMIT 1\n FOR UPDATE\n),\nclaimed AS (\n UPDATE public.hmsh_escalations e\n SET assigned_to = COALESCE(e.assigned_to, $2),\n claimed_at = COALESCE(e.claimed_at, NOW()),\n assigned_until = CASE\n WHEN e.assigned_to IS NOT NULL AND e.assigned_until > NOW() THEN e.assigned_until\n ELSE NOW() + INTERVAL '5 minutes' END,\n claim_expires_at = CASE\n WHEN e.assigned_to IS NOT NULL AND e.assigned_until > NOW() THEN e.claim_expires_at\n ELSE NOW() + INTERVAL '5 minutes' END,\n metadata = CASE WHEN $4::jsonb IS NOT NULL\n THEN COALESCE(e.metadata, '{}'::jsonb) || $4::jsonb\n ELSE e.metadata END,\n updated_at = NOW()\n FROM target\n WHERE e.id = target.id\n AND target.signal_key IS NULL\n RETURNING e.*\n),\nresolved AS (\n UPDATE public.hmsh_escalations e\n SET status = 'resolved',\n resolved_at = NOW(),\n resolver_payload = $3,\n updated_at = NOW()\n FROM claimed\n WHERE e.id = claimed.id\n AND (claimed.metadata->>'signal_id') IS NULL\n RETURNING e.*\n)\nSELECT\n resolved.*,\n target.id AS target_id,\n target.metadata->>'signal_id' AS signal_id,\n target.signal_key AS signal_key,\n target.workflow_id AS target_workflow_id,\n target.workflow_type AS target_workflow_type,\n target.task_queue AS target_task_queue,\n (target.assigned_to IS NOT NULL\n AND target.assigned_until IS NOT NULL\n AND target.assigned_until > NOW()) AS signal_already_claimed,\n CASE WHEN resolved.id IS NOT NULL THEN 'resolved' ELSE 'signal_required' END AS outcome\nFROM target\nLEFT JOIN resolved ON resolved.id = target.id";
|
|
47
|
+
/**
|
|
48
|
+
* Live counts — pending, active claims, and past-SLA (in_arrears) per role.
|
|
49
|
+
*
|
|
50
|
+
* $1 TEXT[] — role filter (NULL = all roles / superadmin).
|
|
51
|
+
*
|
|
52
|
+
* Hits idx_hmsh_esc_pending_role_created (role, created_at DESC) WHERE
|
|
53
|
+
* status='pending'. The pending working set is bounded by real backlog depth,
|
|
54
|
+
* so this stays cheap as historical volume grows — safe on the hot refresh path.
|
|
55
|
+
*/
|
|
56
|
+
export declare const STATION_LIVE_COUNTS_SQL = "\nWITH role_targets AS (\n SELECT role, sla_minutes\n FROM lt_roles\n WHERE ($1::text[] IS NULL OR role = ANY($1::text[]))\n),\nlive_counts AS (\n SELECT\n e.role,\n COUNT(*) AS pending,\n COUNT(*) FILTER (WHERE e.assigned_to IS NOT NULL AND e.assigned_until > NOW()) AS claimed,\n COUNT(*) FILTER (\n WHERE rt.sla_minutes IS NOT NULL\n AND e.created_at < NOW() - (rt.sla_minutes * INTERVAL '1 minute')\n ) AS in_arrears\n FROM hmsh_escalations e\n JOIN role_targets rt ON rt.role = e.role\n WHERE e.status = 'pending'\n GROUP BY e.role\n)\nSELECT\n rt.role,\n COALESCE(lc.pending, 0) AS pending,\n COALESCE(lc.claimed, 0) AS claimed,\n COALESCE(lc.in_arrears, 0) AS in_arrears\nFROM role_targets rt\nLEFT JOIN live_counts lc ON lc.role = rt.role\nORDER BY rt.role\n";
|
|
57
|
+
/**
|
|
58
|
+
* Period metrics — resolved count, throughput efficiency, and P99/P50/avg/max
|
|
59
|
+
* latency for wait (queue time) and work (processing) per role.
|
|
60
|
+
*
|
|
61
|
+
* $1 TEXT[] — role filter (NULL = all roles / superadmin).
|
|
62
|
+
* $2 INTERVAL — time window (e.g. '24 hours', '15 minutes').
|
|
63
|
+
*
|
|
64
|
+
* Hits idx_hmsh_esc_resolved_cover (role, resolved_at DESC, claimed_at,
|
|
65
|
+
* created_at) WHERE status='resolved' — a covering index, so the percentile
|
|
66
|
+
* scan is index-only. PERCENTILE_CONT still sorts the qualifying set in memory,
|
|
67
|
+
* which is why the service caches this result rather than running it per event.
|
|
68
|
+
*
|
|
69
|
+
* Latency is computed over CLOSED work (status='resolved' in the period), not a
|
|
70
|
+
* mix of resolved + in-flight — a clean "how did completed work perform?" read.
|
|
71
|
+
* throughput_pct is NULL when the role has no target_per_hour (NULLIF guard).
|
|
72
|
+
*/
|
|
73
|
+
export declare const STATION_PERIOD_METRICS_SQL = "\nWITH role_targets AS (\n SELECT role, target_per_hour\n FROM lt_roles\n WHERE ($1::text[] IS NULL OR role = ANY($1::text[]))\n),\nperiod_metrics AS (\n SELECT\n e.role,\n COUNT(*) AS resolved,\n ROUND((PERCENTILE_CONT(0.99) WITHIN GROUP (\n ORDER BY EXTRACT(EPOCH FROM (e.claimed_at - e.created_at)) / 60)\n FILTER (WHERE e.claimed_at IS NOT NULL))::numeric, 3) AS p99_wait_min,\n ROUND((PERCENTILE_CONT(0.50) WITHIN GROUP (\n ORDER BY EXTRACT(EPOCH FROM (e.claimed_at - e.created_at)) / 60)\n FILTER (WHERE e.claimed_at IS NOT NULL))::numeric, 3) AS p50_wait_min,\n ROUND((AVG(EXTRACT(EPOCH FROM (e.claimed_at - e.created_at)) / 60)\n FILTER (WHERE e.claimed_at IS NOT NULL))::numeric, 3) AS avg_wait_min,\n ROUND((MAX(EXTRACT(EPOCH FROM (e.claimed_at - e.created_at)) / 60)\n FILTER (WHERE e.claimed_at IS NOT NULL))::numeric, 3) AS max_wait_min,\n ROUND((PERCENTILE_CONT(0.99) WITHIN GROUP (\n ORDER BY EXTRACT(EPOCH FROM (e.resolved_at - e.claimed_at)) / 60)\n FILTER (WHERE e.claimed_at IS NOT NULL))::numeric, 3) AS p99_work_min,\n ROUND((PERCENTILE_CONT(0.50) WITHIN GROUP (\n ORDER BY EXTRACT(EPOCH FROM (e.resolved_at - e.claimed_at)) / 60)\n FILTER (WHERE e.claimed_at IS NOT NULL))::numeric, 3) AS p50_work_min,\n ROUND((AVG(EXTRACT(EPOCH FROM (e.resolved_at - e.claimed_at)) / 60)\n FILTER (WHERE e.claimed_at IS NOT NULL))::numeric, 3) AS avg_work_min,\n ROUND((MAX(EXTRACT(EPOCH FROM (e.resolved_at - e.claimed_at)) / 60)\n FILTER (WHERE e.claimed_at IS NOT NULL))::numeric, 3) AS max_work_min\n FROM hmsh_escalations e\n WHERE e.status = 'resolved'\n AND e.resolved_at >= NOW() - $2::interval\n AND ($1::text[] IS NULL OR e.role = ANY($1::text[]))\n GROUP BY e.role\n)\nSELECT\n rt.role,\n COALESCE(pm.resolved, 0) AS resolved,\n ROUND(\n (COALESCE(pm.resolved, 0)::numeric\n / NULLIF(rt.target_per_hour::numeric * EXTRACT(EPOCH FROM $2::interval) / 3600.0, 0)\n * 100\n )::numeric, 1\n ) AS throughput_pct,\n pm.p99_wait_min,\n pm.p50_wait_min,\n pm.avg_wait_min,\n pm.max_wait_min,\n pm.p99_work_min,\n pm.p50_work_min,\n pm.avg_work_min,\n pm.max_work_min\nFROM role_targets rt\nLEFT JOIN period_metrics pm ON pm.role = rt.role\nORDER BY rt.role\n";
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
// `hmsh_escalations` here rather than composed from generic SDK calls.
|
|
12
12
|
// ---------------------------------------------------------------------------
|
|
13
13
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
-
exports.RESOLVE_BY_METADATA_ATOMIC = exports.COUNT_SEARCH_ESCALATIONS = exports.RELEASE_EXPIRED_CLAIMS = void 0;
|
|
14
|
+
exports.STATION_PERIOD_METRICS_SQL = exports.STATION_LIVE_COUNTS_SQL = exports.RESOLVE_BY_METADATA_ATOMIC = exports.COUNT_SEARCH_ESCALATIONS = exports.RELEASE_EXPIRED_CLAIMS = void 0;
|
|
15
15
|
exports.searchEscalationsQuery = searchEscalationsQuery;
|
|
16
16
|
/**
|
|
17
17
|
* Sweep expired claims back to the available pool.
|
|
@@ -33,19 +33,20 @@ WHERE status = 'pending'
|
|
|
33
33
|
AND assigned_to IS NOT NULL
|
|
34
34
|
AND assigned_until <= NOW()`;
|
|
35
35
|
/**
|
|
36
|
-
*
|
|
36
|
+
* Escalation search by correlation id.
|
|
37
37
|
*
|
|
38
|
-
* The SDK's `client.list()` filters by structured columns only
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
* `
|
|
43
|
-
*
|
|
44
|
-
*
|
|
38
|
+
* The SDK's `client.list()` filters by structured columns only. Long-tail adds a
|
|
39
|
+
* search box that resolves a correlation id — the escalation id, its workflow id,
|
|
40
|
+
* or the origin id (order/ticket) — to the matching rows across the WHOLE result
|
|
41
|
+
* set, not just the current page. This is exact-match (equality), so it stays
|
|
42
|
+
* index-served: `origin_id` and `workflow_id` each have a btree index and `id` is
|
|
43
|
+
* the primary key. It deliberately does NOT run substring `ILIKE` over free text
|
|
44
|
+
* or `metadata::text` — a leading-wildcard ILIKE cannot use any index and would
|
|
45
|
+
* sequentially scan (and cast to text) every row, unbounded by history size.
|
|
45
46
|
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
47
|
+
* Metadata is searched precisely through the GIN-served `metadata @> $12`
|
|
48
|
+
* containment path instead (see `searchEscalationsFaceted`), which needs no status
|
|
49
|
+
* filter — e.g. "every escalation for order X, any status" is one indexed lookup.
|
|
49
50
|
*
|
|
50
51
|
* Role-scope visibility ($3 allRoles, $10 selfRoles, $11 meUserId):
|
|
51
52
|
* - global access → $3 NULL and $10 NULL → no role filter (sees everything)
|
|
@@ -56,9 +57,15 @@ WHERE status = 'pending'
|
|
|
56
57
|
* (idx_lt_escalations_assigned), so the self-branch is index-served at scale.
|
|
57
58
|
*
|
|
58
59
|
* $1 status, $2 role, $3 allRoles (text[]), $4 type, $5 subtype, $6 priority,
|
|
59
|
-
* $7 assigned_to, $8 available (bool), $9 search
|
|
60
|
-
* $11 meUserId, $12 metadata (jsonb containment,
|
|
61
|
-
*
|
|
60
|
+
* $7 assigned_to, $8 available (bool), $9 search (exact workflow_id/origin_id
|
|
61
|
+
* match), $10 selfRoles (text[]), $11 meUserId, $12 metadata (jsonb containment,
|
|
62
|
+
* GIN-served), $13 search-as-uuid (the search term when it parses as a UUID,
|
|
63
|
+
* else NULL). The list query adds $14 limit, $15 offset.
|
|
64
|
+
*
|
|
65
|
+
* The id arm binds $13::uuid instead of comparing id::text = $9: casting the
|
|
66
|
+
* uuid COLUMN to text has no index path, and one unindexable arm forces the
|
|
67
|
+
* whole OR off BitmapOr into a full scan of escalation history. With the
|
|
68
|
+
* app-side parse, all three arms are index-served (origin/workflow btrees, PK).
|
|
62
69
|
*
|
|
63
70
|
* `metadata @> $12` is exact JSONB containment (the GIN index serves it) — this is
|
|
64
71
|
* how findByMetadata routes through the scoped query so its role-scope filter and
|
|
@@ -79,24 +86,16 @@ const SEARCH_ESCALATIONS_WHERE = `\
|
|
|
79
86
|
AND ($7::text IS NULL OR assigned_to = $7)
|
|
80
87
|
AND ($8::boolean IS NULL OR available = $8)
|
|
81
88
|
AND ($12::jsonb IS NULL OR metadata @> $12)
|
|
82
|
-
AND ($9::text IS NULL OR
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
OR subtype ILIKE '%' || $9 || '%'
|
|
86
|
-
OR role ILIKE '%' || $9 || '%'
|
|
87
|
-
OR workflow_id ILIKE '%' || $9 || '%'
|
|
88
|
-
OR origin_id ILIKE '%' || $9 || '%'
|
|
89
|
-
OR id::text ILIKE '%' || $9 || '%'
|
|
90
|
-
OR metadata::text ILIKE '%' || $9 || '%'
|
|
91
|
-
))`;
|
|
92
|
-
/** Count matching the search WHERE (params $1–$12). */
|
|
89
|
+
AND ($9::text IS NULL OR origin_id = $9 OR workflow_id = $9
|
|
90
|
+
OR ($13::uuid IS NOT NULL AND id = $13::uuid))`;
|
|
91
|
+
/** Count matching the search WHERE (params $1–$13). */
|
|
93
92
|
exports.COUNT_SEARCH_ESCALATIONS = `SELECT COUNT(*)::int AS total\n${SEARCH_ESCALATIONS_WHERE}`;
|
|
94
93
|
/**
|
|
95
94
|
* Build the search SELECT. `orderBy` is composed from a whitelist by the caller
|
|
96
95
|
* (never raw user input), so it is safe to interpolate; everything else is bound.
|
|
97
96
|
*/
|
|
98
97
|
function searchEscalationsQuery(orderBy) {
|
|
99
|
-
return `SELECT *\n${SEARCH_ESCALATIONS_WHERE}\n ORDER BY ${orderBy}\n LIMIT $
|
|
98
|
+
return `SELECT *\n${SEARCH_ESCALATIONS_WHERE}\n ORDER BY ${orderBy}\n LIMIT $14 OFFSET $15`;
|
|
100
99
|
}
|
|
101
100
|
/**
|
|
102
101
|
* Atomic resolve by metadata with signal guard.
|
|
@@ -185,3 +184,133 @@ SELECT
|
|
|
185
184
|
CASE WHEN resolved.id IS NOT NULL THEN 'resolved' ELSE 'signal_required' END AS outcome
|
|
186
185
|
FROM target
|
|
187
186
|
LEFT JOIN resolved ON resolved.id = target.id`;
|
|
187
|
+
// ---------------------------------------------------------------------------
|
|
188
|
+
// Station metrics — split into two queries with very different cost profiles.
|
|
189
|
+
//
|
|
190
|
+
// The Operations dashboard refreshes on every escalation socket event (a
|
|
191
|
+
// debounced burst) plus a periodic fallback. Computing percentiles on that hot
|
|
192
|
+
// path is wasteful: percentile aggregates sort every qualifying resolved row,
|
|
193
|
+
// while the live counts are cheap and bounded. So the two halves are separated:
|
|
194
|
+
//
|
|
195
|
+
// STATION_LIVE_COUNTS_SQL — pending / claimed / in_arrears. Cheap, always
|
|
196
|
+
// run fresh. Scans only pending rows (backlog is
|
|
197
|
+
// bounded no matter how much history accrues).
|
|
198
|
+
// STATION_PERIOD_METRICS_SQL — resolved / throughput / latency percentiles.
|
|
199
|
+
// Expensive, slow-changing; the service layer
|
|
200
|
+
// caches it (~30s) and shares it across the
|
|
201
|
+
// refresh burst and all concurrent viewers.
|
|
202
|
+
//
|
|
203
|
+
// Both drive off role_targets so every configured role appears even when idle,
|
|
204
|
+
// and the service merges them by role into one StationMetric per station.
|
|
205
|
+
// ---------------------------------------------------------------------------
|
|
206
|
+
/**
|
|
207
|
+
* Live counts — pending, active claims, and past-SLA (in_arrears) per role.
|
|
208
|
+
*
|
|
209
|
+
* $1 TEXT[] — role filter (NULL = all roles / superadmin).
|
|
210
|
+
*
|
|
211
|
+
* Hits idx_hmsh_esc_pending_role_created (role, created_at DESC) WHERE
|
|
212
|
+
* status='pending'. The pending working set is bounded by real backlog depth,
|
|
213
|
+
* so this stays cheap as historical volume grows — safe on the hot refresh path.
|
|
214
|
+
*/
|
|
215
|
+
exports.STATION_LIVE_COUNTS_SQL = `
|
|
216
|
+
WITH role_targets AS (
|
|
217
|
+
SELECT role, sla_minutes
|
|
218
|
+
FROM lt_roles
|
|
219
|
+
WHERE ($1::text[] IS NULL OR role = ANY($1::text[]))
|
|
220
|
+
),
|
|
221
|
+
live_counts AS (
|
|
222
|
+
SELECT
|
|
223
|
+
e.role,
|
|
224
|
+
COUNT(*) AS pending,
|
|
225
|
+
COUNT(*) FILTER (WHERE e.assigned_to IS NOT NULL AND e.assigned_until > NOW()) AS claimed,
|
|
226
|
+
COUNT(*) FILTER (
|
|
227
|
+
WHERE rt.sla_minutes IS NOT NULL
|
|
228
|
+
AND e.created_at < NOW() - (rt.sla_minutes * INTERVAL '1 minute')
|
|
229
|
+
) AS in_arrears
|
|
230
|
+
FROM hmsh_escalations e
|
|
231
|
+
JOIN role_targets rt ON rt.role = e.role
|
|
232
|
+
WHERE e.status = 'pending'
|
|
233
|
+
GROUP BY e.role
|
|
234
|
+
)
|
|
235
|
+
SELECT
|
|
236
|
+
rt.role,
|
|
237
|
+
COALESCE(lc.pending, 0) AS pending,
|
|
238
|
+
COALESCE(lc.claimed, 0) AS claimed,
|
|
239
|
+
COALESCE(lc.in_arrears, 0) AS in_arrears
|
|
240
|
+
FROM role_targets rt
|
|
241
|
+
LEFT JOIN live_counts lc ON lc.role = rt.role
|
|
242
|
+
ORDER BY rt.role
|
|
243
|
+
`;
|
|
244
|
+
/**
|
|
245
|
+
* Period metrics — resolved count, throughput efficiency, and P99/P50/avg/max
|
|
246
|
+
* latency for wait (queue time) and work (processing) per role.
|
|
247
|
+
*
|
|
248
|
+
* $1 TEXT[] — role filter (NULL = all roles / superadmin).
|
|
249
|
+
* $2 INTERVAL — time window (e.g. '24 hours', '15 minutes').
|
|
250
|
+
*
|
|
251
|
+
* Hits idx_hmsh_esc_resolved_cover (role, resolved_at DESC, claimed_at,
|
|
252
|
+
* created_at) WHERE status='resolved' — a covering index, so the percentile
|
|
253
|
+
* scan is index-only. PERCENTILE_CONT still sorts the qualifying set in memory,
|
|
254
|
+
* which is why the service caches this result rather than running it per event.
|
|
255
|
+
*
|
|
256
|
+
* Latency is computed over CLOSED work (status='resolved' in the period), not a
|
|
257
|
+
* mix of resolved + in-flight — a clean "how did completed work perform?" read.
|
|
258
|
+
* throughput_pct is NULL when the role has no target_per_hour (NULLIF guard).
|
|
259
|
+
*/
|
|
260
|
+
exports.STATION_PERIOD_METRICS_SQL = `
|
|
261
|
+
WITH role_targets AS (
|
|
262
|
+
SELECT role, target_per_hour
|
|
263
|
+
FROM lt_roles
|
|
264
|
+
WHERE ($1::text[] IS NULL OR role = ANY($1::text[]))
|
|
265
|
+
),
|
|
266
|
+
period_metrics AS (
|
|
267
|
+
SELECT
|
|
268
|
+
e.role,
|
|
269
|
+
COUNT(*) AS resolved,
|
|
270
|
+
ROUND((PERCENTILE_CONT(0.99) WITHIN GROUP (
|
|
271
|
+
ORDER BY EXTRACT(EPOCH FROM (e.claimed_at - e.created_at)) / 60)
|
|
272
|
+
FILTER (WHERE e.claimed_at IS NOT NULL))::numeric, 3) AS p99_wait_min,
|
|
273
|
+
ROUND((PERCENTILE_CONT(0.50) WITHIN GROUP (
|
|
274
|
+
ORDER BY EXTRACT(EPOCH FROM (e.claimed_at - e.created_at)) / 60)
|
|
275
|
+
FILTER (WHERE e.claimed_at IS NOT NULL))::numeric, 3) AS p50_wait_min,
|
|
276
|
+
ROUND((AVG(EXTRACT(EPOCH FROM (e.claimed_at - e.created_at)) / 60)
|
|
277
|
+
FILTER (WHERE e.claimed_at IS NOT NULL))::numeric, 3) AS avg_wait_min,
|
|
278
|
+
ROUND((MAX(EXTRACT(EPOCH FROM (e.claimed_at - e.created_at)) / 60)
|
|
279
|
+
FILTER (WHERE e.claimed_at IS NOT NULL))::numeric, 3) AS max_wait_min,
|
|
280
|
+
ROUND((PERCENTILE_CONT(0.99) WITHIN GROUP (
|
|
281
|
+
ORDER BY EXTRACT(EPOCH FROM (e.resolved_at - e.claimed_at)) / 60)
|
|
282
|
+
FILTER (WHERE e.claimed_at IS NOT NULL))::numeric, 3) AS p99_work_min,
|
|
283
|
+
ROUND((PERCENTILE_CONT(0.50) WITHIN GROUP (
|
|
284
|
+
ORDER BY EXTRACT(EPOCH FROM (e.resolved_at - e.claimed_at)) / 60)
|
|
285
|
+
FILTER (WHERE e.claimed_at IS NOT NULL))::numeric, 3) AS p50_work_min,
|
|
286
|
+
ROUND((AVG(EXTRACT(EPOCH FROM (e.resolved_at - e.claimed_at)) / 60)
|
|
287
|
+
FILTER (WHERE e.claimed_at IS NOT NULL))::numeric, 3) AS avg_work_min,
|
|
288
|
+
ROUND((MAX(EXTRACT(EPOCH FROM (e.resolved_at - e.claimed_at)) / 60)
|
|
289
|
+
FILTER (WHERE e.claimed_at IS NOT NULL))::numeric, 3) AS max_work_min
|
|
290
|
+
FROM hmsh_escalations e
|
|
291
|
+
WHERE e.status = 'resolved'
|
|
292
|
+
AND e.resolved_at >= NOW() - $2::interval
|
|
293
|
+
AND ($1::text[] IS NULL OR e.role = ANY($1::text[]))
|
|
294
|
+
GROUP BY e.role
|
|
295
|
+
)
|
|
296
|
+
SELECT
|
|
297
|
+
rt.role,
|
|
298
|
+
COALESCE(pm.resolved, 0) AS resolved,
|
|
299
|
+
ROUND(
|
|
300
|
+
(COALESCE(pm.resolved, 0)::numeric
|
|
301
|
+
/ NULLIF(rt.target_per_hour::numeric * EXTRACT(EPOCH FROM $2::interval) / 3600.0, 0)
|
|
302
|
+
* 100
|
|
303
|
+
)::numeric, 1
|
|
304
|
+
) AS throughput_pct,
|
|
305
|
+
pm.p99_wait_min,
|
|
306
|
+
pm.p50_wait_min,
|
|
307
|
+
pm.avg_wait_min,
|
|
308
|
+
pm.max_wait_min,
|
|
309
|
+
pm.p99_work_min,
|
|
310
|
+
pm.p50_work_min,
|
|
311
|
+
pm.avg_work_min,
|
|
312
|
+
pm.max_work_min
|
|
313
|
+
FROM role_targets rt
|
|
314
|
+
LEFT JOIN period_metrics pm ON pm.role = rt.role
|
|
315
|
+
ORDER BY rt.role
|
|
316
|
+
`;
|
|
@@ -39,5 +39,22 @@ export interface EscalationStats {
|
|
|
39
39
|
}[];
|
|
40
40
|
}
|
|
41
41
|
export declare const VALID_PERIODS: Record<string, string>;
|
|
42
|
+
export interface StationMetricPeriod {
|
|
43
|
+
p99: number | null;
|
|
44
|
+
p50: number | null;
|
|
45
|
+
avg: number | null;
|
|
46
|
+
max: number | null;
|
|
47
|
+
}
|
|
48
|
+
export interface StationMetric {
|
|
49
|
+
role: string;
|
|
50
|
+
pending: number;
|
|
51
|
+
claimed: number;
|
|
52
|
+
resolved: number;
|
|
53
|
+
in_arrears: number;
|
|
54
|
+
/** resolved_in_period / (target_per_hour × period_hours) × 100. Null when no target set. */
|
|
55
|
+
throughput_pct: number | null;
|
|
56
|
+
wait: StationMetricPeriod;
|
|
57
|
+
work: StationMetricPeriod;
|
|
58
|
+
}
|
|
42
59
|
/** Columns allowed for user-chosen ORDER BY. */
|
|
43
60
|
export declare const SORTABLE_COLUMNS: Set<string>;
|
|
@@ -75,8 +75,12 @@ function isToolAllowed(toolName, serverName, exposure, callerScopes) {
|
|
|
75
75
|
const manifest = config?.toolManifest;
|
|
76
76
|
if (!manifest)
|
|
77
77
|
return true; // no manifest = allow (conservative)
|
|
78
|
+
// Fail closed: a server that publishes a manifest declares its read-safe
|
|
79
|
+
// surface completely. A tool missing from the manifest is treated as a
|
|
80
|
+
// write tool — otherwise every newly registered tool ships pre-exposed to
|
|
81
|
+
// read-scoped callers until someone remembers the manifest entry.
|
|
78
82
|
const entry = manifest.find((t) => t.name === toolName);
|
|
79
|
-
return entry?.read_safe
|
|
83
|
+
return entry?.read_safe === true;
|
|
80
84
|
}
|
|
81
85
|
// ── Unified server creation ──────────────────────────────────────────────────
|
|
82
86
|
/**
|
|
@@ -23,6 +23,26 @@ import type { Types } from '@hotmeshio/hotmesh';
|
|
|
23
23
|
* });
|
|
24
24
|
* ```
|
|
25
25
|
*
|
|
26
|
+
* **With an SLA (hotmesh 0.25.1+) — add `timeout` to the config.** The same
|
|
27
|
+
* single wait arms a resume timer alongside the escalation row: when the timer
|
|
28
|
+
* fires first, this helper returns `false`, the row transitions to
|
|
29
|
+
* `status='expired'` (engine-side, atomically), and a late resolve fails as
|
|
30
|
+
* `already-expired`. A signal that arrives first resolves normally and the
|
|
31
|
+
* timer is inert.
|
|
32
|
+
*
|
|
33
|
+
* ```typescript
|
|
34
|
+
* const decision = await conditionLT<{ approved: boolean }>(signalId, {
|
|
35
|
+
* role: 'reviewer',
|
|
36
|
+
* description: instructions,
|
|
37
|
+
* metadata: { orderId },
|
|
38
|
+
* timeout: '24h', // SLA deadline for this worklist row
|
|
39
|
+
* });
|
|
40
|
+
* if (decision === false) {
|
|
41
|
+
* // deadline passed — the row is already status='expired'; branch to the
|
|
42
|
+
* // fallback path (auto-reject, escalate the order, notify, …)
|
|
43
|
+
* }
|
|
44
|
+
* ```
|
|
45
|
+
*
|
|
26
46
|
* **Legacy (two-step) — no config.** Create the escalation first (e.g. via
|
|
27
47
|
* `ltCreateEscalation`) with `signal_id`/`signal_routing` metadata, then wait.
|
|
28
48
|
* On resume the signal payload carries an injected `$escalation_id`; this helper
|
|
@@ -61,6 +61,26 @@ const LT_ACTIVITY_QUEUE = 'lt-interceptor';
|
|
|
61
61
|
* });
|
|
62
62
|
* ```
|
|
63
63
|
*
|
|
64
|
+
* **With an SLA (hotmesh 0.25.1+) — add `timeout` to the config.** The same
|
|
65
|
+
* single wait arms a resume timer alongside the escalation row: when the timer
|
|
66
|
+
* fires first, this helper returns `false`, the row transitions to
|
|
67
|
+
* `status='expired'` (engine-side, atomically), and a late resolve fails as
|
|
68
|
+
* `already-expired`. A signal that arrives first resolves normally and the
|
|
69
|
+
* timer is inert.
|
|
70
|
+
*
|
|
71
|
+
* ```typescript
|
|
72
|
+
* const decision = await conditionLT<{ approved: boolean }>(signalId, {
|
|
73
|
+
* role: 'reviewer',
|
|
74
|
+
* description: instructions,
|
|
75
|
+
* metadata: { orderId },
|
|
76
|
+
* timeout: '24h', // SLA deadline for this worklist row
|
|
77
|
+
* });
|
|
78
|
+
* if (decision === false) {
|
|
79
|
+
* // deadline passed — the row is already status='expired'; branch to the
|
|
80
|
+
* // fallback path (auto-reject, escalate the order, notify, …)
|
|
81
|
+
* }
|
|
82
|
+
* ```
|
|
83
|
+
*
|
|
64
84
|
* **Legacy (two-step) — no config.** Create the escalation first (e.g. via
|
|
65
85
|
* `ltCreateEscalation`) with `signal_id`/`signal_routing` metadata, then wait.
|
|
66
86
|
* On resume the signal payload carries an injected `$escalation_id`; this helper
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { EscalationChain, RoleDetail } from './types';
|
|
1
|
+
import type { EscalationChain, RoleDetail, UpdateRoleInput } from './types';
|
|
2
2
|
/**
|
|
3
3
|
* Get the roles a given source role can escalate to.
|
|
4
4
|
*/
|
|
@@ -35,9 +35,28 @@ export declare function listDistinctRoles(): Promise<string[]>;
|
|
|
35
35
|
*/
|
|
36
36
|
export declare function listRolesWithDetails(): Promise<RoleDetail[]>;
|
|
37
37
|
/**
|
|
38
|
-
* Create a standalone role entry.
|
|
38
|
+
* Create a standalone role entry. Returns true when the role was created by
|
|
39
|
+
* this call, false when it already existed — seeders use this to write
|
|
40
|
+
* default metadata exactly once and leave later admin edits alone.
|
|
39
41
|
*/
|
|
40
|
-
export declare function createRole(role: string): Promise<
|
|
42
|
+
export declare function createRole(role: string): Promise<boolean>;
|
|
43
|
+
/**
|
|
44
|
+
* Update role metadata with PATCH semantics: a field omitted from the input
|
|
45
|
+
* (undefined) keeps its current value; explicit null clears it (properties
|
|
46
|
+
* resets to {}). One atomic UPDATE — see UPDATE_ROLE_METADATA.
|
|
47
|
+
*/
|
|
48
|
+
export declare function updateRoleMetadata(role: string, input: UpdateRoleInput): Promise<RoleDetail | null>;
|
|
49
|
+
/**
|
|
50
|
+
* Fetch form_schema for a role (used as default escalation form when workflow
|
|
51
|
+
* config does not specify a resolver_schema).
|
|
52
|
+
*/
|
|
53
|
+
export declare function getRoleFormSchema(role: string): Promise<Record<string, any> | null>;
|
|
54
|
+
/**
|
|
55
|
+
* Fetch metadata_schema for a role. Used at escalation creation time to
|
|
56
|
+
* validate the caller-supplied metadata bag, and by the faceted-query UI
|
|
57
|
+
* to surface expected keys before any data exists.
|
|
58
|
+
*/
|
|
59
|
+
export declare function getRoleMetadataSchema(role: string): Promise<Record<string, any> | null>;
|
|
41
60
|
/**
|
|
42
61
|
* Delete a role. Returns an error message if the role is referenced.
|
|
43
62
|
*/
|