@hotmeshio/long-tail 0.5.12 → 0.6.2
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/README.md +1 -0
- package/build/api/escalations/bulk.js +9 -1
- package/build/api/escalations/cancel.js +5 -7
- package/build/api/escalations/claim.js +18 -15
- package/build/api/escalations/create.d.ts +6 -0
- package/build/api/escalations/create.js +11 -8
- package/build/api/escalations/facets.d.ts +18 -0
- package/build/api/escalations/facets.js +115 -0
- package/build/api/escalations/helpers.d.ts +70 -1
- package/build/api/escalations/helpers.js +101 -7
- package/build/api/escalations/index.d.ts +3 -2
- package/build/api/escalations/index.js +7 -1
- package/build/api/escalations/list.d.ts +21 -2
- package/build/api/escalations/list.js +109 -10
- package/build/api/escalations/metadata.js +22 -27
- package/build/api/escalations/resolve.d.ts +19 -1
- package/build/api/escalations/resolve.js +111 -26
- package/build/api/escalations/single.js +8 -8
- package/build/api/users.d.ts +6 -0
- package/build/api/users.js +29 -1
- package/build/bin/ltc.js +40 -0
- package/build/lib/cli/commands/escalations.d.ts +36 -0
- package/build/lib/cli/commands/escalations.js +98 -0
- package/build/lib/db/schemas/001_schema.sql +10 -5
- package/build/lib/db/schemas/012_lt_tasks_workflow_id_unique.sql +20 -0
- package/build/lib/db/schemas/013_role_scope.sql +37 -0
- package/build/modules/version.d.ts +10 -5
- package/build/modules/version.js +27 -11
- package/build/routes/escalations/facets.d.ts +7 -0
- package/build/routes/escalations/facets.js +81 -0
- package/build/routes/escalations/index.js +3 -0
- package/build/routes/escalations/list.js +46 -0
- package/build/routes/escalations/resolve.js +22 -4
- package/build/routes/users.js +9 -5
- package/build/sdk/index.d.ts +33 -2
- package/build/sdk/index.js +7 -0
- package/build/services/escalation/crud.d.ts +56 -6
- package/build/services/escalation/crud.js +102 -31
- package/build/services/escalation/facet-sql.d.ts +18 -0
- package/build/services/escalation/facet-sql.js +118 -0
- package/build/services/escalation/facets.d.ts +45 -0
- package/build/services/escalation/facets.js +218 -0
- package/build/services/escalation/index.d.ts +1 -0
- package/build/services/escalation/index.js +8 -1
- package/build/services/escalation/queries.d.ts +45 -0
- package/build/services/escalation/queries.js +107 -6
- package/build/services/escalation/sql.d.ts +10 -4
- package/build/services/escalation/sql.js +36 -8
- package/build/services/interceptor/activities/escalation.d.ts +2 -0
- package/build/services/interceptor/activities/escalation.js +1 -1
- package/build/services/interceptor/lifecycle.js +4 -0
- package/build/services/mcp/server-tools.js +10 -4
- package/build/services/orchestrator/condition.js +6 -2
- package/build/services/role/index.js +2 -3
- package/build/services/role/sql.d.ts +8 -0
- package/build/services/role/sql.js +17 -1
- package/build/services/task/crud.js +22 -14
- package/build/services/task/process.js +3 -21
- package/build/services/task/sql.d.ts +24 -1
- package/build/services/task/sql.js +65 -1
- package/build/services/user/crud.d.ts +1 -0
- package/build/services/user/crud.js +29 -11
- package/build/services/user/index.d.ts +2 -1
- package/build/services/user/index.js +12 -1
- package/build/services/user/rbac.d.ts +18 -0
- package/build/services/user/rbac.js +27 -0
- package/build/services/user/roles.d.ts +7 -2
- package/build/services/user/roles.js +21 -5
- package/build/services/user/scope.d.ts +22 -0
- package/build/services/user/scope.js +50 -0
- package/build/services/user/seed-admin.js +34 -17
- package/build/services/user/sql.d.ts +18 -7
- package/build/services/user/sql.js +38 -13
- package/build/services/user/sso-provision.js +41 -24
- package/build/services/user/types.d.ts +9 -5
- package/build/system/mcp-servers/admin/escalations.js +65 -0
- package/build/system/mcp-servers/admin/schemas.d.ts +325 -0
- package/build/system/mcp-servers/admin/schemas.js +63 -2
- package/build/system/mcp-servers/admin/users.js +28 -2
- package/build/tsconfig.tsbuildinfo +1 -1
- package/build/types/envelope.d.ts +7 -0
- package/build/types/facets.d.ts +60 -0
- package/build/types/facets.js +10 -0
- package/build/types/index.d.ts +2 -1
- package/build/types/user.d.ts +11 -0
- package/dashboard/dist/assets/AdminDashboard-C5gfVEeL.js +2 -0
- package/dashboard/dist/assets/{AdminDashboard-LTiYLuzq.js.map → AdminDashboard-C5gfVEeL.js.map} +1 -1
- package/dashboard/dist/assets/{AgentConfigPage-nlOudfpZ.js → AgentConfigPage-CWEnuiGN.js} +6 -6
- package/dashboard/dist/assets/{AgentConfigPage-nlOudfpZ.js.map → AgentConfigPage-CWEnuiGN.js.map} +1 -1
- package/dashboard/dist/assets/{AgentDetailPage-BSUnDvGp.js → AgentDetailPage-B6IE27iK.js} +3 -3
- package/dashboard/dist/assets/AgentDetailPage-B6IE27iK.js.map +1 -0
- package/dashboard/dist/assets/AgentsPage-Blhh2RsK.js +2 -0
- package/dashboard/dist/assets/{AgentsPage-DpS0cCYe.js.map → AgentsPage-Blhh2RsK.js.map} +1 -1
- package/dashboard/dist/assets/AvailableEscalationsPage-ClwAle76.js +2 -0
- package/dashboard/dist/assets/AvailableEscalationsPage-ClwAle76.js.map +1 -0
- package/dashboard/dist/assets/{BotPicker-BhKmq7M7.js → BotPicker-DSozotGk.js} +2 -2
- package/dashboard/dist/assets/{BotPicker-BhKmq7M7.js.map → BotPicker-DSozotGk.js.map} +1 -1
- package/dashboard/dist/assets/CapabilitiesPage-CrS_DTA1.js +2 -0
- package/dashboard/dist/assets/CapabilitiesPage-CrS_DTA1.js.map +1 -0
- package/dashboard/dist/assets/{CollapsibleSection-D7SVwNVk.js → CollapsibleSection-1i6bOQ1L.js} +2 -2
- package/dashboard/dist/assets/{CollapsibleSection-D7SVwNVk.js.map → CollapsibleSection-1i6bOQ1L.js.map} +1 -1
- package/dashboard/dist/assets/CopyableId-blg-gZvT.js +2 -0
- package/dashboard/dist/assets/CopyableId-blg-gZvT.js.map +1 -0
- package/dashboard/dist/assets/{CredentialsPage-fat5Z2U9.js → CredentialsPage-BxJio06E.js} +2 -2
- package/dashboard/dist/assets/{CredentialsPage-fat5Z2U9.js.map → CredentialsPage-BxJio06E.js.map} +1 -1
- package/dashboard/dist/assets/{CronLabel-OOP4P2mJ.js → CronLabel-BAfIHHz4.js} +2 -2
- package/dashboard/dist/assets/{CronLabel-OOP4P2mJ.js.map → CronLabel-BAfIHHz4.js.map} +1 -1
- package/dashboard/dist/assets/{CustomDurationPicker-COacSAXO.js → CustomDurationPicker-BLF0NFJK.js} +2 -2
- package/dashboard/dist/assets/{CustomDurationPicker-COacSAXO.js.map → CustomDurationPicker-BLF0NFJK.js.map} +1 -1
- package/dashboard/dist/assets/{DataTable-D9yuBv0w.js → DataTable-ZWgVQg7F.js} +2 -2
- package/dashboard/dist/assets/DataTable-ZWgVQg7F.js.map +1 -0
- package/dashboard/dist/assets/{DropZone-Bfw2YLVS.js → DropZone-CEx_wk-g.js} +2 -2
- package/dashboard/dist/assets/{DropZone-Bfw2YLVS.js.map → DropZone-CEx_wk-g.js.map} +1 -1
- package/dashboard/dist/assets/{ElapsedCell-CkfCD-m7.js → ElapsedCell-nKGJvznH.js} +2 -2
- package/dashboard/dist/assets/{ElapsedCell-CkfCD-m7.js.map → ElapsedCell-nKGJvznH.js.map} +1 -1
- package/dashboard/dist/assets/{EscalationsOverview-BmI8jxlh.js → EscalationsOverview-bNNVA6R2.js} +2 -2
- package/dashboard/dist/assets/{EscalationsOverview-BmI8jxlh.js.map → EscalationsOverview-bNNVA6R2.js.map} +1 -1
- package/dashboard/dist/assets/{EventTable-CeKfFk3m.js → EventTable-BCuDdqlU.js} +2 -2
- package/dashboard/dist/assets/{EventTable-CeKfFk3m.js.map → EventTable-BCuDdqlU.js.map} +1 -1
- package/dashboard/dist/assets/FilterBar-BQ2JGg8U.js +2 -0
- package/dashboard/dist/assets/FilterBar-BQ2JGg8U.js.map +1 -0
- package/dashboard/dist/assets/GraphInvokePage-BPAvqHls.js +2 -0
- package/dashboard/dist/assets/GraphInvokePage-BPAvqHls.js.map +1 -0
- package/dashboard/dist/assets/HomePage-BZ1YTrjR.js +2 -0
- package/dashboard/dist/assets/HomePage-BZ1YTrjR.js.map +1 -0
- package/dashboard/dist/assets/{ListToolbar-BJaCzAXS.js → ListToolbar-DIJZfEgu.js} +2 -2
- package/dashboard/dist/assets/{ListToolbar-BJaCzAXS.js.map → ListToolbar-DIJZfEgu.js.map} +1 -1
- package/dashboard/dist/assets/{McpOverview-CvnDG5OB.js → McpOverview-DCfSCfPg.js} +2 -2
- package/dashboard/dist/assets/{McpOverview-CvnDG5OB.js.map → McpOverview-DCfSCfPg.js.map} +1 -1
- package/dashboard/dist/assets/{McpQueryDetailPage-DwmSIcIL.js → McpQueryDetailPage-mb7rmtNe.js} +2 -2
- package/dashboard/dist/assets/{McpQueryDetailPage-DwmSIcIL.js.map → McpQueryDetailPage-mb7rmtNe.js.map} +1 -1
- package/dashboard/dist/assets/{McpQueryPage-CU6FMEEk.js → McpQueryPage-D9b1T0I3.js} +2 -2
- package/dashboard/dist/assets/{McpQueryPage-CU6FMEEk.js.map → McpQueryPage-D9b1T0I3.js.map} +1 -1
- package/dashboard/dist/assets/{McpRunDetailPage-8a6lsehv.js → McpRunDetailPage-Bz2UyuNm.js} +2 -2
- package/dashboard/dist/assets/{McpRunDetailPage-8a6lsehv.js.map → McpRunDetailPage-Bz2UyuNm.js.map} +1 -1
- package/dashboard/dist/assets/McpRunsPage-Dw7w_ZO-.js +2 -0
- package/dashboard/dist/assets/McpRunsPage-Dw7w_ZO-.js.map +1 -0
- package/dashboard/dist/assets/{NamespacePill-D6btr6e8.js → NamespacePill-D27vqDXy.js} +2 -2
- package/dashboard/dist/assets/{NamespacePill-D6btr6e8.js.map → NamespacePill-D27vqDXy.js.map} +1 -1
- package/dashboard/dist/assets/OperatorDashboard-CTzSDW3N.js +2 -0
- package/dashboard/dist/assets/{OperatorDashboard-g9o7I925.js.map → OperatorDashboard-CTzSDW3N.js.map} +1 -1
- package/dashboard/dist/assets/PageHeader-B_1zTx2q.js +2 -0
- package/dashboard/dist/assets/PageHeader-B_1zTx2q.js.map +1 -0
- package/dashboard/dist/assets/{PageHeaderWithStats-DJQCZ0FR.js → PageHeaderWithStats-D-cPg7UD.js} +2 -2
- package/dashboard/dist/assets/{PageHeaderWithStats-DJQCZ0FR.js.map → PageHeaderWithStats-D-cPg7UD.js.map} +1 -1
- package/dashboard/dist/assets/PriorityBadge-B4ykBH7f.js +2 -0
- package/dashboard/dist/assets/PriorityBadge-B4ykBH7f.js.map +1 -0
- package/dashboard/dist/assets/{ProcessDetailPage-ByyWCs2A.js → ProcessDetailPage-olG2CS-h.js} +2 -2
- package/dashboard/dist/assets/{ProcessDetailPage-ByyWCs2A.js.map → ProcessDetailPage-olG2CS-h.js.map} +1 -1
- package/dashboard/dist/assets/{ProcessesListPage-BTsXds4l.js → ProcessesListPage-CUZSL06Y.js} +2 -2
- package/dashboard/dist/assets/{ProcessesListPage-BTsXds4l.js.map → ProcessesListPage-CUZSL06Y.js.map} +1 -1
- package/dashboard/dist/assets/RolePill-B-V-31lC.js +2 -0
- package/dashboard/dist/assets/RolePill-B-V-31lC.js.map +1 -0
- package/dashboard/dist/assets/RolesPage-DLnaW6iD.js +2 -0
- package/dashboard/dist/assets/RolesPage-DLnaW6iD.js.map +1 -0
- package/dashboard/dist/assets/RowActions-BA9L9djf.js +2 -0
- package/dashboard/dist/assets/RowActions-BA9L9djf.js.map +1 -0
- package/dashboard/dist/assets/{RunAsSelector-BzkZXpSX.js → RunAsSelector-BoLHF4fy.js} +2 -2
- package/dashboard/dist/assets/{RunAsSelector-BzkZXpSX.js.map → RunAsSelector-BoLHF4fy.js.map} +1 -1
- package/dashboard/dist/assets/StickyPagination-BEMY1L-H.js +2 -0
- package/dashboard/dist/assets/StickyPagination-BEMY1L-H.js.map +1 -0
- package/dashboard/dist/assets/{StreamMessageDetail-B6ch-6SY.js → StreamMessageDetail-CVRVWXcU.js} +2 -2
- package/dashboard/dist/assets/{StreamMessageDetail-B6ch-6SY.js.map → StreamMessageDetail-CVRVWXcU.js.map} +1 -1
- package/dashboard/dist/assets/{SwimlaneTimeline-CjvfUkls.js → SwimlaneTimeline-DDpU0-Jn.js} +2 -2
- package/dashboard/dist/assets/{SwimlaneTimeline-CjvfUkls.js.map → SwimlaneTimeline-DDpU0-Jn.js.map} +1 -1
- package/dashboard/dist/assets/{TagInput-CZSeyesi.js → TagInput-BHVfG307.js} +2 -2
- package/dashboard/dist/assets/{TagInput-CZSeyesi.js.map → TagInput-BHVfG307.js.map} +1 -1
- package/dashboard/dist/assets/{TaskDetailPage-BjHU6opi.js → TaskDetailPage-B_xQtJMg.js} +2 -2
- package/dashboard/dist/assets/{TaskDetailPage-BjHU6opi.js.map → TaskDetailPage-B_xQtJMg.js.map} +1 -1
- package/dashboard/dist/assets/{TaskQueuePill-CQrXrSxk.js → TaskQueuePill-CGlxXqWt.js} +2 -2
- package/dashboard/dist/assets/{TaskQueuePill-CQrXrSxk.js.map → TaskQueuePill-CGlxXqWt.js.map} +1 -1
- package/dashboard/dist/assets/{TasksListPage-Bem44zd0.js → TasksListPage-CsrdIOKk.js} +2 -2
- package/dashboard/dist/assets/{TasksListPage-Bem44zd0.js.map → TasksListPage-CsrdIOKk.js.map} +1 -1
- package/dashboard/dist/assets/TimeAgo-DmicaxjT.js +2 -0
- package/dashboard/dist/assets/{TimeAgo-CUIYqSt_.js.map → TimeAgo-DmicaxjT.js.map} +1 -1
- package/dashboard/dist/assets/{TimestampCell--tnreI1i.js → TimestampCell-DeK89eRV.js} +2 -2
- package/dashboard/dist/assets/{TimestampCell--tnreI1i.js.map → TimestampCell-DeK89eRV.js.map} +1 -1
- package/dashboard/dist/assets/{ToolPill-O09jsTQM.js → ToolPill-NV7PxSpA.js} +2 -2
- package/dashboard/dist/assets/{ToolPill-O09jsTQM.js.map → ToolPill-NV7PxSpA.js.map} +1 -1
- package/dashboard/dist/assets/{ToolTestPanel-OD99H7Pk.js → ToolTestPanel-D3s2n9-9.js} +2 -2
- package/dashboard/dist/assets/{ToolTestPanel-OD99H7Pk.js.map → ToolTestPanel-D3s2n9-9.js.map} +1 -1
- package/dashboard/dist/assets/{TopicDetailPage-rh_dsTJI.js → TopicDetailPage-qyv7UWT_.js} +3 -3
- package/dashboard/dist/assets/TopicDetailPage-qyv7UWT_.js.map +1 -0
- package/dashboard/dist/assets/TopicsPage-CEUfMoed.js +2 -0
- package/dashboard/dist/assets/TopicsPage-CEUfMoed.js.map +1 -0
- package/dashboard/dist/assets/{UserName-C__c8BN0.js → UserName-Bn_BxI8K.js} +2 -2
- package/dashboard/dist/assets/{UserName-C__c8BN0.js.map → UserName-Bn_BxI8K.js.map} +1 -1
- package/dashboard/dist/assets/{WorkflowExecutionPage-Bw6QdtUO.js → WorkflowExecutionPage-DAk2RqpB.js} +2 -2
- package/dashboard/dist/assets/{WorkflowExecutionPage-Bw6QdtUO.js.map → WorkflowExecutionPage-DAk2RqpB.js.map} +1 -1
- package/dashboard/dist/assets/{WorkflowPill-CS-IsR0U.js → WorkflowPill-DDR0B6DN.js} +2 -2
- package/dashboard/dist/assets/{WorkflowPill-CS-IsR0U.js.map → WorkflowPill-DDR0B6DN.js.map} +1 -1
- package/dashboard/dist/assets/WorkflowsDashboard-DfBgM80t.js +2 -0
- package/dashboard/dist/assets/WorkflowsDashboard-DfBgM80t.js.map +1 -0
- package/dashboard/dist/assets/{WorkflowsOverview-BRc0QbA7.js → WorkflowsOverview--Dkoo9Jq.js} +2 -2
- package/dashboard/dist/assets/{WorkflowsOverview-BRc0QbA7.js.map → WorkflowsOverview--Dkoo9Jq.js.map} +1 -1
- package/dashboard/dist/assets/{YamlWorkflowDetailPage-DdzspNQR.js → YamlWorkflowDetailPage-B6HYbfFj.js} +25 -25
- package/dashboard/dist/assets/{YamlWorkflowDetailPage-DdzspNQR.js.map → YamlWorkflowDetailPage-B6HYbfFj.js.map} +1 -1
- package/dashboard/dist/assets/{YamlWorkflowsPage-Djw5ZbgQ.js → YamlWorkflowsPage-CJE-PxC0.js} +2 -2
- package/dashboard/dist/assets/{YamlWorkflowsPage-Djw5ZbgQ.js.map → YamlWorkflowsPage-CJE-PxC0.js.map} +1 -1
- package/dashboard/dist/assets/{agents-xstNkF6c.js → agents-DUciUZpt.js} +2 -2
- package/dashboard/dist/assets/{agents-xstNkF6c.js.map → agents-DUciUZpt.js.map} +1 -1
- package/dashboard/dist/assets/{bots-BztAI0s_.js → bots-BnyqCfuC.js} +2 -2
- package/dashboard/dist/assets/{bots-BztAI0s_.js.map → bots-BnyqCfuC.js.map} +1 -1
- package/dashboard/dist/assets/{capabilities-CEDwtxNL.js → capabilities-CkTWWT0q.js} +2 -2
- package/dashboard/dist/assets/{capabilities-CEDwtxNL.js.map → capabilities-CkTWWT0q.js.map} +1 -1
- package/dashboard/dist/assets/{controlplane-BlGEsRKe.js → controlplane-D9HaFZsC.js} +2 -2
- package/dashboard/dist/assets/{controlplane-BlGEsRKe.js.map → controlplane-D9HaFZsC.js.map} +1 -1
- package/dashboard/dist/assets/{escalation-iI4QKlP-.js → escalation-De9JnTKI.js} +2 -2
- package/dashboard/dist/assets/{escalation-iI4QKlP-.js.map → escalation-De9JnTKI.js.map} +1 -1
- package/dashboard/dist/assets/escalation-columns-Df7Pluzg.js +2 -0
- package/dashboard/dist/assets/escalation-columns-Df7Pluzg.js.map +1 -0
- package/dashboard/dist/assets/index-7hqtQI43.js +2 -0
- package/dashboard/dist/assets/index-7hqtQI43.js.map +1 -0
- package/dashboard/dist/assets/{index-CAqEy2Db.js → index-B07xLlyz.js} +2 -2
- package/dashboard/dist/assets/{index-CAqEy2Db.js.map → index-B07xLlyz.js.map} +1 -1
- package/dashboard/dist/assets/{index-D1xVY4JR.js → index-B1kc6itM.js} +24 -24
- package/dashboard/dist/assets/index-B1kc6itM.js.map +1 -0
- package/dashboard/dist/assets/index-BAhkWcOq.js +9 -0
- package/dashboard/dist/assets/index-BAhkWcOq.js.map +1 -0
- package/dashboard/dist/assets/{index-Dsi5o7dx.js → index-BsIaUbKD.js} +2 -2
- package/dashboard/dist/assets/{index-Dsi5o7dx.js.map → index-BsIaUbKD.js.map} +1 -1
- package/dashboard/dist/assets/index-BxdiKgnH.js +6 -0
- package/dashboard/dist/assets/index-BxdiKgnH.js.map +1 -0
- package/dashboard/dist/assets/{index-B7VC0vjf.js → index-BzMsh6Ui.js} +2 -2
- package/dashboard/dist/assets/{index-B7VC0vjf.js.map → index-BzMsh6Ui.js.map} +1 -1
- package/dashboard/dist/assets/index-CN0efCE9.js +6 -0
- package/dashboard/dist/assets/index-CN0efCE9.js.map +1 -0
- package/dashboard/dist/assets/{index-B5qsp8lb.js → index-CgD9fA9P.js} +2 -2
- package/dashboard/dist/assets/{index-B5qsp8lb.js.map → index-CgD9fA9P.js.map} +1 -1
- package/dashboard/dist/assets/{index-CUfgt3mV.js → index-Cqjf-yVP.js} +2 -2
- package/dashboard/dist/assets/{index-CUfgt3mV.js.map → index-Cqjf-yVP.js.map} +1 -1
- package/dashboard/dist/assets/{index-DjJ6VCuc.js → index-DNugmBs7.js} +2 -2
- package/dashboard/dist/assets/{index-DjJ6VCuc.js.map → index-DNugmBs7.js.map} +1 -1
- package/dashboard/dist/assets/{index-BUmnJ4eT.js → index-DjxTV3cn.js} +2 -2
- package/dashboard/dist/assets/{index-BUmnJ4eT.js.map → index-DjxTV3cn.js.map} +1 -1
- package/dashboard/dist/assets/index-DpAS4-DA.js +2 -0
- package/dashboard/dist/assets/index-DpAS4-DA.js.map +1 -0
- package/dashboard/dist/assets/index-nO0cim3_.css +1 -0
- package/dashboard/dist/assets/{knowledge-BKg0cr39.js → knowledge-D4SvdNv5.js} +2 -2
- package/dashboard/dist/assets/{knowledge-BKg0cr39.js.map → knowledge-D4SvdNv5.js.map} +1 -1
- package/dashboard/dist/assets/{mcp-CVdHpKlz.js → mcp-DVxt4T9J.js} +2 -2
- package/dashboard/dist/assets/{mcp-CVdHpKlz.js.map → mcp-DVxt4T9J.js.map} +1 -1
- package/dashboard/dist/assets/{mcp-query-CYxZcbwQ.js → mcp-query-CLP9EnnU.js} +2 -2
- package/dashboard/dist/assets/{mcp-query-CYxZcbwQ.js.map → mcp-query-CLP9EnnU.js.map} +1 -1
- package/dashboard/dist/assets/{pipelines-BOEv7P6O.js → pipelines-C_x0LTBN.js} +2 -2
- package/dashboard/dist/assets/{pipelines-BOEv7P6O.js.map → pipelines-C_x0LTBN.js.map} +1 -1
- package/dashboard/dist/assets/{roles-c5cV2QYQ.js → roles-QaGFZQ58.js} +2 -2
- package/dashboard/dist/assets/{roles-c5cV2QYQ.js.map → roles-QaGFZQ58.js.map} +1 -1
- package/dashboard/dist/assets/{tasks-JV3BjeEF.js → tasks-1eTuP02d.js} +2 -2
- package/dashboard/dist/assets/{tasks-JV3BjeEF.js.map → tasks-1eTuP02d.js.map} +1 -1
- package/dashboard/dist/assets/{topics-BDaPnhnQ.js → topics-B3p8K0PW.js} +2 -2
- package/dashboard/dist/assets/{topics-BDaPnhnQ.js.map → topics-B3p8K0PW.js.map} +1 -1
- package/dashboard/dist/assets/{useEventHooks-a7Dnh8G4.js → useEventHooks-BNZky4xM.js} +2 -2
- package/dashboard/dist/assets/{useEventHooks-a7Dnh8G4.js.map → useEventHooks-BNZky4xM.js.map} +1 -1
- package/dashboard/dist/assets/{useNamespace-BSwEV0e9.js → useNamespace-Dx81_JM0.js} +2 -2
- package/dashboard/dist/assets/{useNamespace-BSwEV0e9.js.map → useNamespace-Dx81_JM0.js.map} +1 -1
- package/dashboard/dist/assets/{useYamlActivityEvents-BuqvL-cn.js → useYamlActivityEvents-D5T3TbE7.js} +2 -2
- package/dashboard/dist/assets/{useYamlActivityEvents-BuqvL-cn.js.map → useYamlActivityEvents-D5T3TbE7.js.map} +1 -1
- package/dashboard/dist/assets/users-BEMysNdR.js +2 -0
- package/dashboard/dist/assets/users-BEMysNdR.js.map +1 -0
- package/dashboard/dist/assets/{vendor-icons-D5ycKPA2.js → vendor-icons--Ivmp-RX.js} +132 -112
- package/dashboard/dist/assets/vendor-icons--Ivmp-RX.js.map +1 -0
- package/dashboard/dist/assets/{workflows-DF-9Voyi.js → workflows-C4rjvQbI.js} +2 -2
- package/dashboard/dist/assets/{workflows-DF-9Voyi.js.map → workflows-C4rjvQbI.js.map} +1 -1
- package/dashboard/dist/assets/{yaml-workflows-2roKaSSK.js → yaml-workflows-4rJ5v5rJ.js} +2 -2
- package/dashboard/dist/assets/{yaml-workflows-2roKaSSK.js.map → yaml-workflows-4rJ5v5rJ.js.map} +1 -1
- package/dashboard/dist/index.html +3 -3
- package/docs/api/http/escalations.md +131 -10
- package/docs/api/http/roles.md +44 -9
- package/docs/api/http/users.md +15 -6
- package/docs/api/mcp/admin.md +8 -2
- package/docs/api/mcp/human-queue.md +34 -2
- package/docs/api/sdk/escalations.md +155 -2
- package/docs/api/sdk/roles.md +11 -0
- package/docs/api/sdk/users.md +26 -1
- package/docs/auth.md +1 -1
- package/docs/dashboard.md +5 -4
- package/docs/data.md +9 -2
- package/docs/faceted-routing.md +221 -0
- package/docs/hitl-guide.md +50 -0
- package/docs/iam.md +31 -0
- package/docs/mcp.md +8 -0
- package/package.json +2 -2
- package/dashboard/dist/assets/AdminDashboard-LTiYLuzq.js +0 -2
- package/dashboard/dist/assets/AgentDetailPage-BSUnDvGp.js.map +0 -1
- package/dashboard/dist/assets/AgentsPage-DpS0cCYe.js +0 -2
- package/dashboard/dist/assets/AvailableEscalationsPage-BEylRqjP.js +0 -2
- package/dashboard/dist/assets/AvailableEscalationsPage-BEylRqjP.js.map +0 -1
- package/dashboard/dist/assets/CapabilitiesPage-DLIleX2L.js +0 -2
- package/dashboard/dist/assets/CapabilitiesPage-DLIleX2L.js.map +0 -1
- package/dashboard/dist/assets/CopyableId-DaT0ZRHg.js +0 -2
- package/dashboard/dist/assets/CopyableId-DaT0ZRHg.js.map +0 -1
- package/dashboard/dist/assets/DataTable-D9yuBv0w.js.map +0 -1
- package/dashboard/dist/assets/FilterBar-Ck4K4rzu.js +0 -2
- package/dashboard/dist/assets/FilterBar-Ck4K4rzu.js.map +0 -1
- package/dashboard/dist/assets/GraphInvokePage-BZBPBCSq.js +0 -2
- package/dashboard/dist/assets/GraphInvokePage-BZBPBCSq.js.map +0 -1
- package/dashboard/dist/assets/HomePage-dM7U2tl6.js +0 -2
- package/dashboard/dist/assets/HomePage-dM7U2tl6.js.map +0 -1
- package/dashboard/dist/assets/McpRunsPage-CPMYWgm_.js +0 -2
- package/dashboard/dist/assets/McpRunsPage-CPMYWgm_.js.map +0 -1
- package/dashboard/dist/assets/OperatorDashboard-g9o7I925.js +0 -2
- package/dashboard/dist/assets/PageHeader-Dk2Pe44Q.js +0 -2
- package/dashboard/dist/assets/PageHeader-Dk2Pe44Q.js.map +0 -1
- package/dashboard/dist/assets/PriorityBadge-DfQY9St9.js +0 -2
- package/dashboard/dist/assets/PriorityBadge-DfQY9St9.js.map +0 -1
- package/dashboard/dist/assets/RolePill-Du-aNnjR.js +0 -2
- package/dashboard/dist/assets/RolePill-Du-aNnjR.js.map +0 -1
- package/dashboard/dist/assets/RolesPage-B2Kdf-L9.js +0 -2
- package/dashboard/dist/assets/RolesPage-B2Kdf-L9.js.map +0 -1
- package/dashboard/dist/assets/RowActions-Dg-Fsm5O.js +0 -2
- package/dashboard/dist/assets/RowActions-Dg-Fsm5O.js.map +0 -1
- package/dashboard/dist/assets/StickyPagination-BWhFSr2d.js +0 -2
- package/dashboard/dist/assets/StickyPagination-BWhFSr2d.js.map +0 -1
- package/dashboard/dist/assets/TimeAgo-CUIYqSt_.js +0 -2
- package/dashboard/dist/assets/TopicDetailPage-rh_dsTJI.js.map +0 -1
- package/dashboard/dist/assets/TopicsPage-CD_40_v-.js +0 -2
- package/dashboard/dist/assets/TopicsPage-CD_40_v-.js.map +0 -1
- package/dashboard/dist/assets/WorkflowsDashboard-BgI-iOP6.js +0 -2
- package/dashboard/dist/assets/WorkflowsDashboard-BgI-iOP6.js.map +0 -1
- package/dashboard/dist/assets/escalation-columns-nVYVt7Gl.js +0 -2
- package/dashboard/dist/assets/escalation-columns-nVYVt7Gl.js.map +0 -1
- package/dashboard/dist/assets/index-CMviPjXQ.js +0 -9
- package/dashboard/dist/assets/index-CMviPjXQ.js.map +0 -1
- package/dashboard/dist/assets/index-CP0JEQhZ.js +0 -6
- package/dashboard/dist/assets/index-CP0JEQhZ.js.map +0 -1
- package/dashboard/dist/assets/index-ClvYSNZd.css +0 -1
- package/dashboard/dist/assets/index-D1xVY4JR.js.map +0 -1
- package/dashboard/dist/assets/index-DPLy-jwO.js +0 -2
- package/dashboard/dist/assets/index-DPLy-jwO.js.map +0 -1
- package/dashboard/dist/assets/index-DzVYjoex.js +0 -6
- package/dashboard/dist/assets/index-DzVYjoex.js.map +0 -1
- package/dashboard/dist/assets/index-FuhiJuHL.js +0 -2
- package/dashboard/dist/assets/index-FuhiJuHL.js.map +0 -1
- package/dashboard/dist/assets/users-Pn85K_3C.js +0 -2
- package/dashboard/dist/assets/users-Pn85K_3C.js.map +0 -1
- package/dashboard/dist/assets/vendor-icons-D5ycKPA2.js.map +0 -1
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// ---------------------------------------------------------------------------
|
|
3
|
+
// Faceted escalation access — a rich search/claim interface over the queue so a
|
|
4
|
+
// consuming app can implement routing *strategies* against the data facets
|
|
5
|
+
// (metadata, priority, …). The platform supplies the surface; the policy
|
|
6
|
+
// (capability, priority, capacity packing, the loop) is the app's (see examples/).
|
|
7
|
+
//
|
|
8
|
+
// The dispatcher pattern: pull a *page* of complete orders (page size = how many
|
|
9
|
+
// printers/consumers are free), then distribute one order per consumer. Reads go
|
|
10
|
+
// through the `public.lt_escalations` view; atomic claims run on
|
|
11
|
+
// `public.hmsh_escalations` with FOR UPDATE SKIP LOCKED, so many dispatchers run
|
|
12
|
+
// without contention. An order is a set of escalations sharing `origin_id`; its
|
|
13
|
+
// unit count lives in a metadata facet (default `orderSize`) the app saves, so a
|
|
14
|
+
// group is claimed all-or-nothing only when complete.
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.ensureFacetReady = ensureFacetReady;
|
|
18
|
+
exports.searchByFacets = searchByFacets;
|
|
19
|
+
exports.searchGroups = searchGroups;
|
|
20
|
+
exports.countByFacets = countByFacets;
|
|
21
|
+
exports.claimGroups = claimGroups;
|
|
22
|
+
exports.claimByFacets = claimByFacets;
|
|
23
|
+
const db_1 = require("../../lib/db");
|
|
24
|
+
const client_1 = require("./client");
|
|
25
|
+
const map_1 = require("./map");
|
|
26
|
+
const facet_sql_1 = require("./facet-sql");
|
|
27
|
+
const DEFAULT_LIMIT = 50;
|
|
28
|
+
const DEFAULT_CLAIM_MINUTES = 30;
|
|
29
|
+
const DEFAULT_SIZE_FACET = 'orderSize';
|
|
30
|
+
/** Extra candidates fetched per claim so contention skips still fill the page. */
|
|
31
|
+
const CANDIDATE_OVERSCAN = 4;
|
|
32
|
+
let ready = null;
|
|
33
|
+
/** Ensure the compat view + the origin_id index exist before the first query. */
|
|
34
|
+
function ensureFacetReady() {
|
|
35
|
+
if (!ready) {
|
|
36
|
+
ready = (async () => {
|
|
37
|
+
await (0, client_1.ensureEscalationCompatView)(); // hmsh_escalations + lt_escalations view
|
|
38
|
+
await (0, db_1.getPool)().query(facet_sql_1.ENSURE_ORIGIN_INDEX);
|
|
39
|
+
})();
|
|
40
|
+
}
|
|
41
|
+
return ready;
|
|
42
|
+
}
|
|
43
|
+
// ── Reads ────────────────────────────────────────────────────────────────────
|
|
44
|
+
/** Item-level faceted search — filter/sort over top-level columns and metadata facets. */
|
|
45
|
+
async function searchByFacets(query) {
|
|
46
|
+
await ensureFacetReady();
|
|
47
|
+
const pool = (0, db_1.getPool)();
|
|
48
|
+
const params = [];
|
|
49
|
+
const where = (0, facet_sql_1.buildFacetWhere)(query, params);
|
|
50
|
+
const order = (0, facet_sql_1.buildFacetOrder)(query.orderBy);
|
|
51
|
+
const limit = query.limit ?? DEFAULT_LIMIT;
|
|
52
|
+
const offset = query.offset ?? 0;
|
|
53
|
+
const [rows, count] = await Promise.all([
|
|
54
|
+
pool.query(`SELECT * FROM public.lt_escalations WHERE ${where} ORDER BY ${order} LIMIT $${params.length + 1} OFFSET $${params.length + 2}`, [...params, limit, offset]),
|
|
55
|
+
pool.query(`SELECT count(*)::int AS total FROM public.lt_escalations WHERE ${where}`, params),
|
|
56
|
+
]);
|
|
57
|
+
return { escalations: (0, map_1.toEscalationRecords)(rows.rows), total: count.rows[0]?.total ?? 0 };
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Order-level view of the pond — what a batched dispatcher reads to page by
|
|
61
|
+
* capacity. Each row is an order (origin) with its unit count, availability and
|
|
62
|
+
* completeness, so the app grabs only as many orders as it has free consumers.
|
|
63
|
+
*/
|
|
64
|
+
async function searchGroups(query, opts = {}) {
|
|
65
|
+
await ensureFacetReady();
|
|
66
|
+
const sizeFacet = opts.sizeFacet ?? DEFAULT_SIZE_FACET;
|
|
67
|
+
const params = [sizeFacet]; // $1 = metadata key holding the unit count
|
|
68
|
+
const where = (0, facet_sql_1.buildFacetWhere)(query, params);
|
|
69
|
+
const groupOrder = (0, facet_sql_1.buildGroupOrder)(query.orderBy);
|
|
70
|
+
const limit = opts.limit ?? DEFAULT_LIMIT;
|
|
71
|
+
const offset = opts.offset ?? 0;
|
|
72
|
+
const { rows } = await (0, db_1.getPool)().query(`SELECT origin_id,
|
|
73
|
+
count(*)::int AS member_count,
|
|
74
|
+
max((metadata->>$1)::int) AS order_size,
|
|
75
|
+
bool_and(assigned_to IS NULL OR assigned_until IS NULL OR assigned_until <= NOW()) AS available,
|
|
76
|
+
(max((metadata->>$1)::int) IS NULL OR count(*) >= max((metadata->>$1)::int)) AS complete,
|
|
77
|
+
min(priority)::int AS min_priority,
|
|
78
|
+
min(created_at) AS created_at
|
|
79
|
+
FROM public.lt_escalations
|
|
80
|
+
WHERE origin_id IS NOT NULL AND ${where}
|
|
81
|
+
GROUP BY origin_id
|
|
82
|
+
ORDER BY ${groupOrder}
|
|
83
|
+
LIMIT $${params.length + 1} OFFSET $${params.length + 2}`, [...params, limit, offset]);
|
|
84
|
+
return rows.map((r) => ({
|
|
85
|
+
originId: r.origin_id,
|
|
86
|
+
memberCount: r.member_count,
|
|
87
|
+
orderSize: r.order_size ?? null,
|
|
88
|
+
available: r.available,
|
|
89
|
+
complete: r.complete,
|
|
90
|
+
minPriority: r.min_priority,
|
|
91
|
+
createdAt: r.created_at,
|
|
92
|
+
}));
|
|
93
|
+
}
|
|
94
|
+
/** Aggregate count — capacity / in-flight soft-limit checks. */
|
|
95
|
+
async function countByFacets(query) {
|
|
96
|
+
await ensureFacetReady();
|
|
97
|
+
const params = [];
|
|
98
|
+
const where = (0, facet_sql_1.buildFacetWhere)(query, params);
|
|
99
|
+
const { rows } = await (0, db_1.getPool)().query(`SELECT count(*)::int AS n FROM public.lt_escalations WHERE ${where}`, params);
|
|
100
|
+
return rows[0]?.n ?? 0;
|
|
101
|
+
}
|
|
102
|
+
// ── Claim ────────────────────────────────────────────────────────────────────
|
|
103
|
+
/** Lock + completeness-check + claim one order inside an open transaction. */
|
|
104
|
+
async function tryClaimGroup(client, originId, consumer, durationMinutes, sizeFacet) {
|
|
105
|
+
const locked = await client.query(`SELECT id, (metadata->>$2)::int AS order_size,
|
|
106
|
+
(assigned_to IS NULL OR assigned_until IS NULL OR assigned_until <= NOW()) AS avail
|
|
107
|
+
FROM public.hmsh_escalations
|
|
108
|
+
WHERE origin_id = $1 AND status = 'pending'
|
|
109
|
+
FOR UPDATE SKIP LOCKED`, [originId, sizeFacet]);
|
|
110
|
+
const members = locked.rows;
|
|
111
|
+
const declared = members.reduce((m, r) => Math.max(m, r.order_size ?? 0), 0);
|
|
112
|
+
const expected = declared > 0 ? declared : members.length;
|
|
113
|
+
const allAvailable = members.length > 0 && members.every((r) => r.avail);
|
|
114
|
+
if (members.length !== expected || !allAvailable)
|
|
115
|
+
return null; // contended / incomplete
|
|
116
|
+
const claimed = await client.query(`UPDATE public.hmsh_escalations e
|
|
117
|
+
SET assigned_to = $2, assigned_until = NOW() + make_interval(mins => $3),
|
|
118
|
+
claim_expires_at = NOW() + make_interval(mins => $3),
|
|
119
|
+
claimed_at = COALESCE(e.claimed_at, NOW()), updated_at = NOW()
|
|
120
|
+
WHERE e.origin_id = $1 AND e.status = 'pending' RETURNING e.*`, [originId, consumer, durationMinutes]);
|
|
121
|
+
return { originId, members: (0, map_1.toEscalationRecords)(claimed.rows) };
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Pull a *page* of complete orders — the dispatcher primitive. Claims up to
|
|
125
|
+
* `limit` eligible orders (each all-or-nothing) in rank order, skipping any a
|
|
126
|
+
* competing dispatcher holds. Page `limit` = how many consumers/printers are
|
|
127
|
+
* free, so a dependent grabs exactly as much as it can distribute. `limit=1`
|
|
128
|
+
* claims a single order.
|
|
129
|
+
*/
|
|
130
|
+
async function claimGroups(query, consumer, opts = {}) {
|
|
131
|
+
await ensureFacetReady();
|
|
132
|
+
const limit = Math.max(1, opts.limit ?? 1);
|
|
133
|
+
const durationMinutes = opts.durationMinutes ?? DEFAULT_CLAIM_MINUTES;
|
|
134
|
+
const sizeFacet = opts.sizeFacet ?? DEFAULT_SIZE_FACET;
|
|
135
|
+
const pool = (0, db_1.getPool)();
|
|
136
|
+
const client = await pool.connect();
|
|
137
|
+
try {
|
|
138
|
+
const params = [sizeFacet];
|
|
139
|
+
const where = (0, facet_sql_1.buildFacetWhere)({ ...query, status: query.status ?? 'pending' }, params);
|
|
140
|
+
const groupOrder = (0, facet_sql_1.buildGroupOrder)(query.orderBy);
|
|
141
|
+
const cands = await client.query(`SELECT origin_id FROM public.hmsh_escalations
|
|
142
|
+
WHERE origin_id IS NOT NULL AND ${where}
|
|
143
|
+
GROUP BY origin_id
|
|
144
|
+
HAVING bool_and(assigned_to IS NULL OR assigned_until IS NULL OR assigned_until <= NOW())
|
|
145
|
+
AND count(*) = COALESCE(max((metadata->>$1)::int), count(*))
|
|
146
|
+
ORDER BY ${groupOrder}
|
|
147
|
+
LIMIT ${limit * CANDIDATE_OVERSCAN}`, params);
|
|
148
|
+
await client.query('BEGIN');
|
|
149
|
+
const claimed = [];
|
|
150
|
+
for (const { origin_id: originId } of cands.rows) {
|
|
151
|
+
if (claimed.length >= limit)
|
|
152
|
+
break;
|
|
153
|
+
const group = await tryClaimGroup(client, originId, consumer, durationMinutes, sizeFacet);
|
|
154
|
+
if (group)
|
|
155
|
+
claimed.push(group);
|
|
156
|
+
}
|
|
157
|
+
await client.query('COMMIT');
|
|
158
|
+
return claimed;
|
|
159
|
+
}
|
|
160
|
+
catch (err) {
|
|
161
|
+
await client.query('ROLLBACK').catch(() => { });
|
|
162
|
+
throw err;
|
|
163
|
+
}
|
|
164
|
+
finally {
|
|
165
|
+
client.release();
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Batch-claim *individual* escalations matching a facet query — the single-row
|
|
170
|
+
* sibling of `claimGroups`. Locks up to `limit` available rows in rank order with
|
|
171
|
+
* `FOR UPDATE SKIP LOCKED`, so concurrent claimers take disjoint sets without
|
|
172
|
+
* contention. With `allOrNone`, the claim commits only when the full `limit` was
|
|
173
|
+
* acquired (otherwise it rolls back and returns `[]`) — the all-or-none lock a
|
|
174
|
+
* dispatcher wants over a counted set it anticipated. Returns the claimed records.
|
|
175
|
+
*/
|
|
176
|
+
async function claimByFacets(query, consumer, opts = {}) {
|
|
177
|
+
await ensureFacetReady();
|
|
178
|
+
const limit = Math.max(1, opts.limit ?? 1);
|
|
179
|
+
const durationMinutes = opts.durationMinutes ?? DEFAULT_CLAIM_MINUTES;
|
|
180
|
+
const pool = (0, db_1.getPool)();
|
|
181
|
+
const client = await pool.connect();
|
|
182
|
+
try {
|
|
183
|
+
const params = [];
|
|
184
|
+
const where = (0, facet_sql_1.buildFacetWhere)({ ...query, available: true, status: query.status ?? 'pending' }, params);
|
|
185
|
+
const order = (0, facet_sql_1.buildFacetOrder)(query.orderBy);
|
|
186
|
+
const consumerIdx = params.length + 1;
|
|
187
|
+
const durationIdx = params.length + 2;
|
|
188
|
+
// One atomic statement: a FOR UPDATE SKIP LOCKED CTE picks the rank-ordered
|
|
189
|
+
// page, the UPDATE claims exactly those rows. RETURNING count = rows acquired.
|
|
190
|
+
await client.query('BEGIN');
|
|
191
|
+
const claimed = await client.query(`WITH locked AS (
|
|
192
|
+
SELECT id FROM public.hmsh_escalations
|
|
193
|
+
WHERE ${where}
|
|
194
|
+
ORDER BY ${order}
|
|
195
|
+
LIMIT ${limit}
|
|
196
|
+
FOR UPDATE SKIP LOCKED
|
|
197
|
+
)
|
|
198
|
+
UPDATE public.hmsh_escalations e
|
|
199
|
+
SET assigned_to = $${consumerIdx}, assigned_until = NOW() + make_interval(mins => $${durationIdx}),
|
|
200
|
+
claim_expires_at = NOW() + make_interval(mins => $${durationIdx}),
|
|
201
|
+
claimed_at = COALESCE(e.claimed_at, NOW()), updated_at = NOW()
|
|
202
|
+
FROM locked WHERE e.id = locked.id
|
|
203
|
+
RETURNING e.*`, [...params, consumer, durationMinutes]);
|
|
204
|
+
if (claimed.rows.length === 0 || (opts.allOrNone && claimed.rows.length < limit)) {
|
|
205
|
+
await client.query('ROLLBACK'); // all-or-none unmet, or nothing eligible
|
|
206
|
+
return [];
|
|
207
|
+
}
|
|
208
|
+
await client.query('COMMIT');
|
|
209
|
+
return (0, map_1.toEscalationRecords)(claimed.rows);
|
|
210
|
+
}
|
|
211
|
+
catch (err) {
|
|
212
|
+
await client.query('ROLLBACK').catch(() => { });
|
|
213
|
+
throw err;
|
|
214
|
+
}
|
|
215
|
+
finally {
|
|
216
|
+
client.release();
|
|
217
|
+
}
|
|
218
|
+
}
|
|
@@ -14,10 +14,17 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
14
14
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
-
exports.ensureEscalationCompatView = void 0;
|
|
17
|
+
exports.claimByFacets = exports.claimGroups = exports.countByFacets = exports.searchGroups = exports.searchByFacets = exports.ensureFacetReady = exports.ensureEscalationCompatView = void 0;
|
|
18
18
|
__exportStar(require("./types"), exports);
|
|
19
19
|
__exportStar(require("./crud"), exports);
|
|
20
20
|
__exportStar(require("./bulk"), exports);
|
|
21
21
|
__exportStar(require("./queries"), exports);
|
|
22
22
|
var client_1 = require("./client");
|
|
23
23
|
Object.defineProperty(exports, "ensureEscalationCompatView", { enumerable: true, get: function () { return client_1.ensureEscalationCompatView; } });
|
|
24
|
+
var facets_1 = require("./facets");
|
|
25
|
+
Object.defineProperty(exports, "ensureFacetReady", { enumerable: true, get: function () { return facets_1.ensureFacetReady; } });
|
|
26
|
+
Object.defineProperty(exports, "searchByFacets", { enumerable: true, get: function () { return facets_1.searchByFacets; } });
|
|
27
|
+
Object.defineProperty(exports, "searchGroups", { enumerable: true, get: function () { return facets_1.searchGroups; } });
|
|
28
|
+
Object.defineProperty(exports, "countByFacets", { enumerable: true, get: function () { return facets_1.countByFacets; } });
|
|
29
|
+
Object.defineProperty(exports, "claimGroups", { enumerable: true, get: function () { return facets_1.claimGroups; } });
|
|
30
|
+
Object.defineProperty(exports, "claimByFacets", { enumerable: true, get: function () { return facets_1.claimByFacets; } });
|
|
@@ -1,5 +1,45 @@
|
|
|
1
1
|
import type { LTEscalationRecord, LTEscalationStatus } from '../../types';
|
|
2
2
|
import type { EscalationStats } from './types';
|
|
3
|
+
import type { FacetQuery } from '../../types';
|
|
4
|
+
/**
|
|
5
|
+
* Scoped faceted search — the HUMAN operations query. Composes the read-scope
|
|
6
|
+
* predicate (global → none; else `role ∈ allRoles OR (role ∈ selfRoles AND
|
|
7
|
+
* assigned_to = me)`) with the full FacetQuery language (`buildFacetWhere`:
|
|
8
|
+
* facets `@>`, block, numeric range, exists, status, available) plus the extra
|
|
9
|
+
* top-level filters (type/subtype/priority/assigned_to) and free-text, then orders
|
|
10
|
+
* by `buildFacetOrder` (columns + `metadata.<key>`). Every value is bound; the
|
|
11
|
+
* COUNT shares the WHERE so totals stay correct across pages — no client-side
|
|
12
|
+
* filtering. Reads go through the `public.lt_escalations` view.
|
|
13
|
+
*/
|
|
14
|
+
export declare function searchEscalationsFaceted(opts: {
|
|
15
|
+
global?: boolean;
|
|
16
|
+
visibleRoles?: string[];
|
|
17
|
+
selfRoles?: string[];
|
|
18
|
+
meUserId?: string;
|
|
19
|
+
facet: FacetQuery;
|
|
20
|
+
type?: string;
|
|
21
|
+
subtype?: string;
|
|
22
|
+
priority?: number;
|
|
23
|
+
assigned_to?: string;
|
|
24
|
+
search?: string;
|
|
25
|
+
limit: number;
|
|
26
|
+
offset: number;
|
|
27
|
+
}): Promise<{
|
|
28
|
+
escalations: LTEscalationRecord[];
|
|
29
|
+
total: number;
|
|
30
|
+
}>;
|
|
31
|
+
/**
|
|
32
|
+
* Distinct top-level metadata facet KEYS visible to the caller — feeds the faceted
|
|
33
|
+
* UI's key autocomplete so it only ever offers facets that ACTUALLY exist in the
|
|
34
|
+
* caller's rows (a key in metadata, not description text). Role-scoped with the same
|
|
35
|
+
* predicate as the list; only object-typed metadata is unpacked (`jsonb_object_keys`).
|
|
36
|
+
*/
|
|
37
|
+
export declare function listFacetKeys(opts: {
|
|
38
|
+
global?: boolean;
|
|
39
|
+
visibleRoles?: string[];
|
|
40
|
+
selfRoles?: string[];
|
|
41
|
+
meUserId?: string;
|
|
42
|
+
}): Promise<string[]>;
|
|
3
43
|
export declare function getEscalationStats(visibleRoles?: string[], period?: string): Promise<EscalationStats>;
|
|
4
44
|
export declare function listDistinctTypes(): Promise<string[]>;
|
|
5
45
|
export declare function listEscalations(filters: {
|
|
@@ -13,9 +53,12 @@ export declare function listEscalations(filters: {
|
|
|
13
53
|
limit?: number;
|
|
14
54
|
offset?: number;
|
|
15
55
|
visibleRoles?: string[];
|
|
56
|
+
selfRoles?: string[];
|
|
57
|
+
meUserId?: string;
|
|
16
58
|
sort_by?: string;
|
|
17
59
|
order?: string;
|
|
18
60
|
search?: string;
|
|
61
|
+
metadata?: Record<string, any>;
|
|
19
62
|
}): Promise<{
|
|
20
63
|
escalations: LTEscalationRecord[];
|
|
21
64
|
total: number;
|
|
@@ -31,6 +74,8 @@ export declare function listAvailableEscalations(filters: {
|
|
|
31
74
|
limit?: number;
|
|
32
75
|
offset?: number;
|
|
33
76
|
visibleRoles?: string[];
|
|
77
|
+
selfRoles?: string[];
|
|
78
|
+
meUserId?: string;
|
|
34
79
|
sort_by?: string;
|
|
35
80
|
order?: string;
|
|
36
81
|
search?: string;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.searchEscalationsFaceted = searchEscalationsFaceted;
|
|
4
|
+
exports.listFacetKeys = listFacetKeys;
|
|
3
5
|
exports.getEscalationStats = getEscalationStats;
|
|
4
6
|
exports.listDistinctTypes = listDistinctTypes;
|
|
5
7
|
exports.listEscalations = listEscalations;
|
|
@@ -7,6 +9,7 @@ exports.listAvailableEscalations = listAvailableEscalations;
|
|
|
7
9
|
const db_1 = require("../../lib/db");
|
|
8
10
|
const client_1 = require("./client");
|
|
9
11
|
const map_1 = require("./map");
|
|
12
|
+
const facet_sql_1 = require("./facet-sql");
|
|
10
13
|
const types_1 = require("./types");
|
|
11
14
|
const sql_1 = require("./sql");
|
|
12
15
|
/**
|
|
@@ -44,7 +47,8 @@ async function searchEscalations(params) {
|
|
|
44
47
|
const pool = (0, db_1.getPool)();
|
|
45
48
|
// Coerce empty strings to NULL so an absent filter (e.g. assigned_to='') does
|
|
46
49
|
// not become `column = ''` and match zero rows. Mirrors the SDK path's
|
|
47
|
-
// `if (filters.x)` truthiness guards.
|
|
50
|
+
// `if (filters.x)` truthiness guards. An empty search term means "no free-text
|
|
51
|
+
// filter" — this path also serves scoped (self-role) listing with no search.
|
|
48
52
|
const filterArgs = [
|
|
49
53
|
params.status || null,
|
|
50
54
|
params.role || null,
|
|
@@ -54,7 +58,10 @@ async function searchEscalations(params) {
|
|
|
54
58
|
params.priority ?? null,
|
|
55
59
|
params.assigned_to || null,
|
|
56
60
|
params.available ?? null,
|
|
57
|
-
params.search,
|
|
61
|
+
params.search || null,
|
|
62
|
+
params.selfRoles && params.selfRoles.length ? params.selfRoles : null,
|
|
63
|
+
params.meUserId || null,
|
|
64
|
+
params.metadata ? JSON.stringify(params.metadata) : null,
|
|
58
65
|
];
|
|
59
66
|
const orderBy = buildSearchOrderBy(params.sort_by, params.order);
|
|
60
67
|
const [rows, countRows] = await Promise.all([
|
|
@@ -66,6 +73,87 @@ async function searchEscalations(params) {
|
|
|
66
73
|
total: countRows.rows[0]?.total ?? 0,
|
|
67
74
|
};
|
|
68
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* Scoped faceted search — the HUMAN operations query. Composes the read-scope
|
|
78
|
+
* predicate (global → none; else `role ∈ allRoles OR (role ∈ selfRoles AND
|
|
79
|
+
* assigned_to = me)`) with the full FacetQuery language (`buildFacetWhere`:
|
|
80
|
+
* facets `@>`, block, numeric range, exists, status, available) plus the extra
|
|
81
|
+
* top-level filters (type/subtype/priority/assigned_to) and free-text, then orders
|
|
82
|
+
* by `buildFacetOrder` (columns + `metadata.<key>`). Every value is bound; the
|
|
83
|
+
* COUNT shares the WHERE so totals stay correct across pages — no client-side
|
|
84
|
+
* filtering. Reads go through the `public.lt_escalations` view.
|
|
85
|
+
*/
|
|
86
|
+
async function searchEscalationsFaceted(opts) {
|
|
87
|
+
await (0, client_1.ensureEscalationCompatView)();
|
|
88
|
+
const pool = (0, db_1.getPool)();
|
|
89
|
+
const params = [];
|
|
90
|
+
const clauses = [];
|
|
91
|
+
// 1. Read-scope predicate (skip entirely for global access).
|
|
92
|
+
if (!opts.global) {
|
|
93
|
+
const ai = params.push(opts.visibleRoles?.length ? opts.visibleRoles : null);
|
|
94
|
+
const si = params.push(opts.selfRoles?.length ? opts.selfRoles : null);
|
|
95
|
+
const mi = params.push(opts.meUserId || null);
|
|
96
|
+
clauses.push(`(($${ai}::text[] IS NULL AND $${si}::text[] IS NULL)
|
|
97
|
+
OR ($${ai}::text[] IS NOT NULL AND role = ANY($${ai}))
|
|
98
|
+
OR ($${si}::text[] IS NOT NULL AND role = ANY($${si}) AND assigned_to = $${mi}))`);
|
|
99
|
+
}
|
|
100
|
+
// 2. Faceted predicate (role/roles/status/available/facets/block/range/exists).
|
|
101
|
+
const facetClause = (0, facet_sql_1.buildFacetWhere)(opts.facet, params);
|
|
102
|
+
if (facetClause !== 'TRUE')
|
|
103
|
+
clauses.push(facetClause);
|
|
104
|
+
// 3. Extra top-level filters not expressed by FacetQuery.
|
|
105
|
+
if (opts.type)
|
|
106
|
+
clauses.push(`type = $${params.push(opts.type)}`);
|
|
107
|
+
if (opts.subtype)
|
|
108
|
+
clauses.push(`subtype = $${params.push(opts.subtype)}`);
|
|
109
|
+
if (opts.priority != null)
|
|
110
|
+
clauses.push(`priority = $${params.push(opts.priority)}`);
|
|
111
|
+
if (opts.assigned_to)
|
|
112
|
+
clauses.push(`assigned_to = $${params.push(opts.assigned_to)}`);
|
|
113
|
+
// 4. Free-text (same fields as the list search path).
|
|
114
|
+
if (opts.search) {
|
|
115
|
+
const i = params.push(opts.search);
|
|
116
|
+
clauses.push(`(description ILIKE '%' || $${i} || '%' OR type ILIKE '%' || $${i} || '%'
|
|
117
|
+
OR subtype ILIKE '%' || $${i} || '%' OR role ILIKE '%' || $${i} || '%'
|
|
118
|
+
OR workflow_id ILIKE '%' || $${i} || '%' OR origin_id ILIKE '%' || $${i} || '%'
|
|
119
|
+
OR id::text ILIKE '%' || $${i} || '%' OR metadata::text ILIKE '%' || $${i} || '%')`);
|
|
120
|
+
}
|
|
121
|
+
const where = clauses.length ? `WHERE ${clauses.join('\n AND ')}` : '';
|
|
122
|
+
const orderBy = (0, facet_sql_1.buildFacetOrder)(opts.facet.orderBy);
|
|
123
|
+
const countParams = [...params];
|
|
124
|
+
const li = params.push(opts.limit);
|
|
125
|
+
const oi = params.push(opts.offset);
|
|
126
|
+
const [rows, countRows] = await Promise.all([
|
|
127
|
+
pool.query(`SELECT * FROM public.lt_escalations ${where} ORDER BY ${orderBy} LIMIT $${li} OFFSET $${oi}`, params),
|
|
128
|
+
pool.query(`SELECT COUNT(*)::int AS total FROM public.lt_escalations ${where}`, countParams),
|
|
129
|
+
]);
|
|
130
|
+
return { escalations: (0, map_1.toEscalationRecords)(rows.rows), total: countRows.rows[0]?.total ?? 0 };
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Distinct top-level metadata facet KEYS visible to the caller — feeds the faceted
|
|
134
|
+
* UI's key autocomplete so it only ever offers facets that ACTUALLY exist in the
|
|
135
|
+
* caller's rows (a key in metadata, not description text). Role-scoped with the same
|
|
136
|
+
* predicate as the list; only object-typed metadata is unpacked (`jsonb_object_keys`).
|
|
137
|
+
*/
|
|
138
|
+
async function listFacetKeys(opts) {
|
|
139
|
+
await (0, client_1.ensureEscalationCompatView)();
|
|
140
|
+
const pool = (0, db_1.getPool)();
|
|
141
|
+
const params = [];
|
|
142
|
+
const clauses = ["jsonb_typeof(metadata) = 'object'"];
|
|
143
|
+
if (!opts.global) {
|
|
144
|
+
const ai = params.push(opts.visibleRoles?.length ? opts.visibleRoles : null);
|
|
145
|
+
const si = params.push(opts.selfRoles?.length ? opts.selfRoles : null);
|
|
146
|
+
const mi = params.push(opts.meUserId || null);
|
|
147
|
+
clauses.push(`(($${ai}::text[] IS NULL AND $${si}::text[] IS NULL)
|
|
148
|
+
OR ($${ai}::text[] IS NOT NULL AND role = ANY($${ai}))
|
|
149
|
+
OR ($${si}::text[] IS NOT NULL AND role = ANY($${si}) AND assigned_to = $${mi}))`);
|
|
150
|
+
}
|
|
151
|
+
const { rows } = await pool.query(`SELECT DISTINCT jsonb_object_keys(metadata) AS key
|
|
152
|
+
FROM public.lt_escalations
|
|
153
|
+
WHERE ${clauses.join('\n AND ')}
|
|
154
|
+
ORDER BY key`, params);
|
|
155
|
+
return rows.map((r) => r.key);
|
|
156
|
+
}
|
|
69
157
|
async function getEscalationStats(visibleRoles, period) {
|
|
70
158
|
const client = await (0, client_1.escalations)();
|
|
71
159
|
return client.stats({
|
|
@@ -80,18 +168,25 @@ async function listDistinctTypes() {
|
|
|
80
168
|
async function listEscalations(filters) {
|
|
81
169
|
// Active claim semantics: assigned_to-active and `claimed` both mean "held now".
|
|
82
170
|
const heldNow = !!(filters.assigned_to || filters.claimed);
|
|
83
|
-
|
|
84
|
-
|
|
171
|
+
const hasSelfScope = !!(filters.selfRoles && filters.selfRoles.length);
|
|
172
|
+
// Free-text search OR read_self scoping → server-side SQL path. The SDK list()
|
|
173
|
+
// has no free-text and cannot express the (role ∈ selfRoles AND assigned_to=me)
|
|
174
|
+
// self-scope branch, so both route through the raw-SQL search query (which also
|
|
175
|
+
// serves metadata-containment for findByMetadata's self-scope callers).
|
|
176
|
+
if (filters.search || hasSelfScope) {
|
|
85
177
|
return searchEscalations({
|
|
86
178
|
status: filters.status,
|
|
87
179
|
role: filters.role,
|
|
88
180
|
roles: filters.visibleRoles,
|
|
181
|
+
selfRoles: filters.selfRoles,
|
|
182
|
+
meUserId: filters.meUserId,
|
|
89
183
|
type: filters.type,
|
|
90
184
|
subtype: filters.subtype,
|
|
91
185
|
priority: filters.priority,
|
|
92
186
|
assigned_to: filters.assigned_to,
|
|
93
187
|
available: heldNow ? false : undefined,
|
|
94
188
|
search: filters.search,
|
|
189
|
+
metadata: filters.metadata,
|
|
95
190
|
limit: filters.limit ?? 50,
|
|
96
191
|
offset: filters.offset ?? 0,
|
|
97
192
|
sort_by: filters.sort_by,
|
|
@@ -112,6 +207,8 @@ async function listEscalations(filters) {
|
|
|
112
207
|
where.assignedTo = filters.assigned_to;
|
|
113
208
|
if (heldNow)
|
|
114
209
|
where.available = false;
|
|
210
|
+
if (filters.metadata)
|
|
211
|
+
where.metadata = filters.metadata;
|
|
115
212
|
const [rows, total] = await Promise.all([
|
|
116
213
|
client.list({
|
|
117
214
|
...where,
|
|
@@ -127,12 +224,16 @@ async function listEscalations(filters) {
|
|
|
127
224
|
* List available escalations: pending AND (unassigned OR expired claim).
|
|
128
225
|
*/
|
|
129
226
|
async function listAvailableEscalations(filters) {
|
|
130
|
-
|
|
131
|
-
|
|
227
|
+
const hasSelfScope = !!(filters.selfRoles && filters.selfRoles.length);
|
|
228
|
+
// Free-text search OR read_self scoping → server-side SQL path
|
|
229
|
+
// (available = pending + no active claim).
|
|
230
|
+
if (filters.search || hasSelfScope) {
|
|
132
231
|
return searchEscalations({
|
|
133
232
|
status: 'pending',
|
|
134
233
|
role: filters.role,
|
|
135
234
|
roles: filters.visibleRoles,
|
|
235
|
+
selfRoles: filters.selfRoles,
|
|
236
|
+
meUserId: filters.meUserId,
|
|
136
237
|
type: filters.type,
|
|
137
238
|
subtype: filters.subtype,
|
|
138
239
|
priority: filters.priority,
|
|
@@ -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 ($3::text[] IS NULL OR role = ANY($3))\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 ($9::text IS NULL OR (\n description ILIKE '%' || $9 || '%'\n OR type ILIKE '%' || $9 || '%'\n OR subtype ILIKE '%' || $9 || '%'\n OR role ILIKE '%' || $9 || '%'\n OR workflow_id ILIKE '%' || $9 || '%'\n OR origin_id ILIKE '%' || $9 || '%'\n OR id::text ILIKE '%' || $9 || '%'\n OR metadata::text ILIKE '%' || $9 || '%'\n ))";
|
|
11
|
+
/** Count matching the search WHERE (params $1–$12). */
|
|
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 (\n description ILIKE '%' || $9 || '%'\n OR type ILIKE '%' || $9 || '%'\n OR subtype ILIKE '%' || $9 || '%'\n OR role ILIKE '%' || $9 || '%'\n OR workflow_id ILIKE '%' || $9 || '%'\n OR origin_id ILIKE '%' || $9 || '%'\n OR id::text ILIKE '%' || $9 || '%'\n OR metadata::text ILIKE '%' || $9 || '%'\n ))";
|
|
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.
|
|
@@ -35,6 +35,12 @@ export declare function searchEscalationsQuery(orderBy: string): string;
|
|
|
35
35
|
* The `resolved` CTE still skips signal_id rows — the workflow resolves durably.
|
|
36
36
|
*
|
|
37
37
|
* $1 = metadata filter (jsonb), $2 = userId, $3 = resolver_payload (jsonb),
|
|
38
|
-
* $4 = metadata patch (jsonb, nullable), $5 =
|
|
38
|
+
* $4 = metadata patch (jsonb, nullable), $5 = write_all roles (text[], null = global /
|
|
39
|
+
* no filter), $6 = write_self roles (text[], nullable).
|
|
40
|
+
*
|
|
41
|
+
* Write-scope is folded into the same FOR UPDATE statement (no TOCTOU): the row
|
|
42
|
+
* is resolvable if the caller has global access ($5 NULL), or the row's role is in
|
|
43
|
+
* their write_all set, or the row's role is in their write_self set AND it is
|
|
44
|
+
* already assigned to them ($2). assigned_to is indexed, so the self-branch scales.
|
|
39
45
|
*/
|
|
40
|
-
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 ($5::text[] IS NULL
|
|
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,20 +47,38 @@ WHERE status = 'pending'
|
|
|
47
47
|
* `metadata::text ILIKE` scans the JSONB as text (the GIN index is containment-only
|
|
48
48
|
* and cannot serve ILIKE) — bounded in practice by the other filters and the page.
|
|
49
49
|
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
50
|
+
* Role-scope visibility ($3 allRoles, $10 selfRoles, $11 meUserId):
|
|
51
|
+
* - global access → $3 NULL and $10 NULL → no role filter (sees everything)
|
|
52
|
+
* - read_all roles → row.role ∈ $3
|
|
53
|
+
* - read_self roles → row.role ∈ $10 AND row.assigned_to = $11 (only the user's own)
|
|
54
|
+
* The branches union, so a user with mixed scope (read_all on some roles, read_self
|
|
55
|
+
* on others) is filtered correctly in one pass. `assigned_to` is indexed
|
|
56
|
+
* (idx_lt_escalations_assigned), so the self-branch is index-served at scale.
|
|
57
|
+
*
|
|
58
|
+
* $1 status, $2 role, $3 allRoles (text[]), $4 type, $5 subtype, $6 priority,
|
|
59
|
+
* $7 assigned_to, $8 available (bool), $9 search, $10 selfRoles (text[]),
|
|
60
|
+
* $11 meUserId, $12 metadata (jsonb containment, GIN-served). The list query
|
|
61
|
+
* adds $13 limit, $14 offset.
|
|
62
|
+
*
|
|
63
|
+
* `metadata @> $12` is exact JSONB containment (the GIN index serves it) — this is
|
|
64
|
+
* how findByMetadata routes through the scoped query so its role-scope filter and
|
|
65
|
+
* count run in SQL (no client-side filtering), the same as free-text/self-scope.
|
|
53
66
|
*/
|
|
54
67
|
const SEARCH_ESCALATIONS_WHERE = `\
|
|
55
68
|
FROM public.lt_escalations
|
|
56
69
|
WHERE ($1::text IS NULL OR status = $1)
|
|
57
70
|
AND ($2::text IS NULL OR role = $2)
|
|
58
|
-
AND (
|
|
71
|
+
AND (
|
|
72
|
+
($3::text[] IS NULL AND $10::text[] IS NULL)
|
|
73
|
+
OR ($3::text[] IS NOT NULL AND role = ANY($3))
|
|
74
|
+
OR ($10::text[] IS NOT NULL AND role = ANY($10) AND assigned_to = $11)
|
|
75
|
+
)
|
|
59
76
|
AND ($4::text IS NULL OR type = $4)
|
|
60
77
|
AND ($5::text IS NULL OR subtype = $5)
|
|
61
78
|
AND ($6::int IS NULL OR priority = $6)
|
|
62
79
|
AND ($7::text IS NULL OR assigned_to = $7)
|
|
63
80
|
AND ($8::boolean IS NULL OR available = $8)
|
|
81
|
+
AND ($12::jsonb IS NULL OR metadata @> $12)
|
|
64
82
|
AND ($9::text IS NULL OR (
|
|
65
83
|
description ILIKE '%' || $9 || '%'
|
|
66
84
|
OR type ILIKE '%' || $9 || '%'
|
|
@@ -71,14 +89,14 @@ const SEARCH_ESCALATIONS_WHERE = `\
|
|
|
71
89
|
OR id::text ILIKE '%' || $9 || '%'
|
|
72
90
|
OR metadata::text ILIKE '%' || $9 || '%'
|
|
73
91
|
))`;
|
|
74
|
-
/** Count matching the search WHERE (params $1–$
|
|
92
|
+
/** Count matching the search WHERE (params $1–$12). */
|
|
75
93
|
exports.COUNT_SEARCH_ESCALATIONS = `SELECT COUNT(*)::int AS total\n${SEARCH_ESCALATIONS_WHERE}`;
|
|
76
94
|
/**
|
|
77
95
|
* Build the search SELECT. `orderBy` is composed from a whitelist by the caller
|
|
78
96
|
* (never raw user input), so it is safe to interpolate; everything else is bound.
|
|
79
97
|
*/
|
|
80
98
|
function searchEscalationsQuery(orderBy) {
|
|
81
|
-
return `SELECT *\n${SEARCH_ESCALATIONS_WHERE}\n ORDER BY ${orderBy}\n LIMIT $
|
|
99
|
+
return `SELECT *\n${SEARCH_ESCALATIONS_WHERE}\n ORDER BY ${orderBy}\n LIMIT $13 OFFSET $14`;
|
|
82
100
|
}
|
|
83
101
|
/**
|
|
84
102
|
* Atomic resolve by metadata with signal guard.
|
|
@@ -100,7 +118,13 @@ function searchEscalationsQuery(orderBy) {
|
|
|
100
118
|
* The `resolved` CTE still skips signal_id rows — the workflow resolves durably.
|
|
101
119
|
*
|
|
102
120
|
* $1 = metadata filter (jsonb), $2 = userId, $3 = resolver_payload (jsonb),
|
|
103
|
-
* $4 = metadata patch (jsonb, nullable), $5 =
|
|
121
|
+
* $4 = metadata patch (jsonb, nullable), $5 = write_all roles (text[], null = global /
|
|
122
|
+
* no filter), $6 = write_self roles (text[], nullable).
|
|
123
|
+
*
|
|
124
|
+
* Write-scope is folded into the same FOR UPDATE statement (no TOCTOU): the row
|
|
125
|
+
* is resolvable if the caller has global access ($5 NULL), or the row's role is in
|
|
126
|
+
* their write_all set, or the row's role is in their write_self set AND it is
|
|
127
|
+
* already assigned to them ($2). assigned_to is indexed, so the self-branch scales.
|
|
104
128
|
*/
|
|
105
129
|
exports.RESOLVE_BY_METADATA_ATOMIC = `\
|
|
106
130
|
WITH target AS MATERIALIZED (
|
|
@@ -108,7 +132,11 @@ WITH target AS MATERIALIZED (
|
|
|
108
132
|
FROM public.hmsh_escalations
|
|
109
133
|
WHERE metadata @> $1::jsonb
|
|
110
134
|
AND status = 'pending'
|
|
111
|
-
AND (
|
|
135
|
+
AND (
|
|
136
|
+
$5::text[] IS NULL
|
|
137
|
+
OR role = ANY($5)
|
|
138
|
+
OR (role = ANY($6::text[]) AND assigned_to = $2)
|
|
139
|
+
)
|
|
112
140
|
ORDER BY priority ASC, created_at ASC
|
|
113
141
|
LIMIT 1
|
|
114
142
|
FOR UPDATE
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
export declare function ltResolveEscalation(input: {
|
|
9
9
|
escalationId: string;
|
|
10
10
|
resolverPayload: Record<string, any>;
|
|
11
|
+
/** Outcome patch merged into the row inside the same atomic resolve (see resolveEscalation). */
|
|
12
|
+
metadata?: Record<string, any>;
|
|
11
13
|
}): Promise<void>;
|
|
12
14
|
/**
|
|
13
15
|
* Claim an escalation to a specific user. Used for auto-assignment
|
|
@@ -47,7 +47,7 @@ const logger_1 = require("../../../lib/logger");
|
|
|
47
47
|
* retries this activity.
|
|
48
48
|
*/
|
|
49
49
|
async function ltResolveEscalation(input) {
|
|
50
|
-
const result = await escalationService.resolveEscalation(input.escalationId, input.resolverPayload);
|
|
50
|
+
const result = await escalationService.resolveEscalation(input.escalationId, input.resolverPayload, input.metadata);
|
|
51
51
|
if (!result) {
|
|
52
52
|
logger_1.loggerRegistry.warn(`[ltResolveEscalation] Escalation ${input.escalationId} already resolved or not found`);
|
|
53
53
|
}
|
|
@@ -53,9 +53,13 @@ async function resolveReRun(activities, envelope, existingTask, wf, taskQueue) {
|
|
|
53
53
|
const escalationId = envelope?.lt?.escalationId;
|
|
54
54
|
const isRealEscalation = isReRun && escalationId && !escalationId.startsWith('auto-triage-');
|
|
55
55
|
if (isRealEscalation) {
|
|
56
|
+
// Thread the outcome patch carried on the re-run envelope so it merges into the
|
|
57
|
+
// prior escalation's metadata inside this SAME atomic resolve — never a separate
|
|
58
|
+
// write. (resolve orchestrator Path E sets envelope.lt.escalationMetadata.)
|
|
56
59
|
await activities.ltResolveEscalation({
|
|
57
60
|
escalationId: escalationId,
|
|
58
61
|
resolverPayload: envelope.resolver,
|
|
62
|
+
metadata: envelope.lt?.escalationMetadata,
|
|
59
63
|
});
|
|
60
64
|
// escalation.resolved event published by service layer (services/escalation/crud.ts)
|
|
61
65
|
}
|