@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
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
Manage human-in-the-loop escalations -- list, claim, resolve, and bulk-operate on workflow escalations.
|
|
4
4
|
|
|
5
|
+
## Work-Surface Scope
|
|
6
|
+
|
|
7
|
+
Escalation methods enforce the caller's role work-surface scope server-side; the SDK method signatures are unchanged. For a `member`:
|
|
8
|
+
|
|
9
|
+
- `read_scope` governs **search** — `list`, `listAvailable`, `findByMetadata`, `getStats`, and a single `get` return only the escalations the member is allowed to see. `read_scope=self` limits this to items assigned to the member; `read_scope=all` exposes the whole role queue.
|
|
10
|
+
- `write_scope` governs **claim / ack (resolve) / delete (cancel)** — a member with `write_scope=self` may only `claim`, `resolve`, and `cancel` items already assigned to them. `release`, `escalate`, and `create` (standalone) are queue-management verbs and require `write_scope=all`.
|
|
11
|
+
|
|
12
|
+
`admin` and `superadmin` ignore scope and act on the whole queue. Scope is set when a role is assigned — see [`lt.users.addRole`](users.md) and [Roles API — Work-Surface Scope](../http/roles.md).
|
|
13
|
+
|
|
5
14
|
## create
|
|
6
15
|
|
|
7
16
|
Create an escalation. The caller must hold the target role or be a superadmin.
|
|
@@ -52,6 +61,18 @@ const result = await lt.escalations.list({
|
|
|
52
61
|
role: 'reviewer',
|
|
53
62
|
limit: 25,
|
|
54
63
|
});
|
|
64
|
+
|
|
65
|
+
// Faceted query — a "facet" is a key/value INSIDE the row's metadata JSONB. The
|
|
66
|
+
// filter AND the count run in SQL, role-scoped; nothing is filtered client-side.
|
|
67
|
+
const faceted = await lt.escalations.list({
|
|
68
|
+
status: 'pending',
|
|
69
|
+
facets: { flags: 'too_short' }, // metadata @> { flags: 'too_short' }
|
|
70
|
+
range: [{ facet: 'confidence', op: '<=', value: 0.7 }],
|
|
71
|
+
block: [{ outcome: 'success' }], // exclude completed
|
|
72
|
+
exists: ['needsReview'],
|
|
73
|
+
orderBy: [{ field: 'metadata.confidence', numeric: true, direction: 'asc' }],
|
|
74
|
+
limit: 50,
|
|
75
|
+
});
|
|
55
76
|
```
|
|
56
77
|
|
|
57
78
|
**Parameters:**
|
|
@@ -60,14 +81,25 @@ const result = await lt.escalations.list({
|
|
|
60
81
|
|-------|------|----------|-------------|
|
|
61
82
|
| `status` | `string` | No | Filter by `pending`, `resolved`, or `cancelled` |
|
|
62
83
|
| `role` | `string` | No | Filter by assigned role |
|
|
84
|
+
| `roles` | `string[]` | No | Restrict to these roles (`role = ANY`) — narrows within the caller's scope, never widens past it |
|
|
63
85
|
| `type` | `string` | No | Filter by workflow type |
|
|
64
86
|
| `subtype` | `string` | No | Filter by subtype |
|
|
65
87
|
| `assigned_to` | `string` | No | Filter by assigned user ID |
|
|
66
88
|
| `priority` | `number` | No | Filter by priority (1--4) |
|
|
89
|
+
| `facets` | `Record<string, any>` | No | Required metadata facets — `metadata @> facets` (AND, GIN-served). `{ k: v }` means `metadata.k == v` for a top-level scalar; for nested/arrays it is JSONB **containment** |
|
|
90
|
+
| `block` | `Record<string, any>[]` | No | Exclude rows whose metadata contains ANY of these facet sets — `NOT (metadata @> ANY(block))` |
|
|
91
|
+
| `range` | `{ facet, op, value }[]` | No | Numeric range over a metadata facet, e.g. `{ facet: 'confidence', op: '<=', value: 0.7 }` |
|
|
92
|
+
| `exists` | `string[]` | No | Metadata keys that must be present — `metadata ? key` |
|
|
93
|
+
| `available` | `boolean` | No | `true` = unclaimed/expired only; `false` = held now |
|
|
67
94
|
| `limit` | `number` | No | Max results (default: 50) |
|
|
68
95
|
| `offset` | `number` | No | Pagination offset |
|
|
69
96
|
| `sort_by` | `string` | No | Column to sort by (e.g. `created_at`, `priority`) |
|
|
70
97
|
| `order` | `string` | No | `asc` or `desc` |
|
|
98
|
+
| `orderBy` | `{ field, direction?, numeric? }[]` | No | Multi-key sort over columns or a metadata path written `metadata.<key>` (set `numeric` for numeric sort) |
|
|
99
|
+
|
|
100
|
+
When any faceted element (`facets`/`block`/`range`/`exists`/`roles`/`available`/`orderBy`) is
|
|
101
|
+
present the request runs through the scoped faceted query; otherwise the simple list path is used.
|
|
102
|
+
See [Faceted Routing — the human / operations query](../../faceted-routing.md#the-human--operations-query).
|
|
71
103
|
|
|
72
104
|
**Returns:** `LTApiResult<{ escalations, total }>`
|
|
73
105
|
|
|
@@ -99,6 +131,9 @@ const result = await lt.escalations.listAvailable({
|
|
|
99
131
|
| `sort_by` | `string` | No | Column to sort by |
|
|
100
132
|
| `order` | `string` | No | `asc` or `desc` |
|
|
101
133
|
|
|
134
|
+
Also accepts the same faceted parameters as [`list`](#list) (`facets`, `block`, `range`,
|
|
135
|
+
`exists`, `roles`, `orderBy`), pinned to the available pool.
|
|
136
|
+
|
|
102
137
|
**Returns:** `LTApiResult<{ escalations, total }>`
|
|
103
138
|
|
|
104
139
|
**Auth:** Required
|
|
@@ -259,6 +294,7 @@ Supports two resolution paths: signal-routed (sends payload to a paused workflow
|
|
|
259
294
|
const result = await lt.escalations.resolve({
|
|
260
295
|
id: 'esc_123',
|
|
261
296
|
resolverPayload: { approved: true, comment: 'Looks good' },
|
|
297
|
+
metadata: { outcome: 'approved', reviewedBy: 'alice', durationMs: 1_240 },
|
|
262
298
|
});
|
|
263
299
|
```
|
|
264
300
|
|
|
@@ -267,7 +303,8 @@ const result = await lt.escalations.resolve({
|
|
|
267
303
|
| Field | Type | Required | Description |
|
|
268
304
|
|-------|------|----------|-------------|
|
|
269
305
|
| `id` | `string` | Yes | Escalation UUID |
|
|
270
|
-
| `resolverPayload` | `Record<string, any>` | Yes | Human decision data |
|
|
306
|
+
| `resolverPayload` | `Record<string, any>` | Yes | Human decision data — resumes the paused workflow; not indexed |
|
|
307
|
+
| `metadata` | `Record<string, any>` | No | Outcome facets merged into the row's GIN-indexed metadata. Records *what happened* (disposition, timing) next to *what was asked*; `@>`-queryable. See [Recording the outcome](#recording-the-outcome-on-resolve) |
|
|
271
308
|
|
|
272
309
|
**Returns:** `LTApiResult<{ signaled, escalationId, workflowId }>` (signal path) or `LTApiResult<{ started, escalationId, workflowId }>` (re-run path) -- returns 404 if not found, 409 if not pending.
|
|
273
310
|
|
|
@@ -275,6 +312,35 @@ const result = await lt.escalations.resolve({
|
|
|
275
312
|
|
|
276
313
|
---
|
|
277
314
|
|
|
315
|
+
## Recording the outcome on resolve
|
|
316
|
+
|
|
317
|
+
Every resolve path — `resolve`, `resolveBySignalKey`, the HTTP routes, the MCP tools, and the
|
|
318
|
+
in-process `EscalationService.resolveEscalation` — takes an optional `metadata` patch. It is
|
|
319
|
+
merged, not replaced, into the row's GIN-indexed metadata, recording the **outcome** on the
|
|
320
|
+
same row that carried the **intent**.
|
|
321
|
+
|
|
322
|
+
```typescript
|
|
323
|
+
await lt.escalations.resolve({
|
|
324
|
+
id: 'esc_123',
|
|
325
|
+
resolverPayload: { approved: true }, // resumes the workflow; not indexed
|
|
326
|
+
metadata: { outcome: 'approved', durationMs: 1_240 }, // recorded on the row; @>-queryable
|
|
327
|
+
});
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
The patch is distinct from `resolverPayload`: the payload is delivered to the waiting workflow
|
|
331
|
+
as `condition()`'s return value; the patch is the durable, queryable record. Intent and
|
|
332
|
+
outcome live on one row — no side table:
|
|
333
|
+
|
|
334
|
+
```typescript
|
|
335
|
+
await lt.escalations.findByMetadata({ key: 'outcome', value: 'approved' });
|
|
336
|
+
// every resolved row that was approved — with its disposition and duration
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
The in-process library takes the same patch as a third argument:
|
|
340
|
+
`resolveEscalation(id, payload, metadata)` and `resolveEscalationBySignalKey(signalKey, payload, metadata)`.
|
|
341
|
+
|
|
342
|
+
---
|
|
343
|
+
|
|
278
344
|
## conditionLT (workflow helper)
|
|
279
345
|
|
|
280
346
|
Wait for a signal and automatically resolve the associated escalation. This is the counterpart to `executeLT` — where `executeLT` wraps `startChild` + `condition`, `conditionLT` wraps `condition` + escalation resolution.
|
|
@@ -283,6 +349,16 @@ Wait for a signal and automatically resolve the associated escalation. This is t
|
|
|
283
349
|
conditionLT<T>(signalId: string, escalation?: ConditionQueueConfig): Promise<T | false | null>
|
|
284
350
|
```
|
|
285
351
|
|
|
352
|
+
### Two ways to pause on an escalation
|
|
353
|
+
|
|
354
|
+
There are two ways to make a workflow pause as a claimable escalation, and they are not equivalent in cost:
|
|
355
|
+
|
|
356
|
+
- **Native `condition(signalId, escalationConfig)` — the efficient primitive.** HotMesh's `condition` takes an optional escalation config as its second argument. The row is written inside the workflow's Leg1 checkpoint, with `signal_key = signalId`. Resolving it (`resolve` / `resolveBySignalKey`) marks the row resolved **and** delivers the signal in one guarded transaction, resuming the job in place. No create activity, no enrich step, and **no proxy-activity round-trip on the resume** — the resolve is the whole transaction. This is the path to prefer.
|
|
357
|
+
|
|
358
|
+
- **`conditionLT(signalId, config?)` — long-tail sugar.** With a config it delegates to the native efficient `condition` above (same atomic behavior — use it freely). Without a config it also supports the older **two-step** pattern: an escalation created separately, where the resume injects `$escalation_id` and `conditionLT` resolves it through a durable `proxyActivity` (`ltResolveEscalation`). That extra activity round-trip is the cost of the two-step form; the efficient form (and native `condition`) avoid it.
|
|
359
|
+
|
|
360
|
+
Reach for native `condition(signalId, config)` when you want the leanest path; reach for `conditionLT` for the ergonomic wrapper or to support the legacy two-step flow. Both resume the same row, and both accept the resolve-time `metadata` patch (the efficient path merges it in the single guarded UPDATE; the two-step path forwards it through `ltResolveEscalation` into that same atomic resolve).
|
|
361
|
+
|
|
286
362
|
### Atomic form (recommended)
|
|
287
363
|
|
|
288
364
|
Pass an escalation config as the second argument. The escalation row is written inside the workflow's Leg1 checkpoint — one commit, crash-safe: no separate `ltCreateEscalation` activity, no enrich step. `signal_key` is set to `signalId`, so the dashboard resolve endpoint (resolve-by-id → Path 0) and `POST /escalations/resolve-by-signal-key` resume *this* job in place, and `system.escalation.{id}.created` fires automatically.
|
|
@@ -662,7 +738,7 @@ const result = await lt.escalations.claimByMetadata({
|
|
|
662
738
|
| `metadata` | `object` | No | Merge into escalation metadata (single atomic SQL call with the claim) |
|
|
663
739
|
| `provisionIfAbsent` | `object` | No | JIT-provision the assignee if they don't exist or lack the required role (superadmin only) |
|
|
664
740
|
|
|
665
|
-
`provisionIfAbsent` accepts `{ displayName?, email?, roles?: [{ role, type? }] }`. Only callers with global escalation access can use this flag. The happy path (user exists, has role) adds zero extra queries.
|
|
741
|
+
`provisionIfAbsent` accepts `{ displayName?, email?, roles?: [{ role, type?, read_scope?, write_scope? }] }`. Each role entry forwards the optional work-surface scope fields `read_scope` (`self` or `all`, default `all`) and `write_scope` (`none`, `self`, or `all`, default `all`), subject to the **write ⊆ read** constraint; scope is ignored for `admin`/`superadmin`. To JIT-provision a one-time user who sees and acts on exactly the item being claimed, provision them `read_scope: 'self'` + `write_scope: 'self'`. Only callers with global escalation access can use this flag. The happy path (user exists, has role) adds zero extra queries.
|
|
666
742
|
|
|
667
743
|
**Returns:** `LTApiResult<{ escalation, isExtension }>` -- 404 if no match, 409 if already claimed.
|
|
668
744
|
|
|
@@ -707,3 +783,80 @@ const result = await lt.escalations.resolveByMetadata({
|
|
|
707
783
|
**Returns:** `LTApiResult<{ escalation }>` for non-signal, `LTApiResult<{ signaled, escalationId, workflowId }>` for signal-backed. 404 if no match.
|
|
708
784
|
|
|
709
785
|
**Auth:** Required
|
|
786
|
+
|
|
787
|
+
## resolveByIds
|
|
788
|
+
|
|
789
|
+
Resolve a set of escalations by id in one guarded statement (the set-based sibling of `resolve`). For bookkeeping rows woken collectively — it does not deliver a per-row signal. RBAC: a scoped caller may only resolve rows whose role they hold.
|
|
790
|
+
|
|
791
|
+
```typescript
|
|
792
|
+
const result = await lt.escalations.resolveByIds({
|
|
793
|
+
ids: ['esc_1', 'esc_2', 'esc_3'],
|
|
794
|
+
resolverPayload: { printerId: 'p-7' },
|
|
795
|
+
metadata: { outcome: 'settled' },
|
|
796
|
+
});
|
|
797
|
+
```
|
|
798
|
+
|
|
799
|
+
| Field | Type | Required | Description |
|
|
800
|
+
|-------|------|----------|-------------|
|
|
801
|
+
| `ids` | `string[]` | Yes | Escalation ids to resolve as one set |
|
|
802
|
+
| `resolverPayload` | `Record<string, any>` | Yes | Payload applied to every row |
|
|
803
|
+
| `metadata` | `Record<string, any>` | No | Outcome patch merged into each row |
|
|
804
|
+
|
|
805
|
+
**Returns:** `LTApiResult<{ resolved: number; escalationIds: string[] }>` — only still-`pending` rows are resolved.
|
|
806
|
+
|
|
807
|
+
## resolveBySignalKey
|
|
808
|
+
|
|
809
|
+
Resolve an efficient (atomic) escalation directly by its `signal_key` and resume the waiting workflow in place. For callers that know the deterministic signal id and want to skip the id lookup. RBAC-scoped to the escalation's role.
|
|
810
|
+
|
|
811
|
+
```typescript
|
|
812
|
+
const result = await lt.escalations.resolveBySignalKey({
|
|
813
|
+
signalKey: 'signal-scan-ar-order-42',
|
|
814
|
+
resolverPayload: { approved: true },
|
|
815
|
+
});
|
|
816
|
+
```
|
|
817
|
+
|
|
818
|
+
**Returns:** `LTApiResult<{ signaled: true; escalationId; workflowId }>`.
|
|
819
|
+
|
|
820
|
+
## searchByFacets
|
|
821
|
+
|
|
822
|
+
Item-level faceted search over a single pond `role`, scoped to the caller's role. The faceted-routing read primitive.
|
|
823
|
+
|
|
824
|
+
```typescript
|
|
825
|
+
const result = await lt.escalations.searchByFacets({
|
|
826
|
+
role: 'printer-pool-diabetic',
|
|
827
|
+
status: 'pending',
|
|
828
|
+
available: true,
|
|
829
|
+
facets: { state: 'ready' },
|
|
830
|
+
limit: 50,
|
|
831
|
+
});
|
|
832
|
+
```
|
|
833
|
+
|
|
834
|
+
**Returns:** `LTApiResult<{ escalations; total }>`.
|
|
835
|
+
|
|
836
|
+
## claimGroups
|
|
837
|
+
|
|
838
|
+
Batch-claim complete origin groups (e.g. all units of an order) in priority order over a pond, assigned to the caller. RBAC-scoped to the pond role.
|
|
839
|
+
|
|
840
|
+
```typescript
|
|
841
|
+
const result = await lt.escalations.claimGroups({
|
|
842
|
+
query: { role: 'print-farm-diabetic', available: true, facets: { filament: 'pla', size_class: 'standard' } },
|
|
843
|
+
limit: 4,
|
|
844
|
+
durationMinutes: 30,
|
|
845
|
+
sizeFacet: 'order_size',
|
|
846
|
+
});
|
|
847
|
+
```
|
|
848
|
+
|
|
849
|
+
**Returns:** `LTApiResult<{ groups }>`.
|
|
850
|
+
|
|
851
|
+
## claimByFacets
|
|
852
|
+
|
|
853
|
+
Batch-claim individual rows matching a facet query (`FOR UPDATE SKIP LOCKED`), assigned to the caller. With `allOrNone`, commits only when the full `limit` is acquired. RBAC-scoped to the pond role.
|
|
854
|
+
|
|
855
|
+
```typescript
|
|
856
|
+
const result = await lt.escalations.claimByFacets({
|
|
857
|
+
query: { role: 'printer-pool-diabetic', facets: { state: 'ready', filament: 'pla' } },
|
|
858
|
+
limit: 3,
|
|
859
|
+
});
|
|
860
|
+
```
|
|
861
|
+
|
|
862
|
+
**Returns:** `LTApiResult<{ claimed }>`.
|
package/docs/api/sdk/roles.md
CHANGED
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
Manage roles and escalation chain routing between roles.
|
|
4
4
|
|
|
5
|
+
## Work-Surface Scope
|
|
6
|
+
|
|
7
|
+
A role is a task queue worked by its members. Each `member` assignment carries two work-surface scope axes that set how much of the queue that member touches:
|
|
8
|
+
|
|
9
|
+
- `read_scope` (`self` or `all`, default `all`) governs **search** — which escalations the member sees.
|
|
10
|
+
- `write_scope` (`none`, `self`, or `all`, default `all`) governs **claim / ack (resolve) / delete (cancel)** — which escalations the member may act on.
|
|
11
|
+
|
|
12
|
+
`self` means items assigned to the member; `all` means the whole role queue. The constraint is **write ⊆ read** — `write_scope=all` requires `read_scope=all`. `admin` and `superadmin` ignore scope and always act on the whole queue. The default `all`/`all` is the full-queue worker.
|
|
13
|
+
|
|
14
|
+
Scope is set when a role is assigned to a user, via [`lt.users.addRole`](users.md) and [`lt.users.create`](users.md). See [Roles API — Work-Surface Scope](../http/roles.md) for the five member profiles.
|
|
15
|
+
|
|
5
16
|
## list
|
|
6
17
|
|
|
7
18
|
List all distinct role names in the system.
|
package/docs/api/sdk/users.md
CHANGED
|
@@ -70,9 +70,19 @@ const result = await lt.users.create({
|
|
|
70
70
|
| `external_id` | `string` | Yes | External system identifier |
|
|
71
71
|
| `email` | `string` | No | Email address |
|
|
72
72
|
| `display_name` | `string` | No | Display name |
|
|
73
|
-
| `roles` | `{ role: string; type: string }[]` | No | Initial role assignments (type: `superadmin`, `admin`, or `member`) |
|
|
73
|
+
| `roles` | `{ role: string; type: string; read_scope?: string; write_scope?: string }[]` | No | Initial role assignments (type: `superadmin`, `admin`, or `member`) |
|
|
74
74
|
| `metadata` | `Record<string, any>` | No | Arbitrary key-value metadata |
|
|
75
75
|
|
|
76
|
+
Each role entry forwards the optional work-surface scope fields `read_scope` (`self` or `all`, default `all`) and `write_scope` (`none`, `self`, or `all`, default `all`) to the API. Scope refines a `member` grant and is ignored for `admin`/`superadmin`. The constraint is **write ⊆ read** — `write_scope=all` requires `read_scope=all`. The default `all`/`all` is the full-queue worker. See [Roles API — Work-Surface Scope](../http/roles.md) for the five member profiles.
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
// A one-time user who sees and acts only on their own pre-assigned item
|
|
80
|
+
const result = await lt.users.create({
|
|
81
|
+
external_id: 'new-user',
|
|
82
|
+
roles: [{ role: 'customer-triage', type: 'member', read_scope: 'self', write_scope: 'self' }],
|
|
83
|
+
});
|
|
84
|
+
```
|
|
85
|
+
|
|
76
86
|
**Returns:** `LTApiResult<User>` (status 201) -- returns 409 if `external_id` already exists.
|
|
77
87
|
|
|
78
88
|
**Auth:** Not required
|
|
@@ -166,6 +176,21 @@ const result = await lt.users.addRole({
|
|
|
166
176
|
| `id` | `string` | Yes | User UUID |
|
|
167
177
|
| `role` | `string` | Yes | Role name to assign |
|
|
168
178
|
| `type` | `string` | Yes | Role type (`superadmin`, `admin`, or `member`) |
|
|
179
|
+
| `read_scope` | `string` | No | `self` or `all` (default `all`). Search breadth for a `member`; ignored for admin/superadmin |
|
|
180
|
+
| `write_scope` | `string` | No | `none`, `self`, or `all` (default `all`). Claim/ack/delete breadth for a `member` |
|
|
181
|
+
|
|
182
|
+
`read_scope` and `write_scope` are the work-surface scope axes for a `member` grant: `read_scope` governs which escalations the member sees in search; `write_scope` governs which they may claim, ack (resolve), or delete (cancel). `self` means items assigned to the member; `all` means the whole role queue. The constraint is **write ⊆ read** — `write_scope=all` requires `read_scope=all`. Both default to `all` (full-queue worker), and both are ignored for `admin`/`superadmin`, which always act on the whole queue. The returned role object includes `read_scope` and `write_scope`. See [Roles API — Work-Surface Scope](../http/roles.md) for the five member profiles.
|
|
183
|
+
|
|
184
|
+
```typescript
|
|
185
|
+
// See the whole queue, act only on own items (e.g. a chat-style room)
|
|
186
|
+
const result = await lt.users.addRole({
|
|
187
|
+
id: 'user_123',
|
|
188
|
+
role: 'reviewer',
|
|
189
|
+
type: 'member',
|
|
190
|
+
read_scope: 'all',
|
|
191
|
+
write_scope: 'self',
|
|
192
|
+
});
|
|
193
|
+
```
|
|
169
194
|
|
|
170
195
|
**Returns:** `LTApiResult<UserRole>` (status 201)
|
|
171
196
|
|
package/docs/auth.md
CHANGED
|
@@ -239,7 +239,7 @@ When SSO is configured and a request arrives without a Bearer token, `requireAut
|
|
|
239
239
|
|
|
240
240
|
Roles are resolved once during the token exchange (not per-request) and baked into the JWT — the same pattern as the built-in login. When the host system changes a user's roles, the new roles take effect on the next token refresh.
|
|
241
241
|
|
|
242
|
-
If `roleMap` is provided, only mapped roles are assigned. If omitted, host role names are passed through directly as LT role names. Roles named `superadmin` or `admin` are assigned the corresponding role type; all others default to `member`.
|
|
242
|
+
If `roleMap` is provided, only mapped roles are assigned. If omitted, host role names are passed through directly as LT role names. Roles named `superadmin` or `admin` are assigned the corresponding role type; all others default to `member`. Provisioned `member` grants take the default work-surface scope of `read_all`/`write_all` — the full-queue worker. Narrower scopes (for one-time or read-only users) are set through the [Roles API](api/http/roles.md#work-surface-scope) or the dashboard Scope picker.
|
|
243
243
|
|
|
244
244
|
### Standalone Deployments
|
|
245
245
|
|
package/docs/dashboard.md
CHANGED
|
@@ -187,18 +187,19 @@ Lists all durable workflow runs across the system.
|
|
|
187
187
|
|
|
188
188
|
User Accounts and Service Accounts live on the same page, separated by a tab toggle.
|
|
189
189
|
|
|
190
|
-
- **User Accounts** — human operators. Create users, assign display names, and grant roles. Roles determine which escalations a user can see and claim, and which workflows they can invoke from the dashboard.
|
|
190
|
+
- **User Accounts** — human operators. Create users, assign display names, and grant roles. Roles determine which escalations a user can see and claim, and which workflows they can invoke from the dashboard. A `member` grant carries a work-surface scope (read/write breadth) chosen from the Scope picker; see [Roles](#roles).
|
|
191
191
|
- **Service Accounts** — programmatic callers (bots, CI pipelines, external systems). Each service account has an API key for authentication. Assign roles to control access just like human users. Service accounts with the `reviewer` role can claim and resolve escalations programmatically.
|
|
192
192
|
- **Role assignment** — both account types participate in the same role system. Click any account to edit roles, change display name, or manage credentials.
|
|
193
193
|
|
|
194
194
|
**API:** `GET /api/users` lists accounts. `POST /api/users` creates. `PUT /api/users/:id/roles` assigns roles.
|
|
195
195
|
|
|
196
|
-
### Roles
|
|
196
|
+
### Roles
|
|
197
197
|
|
|
198
198
|
Define roles and configure escalation chains that control how work flows between teams.
|
|
199
199
|
|
|
200
200
|
- **Role list** — all roles in the system with their type (admin, operator, custom). Click to view assigned users.
|
|
201
201
|
- **Create Role** — add a new role. Roles referenced in workflow configs are auto-created, but you can also create them here for organizational clarity.
|
|
202
|
+
- **Scope picker** — when granting a role at `member` type, a Scope picker offers the five named work-surface profiles: full worker (`all`/`all`, default), see-all-act-own (`all`/`self`), own-items-only (`self`/`self`), read-only auditor (`all`/`none`), and read-only own (`self`/`none`). `admin` and `superadmin` grants show no Scope picker — they always work the whole queue. The picker enforces **write ⊆ read**, so a write breadth wider than the read breadth cannot be selected.
|
|
202
203
|
- **Escalation chains** — define source → target role mappings. When a reviewer escalates, the chain determines which roles receive the escalation next. Chains are directional (reviewer → engineer → admin) and support multiple targets per source.
|
|
203
204
|
|
|
204
205
|
**API:** `GET /api/roles` lists roles. `POST /api/roles` creates. `GET /api/roles/escalation-chains` lists chains. `POST /api/roles/escalation-chains` adds a chain.
|
|
@@ -245,8 +246,8 @@ The central queue for all escalation activity across every workflow.
|
|
|
245
246
|
|
|
246
247
|
- **Filter bar** — filter by status (pending/claimed/resolved), role, workflow type, priority, and time window.
|
|
247
248
|
- **Columns:** Escalation ID, workflow type, role, status, priority, created time, and claimed-by user.
|
|
248
|
-
- **Claim** — click the claim action to lock an escalation to your user. Only users with matching roles see pending escalations.
|
|
249
|
-
- **Resolve** — after claiming, submit a resolver payload (pre-filled from the workflow's `resolver_schema` if configured). Resolution triggers a workflow re-run with the resolver data injected.
|
|
249
|
+
- **Claim** — click the claim action to lock an escalation to your user. Only users with matching roles see pending escalations. The queue list and aggregate stats reflect `read_all` memberships — a member scoped to `read_self` lands directly on their own assigned item in user mode rather than browsing the full queue.
|
|
250
|
+
- **Resolve** — after claiming, submit a resolver payload (pre-filled from the workflow's `resolver_schema` if configured). Resolution triggers a workflow re-run with the resolver data injected. A `member` whose `write_scope` is `self` can resolve only items already assigned to them; `write_scope=none` is read-only.
|
|
250
251
|
- **Escalate** — forward a claimed escalation to a higher-tier role via the escalation chain.
|
|
251
252
|
|
|
252
253
|
**API:** `GET /api/escalations` lists with filters. `POST /api/escalations/:id/claim` claims. `POST /api/escalations/:id/resolve` resolves.
|
package/docs/data.md
CHANGED
|
@@ -74,6 +74,11 @@ backward-compatible **view** over it — `SELECT *` plus a computed `available`
|
|
|
74
74
|
column — so existing read queries and the public API are unchanged. Indexes are
|
|
75
75
|
managed by the SDK on `hmsh_escalations` (see below).
|
|
76
76
|
|
|
77
|
+
Role read/write scope does **not** add columns here. An escalation carries a `role`
|
|
78
|
+
and an optional `assigned_to`; work-surface scope lives on the membership table
|
|
79
|
+
(`lt_user_roles`) and is applied at read time. `condition()` / `conditionLT()` and
|
|
80
|
+
the escalation engine are unaffected.
|
|
81
|
+
|
|
77
82
|
The columns below are the `hmsh_escalations` table. The public API record
|
|
78
83
|
(`LTEscalationRecord`) is mapped from these: the JSONB `envelope` /
|
|
79
84
|
`escalation_payload` / `resolver_payload` are serialized to JSON **strings**, and
|
|
@@ -179,12 +184,14 @@ Maps users to roles. Each user can hold multiple roles with different permission
|
|
|
179
184
|
|--------|------|---------|-------------|
|
|
180
185
|
| `user_id` | `UUID NOT NULL` | — | FK to `lt_users(id)`, CASCADE on delete |
|
|
181
186
|
| `role` | `TEXT NOT NULL` | — | Role name (e.g., `reviewer`, `senior-reviewer`) |
|
|
182
|
-
| `type` | `TEXT NOT NULL` | `'member'` | `superadmin`, `admin`, or `member` |
|
|
187
|
+
| `type` | `TEXT NOT NULL` | `'member'` | `superadmin`, `admin`, or `member` — the management tier |
|
|
188
|
+
| `read_scope` | `TEXT NOT NULL` | `'all'` | `self` or `all` — search breadth for a `member`: which escalations in the role queue the member sees. `self` = items where `assigned_to = user`; `all` = the whole queue. Ignored for `admin`/`superadmin`, which always see the whole queue. |
|
|
189
|
+
| `write_scope` | `TEXT NOT NULL` | `'all'` | `none`, `self`, or `all` — claim/ack/delete breadth for a `member`. `none` = read-only; `self` = items assigned to the member; `all` = the whole queue. Ignored for `admin`/`superadmin`. |
|
|
183
190
|
| `created_at` | `TIMESTAMPTZ NOT NULL` | `NOW()` | When the role was assigned |
|
|
184
191
|
|
|
185
192
|
Primary key: `(user_id, role)` — a user can hold each role at most once.
|
|
186
193
|
|
|
187
|
-
Type is enforced by a CHECK constraint: `type IN ('superadmin', 'admin', 'member')`.
|
|
194
|
+
Type is enforced by a CHECK constraint: `type IN ('superadmin', 'admin', 'member')`. Scope is enforced by CHECK constraints: `read_scope IN ('self', 'all')`, `write_scope IN ('none', 'self', 'all')`, and **write ⊆ read** — `write_scope = 'all'` requires `read_scope = 'all'` (a member cannot act on what it cannot see). Both scopes default to `all`, the full-queue worker.
|
|
188
195
|
|
|
189
196
|
### lt_bot_api_keys
|
|
190
197
|
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
# Faceted Routing
|
|
2
|
+
|
|
3
|
+
The escalation queue is more than a HITL inbox — it is a durable, queryable pool of work
|
|
4
|
+
that any number of consumers can search and atomically claim by its **facets**. Faceted
|
|
5
|
+
routing is the platform surface for that: a composable query language over the queue and a
|
|
6
|
+
set of atomic claim primitives. The platform supplies the surface; the *policy* — what a
|
|
7
|
+
facet means, how capability and priority map onto it, the matching loop — is the consuming
|
|
8
|
+
app's. The [print-routing example](../examples/workflows/print-routing/README.md) is a full
|
|
9
|
+
worked policy built entirely on these primitives.
|
|
10
|
+
|
|
11
|
+
## Contents
|
|
12
|
+
|
|
13
|
+
- [The model](#the-model)
|
|
14
|
+
- [The query — `FacetQuery`](#the-query--facetquery)
|
|
15
|
+
- [Reads](#reads)
|
|
16
|
+
- [Atomic claims](#atomic-claims)
|
|
17
|
+
- [The dispatcher pattern](#the-dispatcher-pattern)
|
|
18
|
+
- [Recording the outcome on resolve](#recording-the-outcome-on-resolve)
|
|
19
|
+
- [Safety](#safety)
|
|
20
|
+
|
|
21
|
+
## The model
|
|
22
|
+
|
|
23
|
+
Every escalation row carries a `role` (the hard isolation boundary) and a `metadata` JSONB
|
|
24
|
+
object (the soft facets a late-binding consumer queries, sorts, and claims by). Routing
|
|
25
|
+
work is a funnel over those columns:
|
|
26
|
+
|
|
27
|
+
- **Capability** — the hard wall — is the `role`. A consumer bound to one role can never
|
|
28
|
+
see another role's work. Soft capability is `metadata @>` containment.
|
|
29
|
+
- **Preference / priority** — ordering — is `orderBy` over top-level columns and facets.
|
|
30
|
+
- **Capacity** — how much to take — is the page `limit` of a claim.
|
|
31
|
+
|
|
32
|
+
An **order** (or any multi-unit job) is a set of rows sharing an `origin_id`; its unit
|
|
33
|
+
count lives in a metadata facet so a group can be claimed all-or-nothing only when complete.
|
|
34
|
+
|
|
35
|
+
## The query — `FacetQuery`
|
|
36
|
+
|
|
37
|
+
```typescript
|
|
38
|
+
interface FacetQuery {
|
|
39
|
+
role?: string; // hard isolation (exact)
|
|
40
|
+
roles?: string[]; // role = ANY(...)
|
|
41
|
+
facets?: Record<string, any>; // metadata @> facets — required (AND), GIN-served
|
|
42
|
+
block?: Record<string, any>[]; // NOT (metadata @> ANY(block)) — exclusion list
|
|
43
|
+
range?: { facet: string; op: '<' | '<=' | '>' | '>=' | '='; value: number }[];
|
|
44
|
+
exists?: string[]; // metadata ? key — facet must be present
|
|
45
|
+
status?: string; // e.g. 'pending'
|
|
46
|
+
available?: boolean; // true = unclaimed/expired only; false = held now
|
|
47
|
+
orderBy?: { field: string; direction?: 'asc' | 'desc'; numeric?: boolean }[];
|
|
48
|
+
limit?: number;
|
|
49
|
+
offset?: number;
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
`orderBy.field` is either a whitelisted top-level column (`priority`, `created_at`,
|
|
54
|
+
`updated_at`, `status`, `role`) or a metadata path written as `metadata.<key>` (extracted as
|
|
55
|
+
text, or numeric when `numeric` is set). Every value is parameterized; only validated column
|
|
56
|
+
names / metadata keys and a fixed operator set are ever interpolated.
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
// Pending, available diabetic-certified work needing PLA, soonest-deadline first.
|
|
60
|
+
const query: FacetQuery = {
|
|
61
|
+
role: 'print-farm-diabetic',
|
|
62
|
+
status: 'pending',
|
|
63
|
+
available: true,
|
|
64
|
+
facets: { filament: 'pla' },
|
|
65
|
+
orderBy: [{ field: 'metadata.mustCompleteBy', numeric: true, direction: 'asc' }],
|
|
66
|
+
};
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Reads
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
import { searchByFacets, searchGroups, countByFacets } from '@hotmeshio/long-tail';
|
|
73
|
+
|
|
74
|
+
// Item-level: filter/sort over columns and metadata facets.
|
|
75
|
+
const { escalations, total } = await searchByFacets(query);
|
|
76
|
+
|
|
77
|
+
// Order-level: each row is an origin group with its unit count, availability and
|
|
78
|
+
// completeness — what a batched dispatcher reads to page by capacity.
|
|
79
|
+
const groups = await searchGroups(query, { sizeFacet: 'orderSize', limit: 10 });
|
|
80
|
+
|
|
81
|
+
// Aggregate: capacity / in-flight soft-limit checks.
|
|
82
|
+
const inFlight = await countByFacets({ role: 'print-farm-diabetic', available: false });
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Atomic claims
|
|
86
|
+
|
|
87
|
+
Two claim primitives, both `FOR UPDATE SKIP LOCKED` so many consumers run without
|
|
88
|
+
contention. Each takes the `FacetQuery`, a `consumer` id, and options.
|
|
89
|
+
|
|
90
|
+
```typescript
|
|
91
|
+
import { claimGroups, claimByFacets } from '@hotmeshio/long-tail';
|
|
92
|
+
|
|
93
|
+
// Claim whole orders (origin groups), all-or-nothing, in rank order. The page `limit`
|
|
94
|
+
// is how many consumers/printers are free, so a dependent grabs exactly what it can
|
|
95
|
+
// distribute. A group is only eligible when complete (member count = declared size).
|
|
96
|
+
const orders = await claimGroups(query, 'broker-1', { limit: 4, sizeFacet: 'orderSize' });
|
|
97
|
+
|
|
98
|
+
// Claim individual rows by facet — the single-row sibling. Locks up to `limit` available
|
|
99
|
+
// rows in rank order. With { allOrNone: true } the claim commits only when the full limit
|
|
100
|
+
// is acquired, otherwise it rolls back and returns [] — the all-or-none lock a dispatcher
|
|
101
|
+
// wants over a counted set it anticipated.
|
|
102
|
+
const printers = await claimByFacets(
|
|
103
|
+
{ role: 'printer-pool-diabetic', facets: { state: 'ready' } },
|
|
104
|
+
'broker-1',
|
|
105
|
+
{ limit: 4 },
|
|
106
|
+
);
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
A claim sets `assigned_to` + `assigned_until` (default 30 minutes). Availability is a
|
|
110
|
+
query, not a flag: a row is held iff `assigned_to` is set and `assigned_until > NOW()`.
|
|
111
|
+
Resolve, release, or let the claim expire to return it to the pool.
|
|
112
|
+
|
|
113
|
+
## The dispatcher pattern
|
|
114
|
+
|
|
115
|
+
The primitives compose into a market: pull a *page* of complete orders sized to how many
|
|
116
|
+
consumers are free, then distribute one per consumer. Reads go through the
|
|
117
|
+
`public.lt_escalations` view (it adds the computed `available` flag); atomic claims run on
|
|
118
|
+
the shared `public.hmsh_escalations` table — the same raw-SQL-on-the-shared-table pattern as
|
|
119
|
+
the rest of the escalation service. Metadata facets use the GIN index (`@>`).
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
// 1. Anticipate supply.
|
|
123
|
+
const free = await searchByFacets({ role: 'printer-pool', available: true, facets: { state: 'ready' } });
|
|
124
|
+
// 2. Claim that much demand by priority.
|
|
125
|
+
const orders = await claimGroups(demandQuery, consumer, { limit: free.escalations.length });
|
|
126
|
+
// 3. Lock the printers for the claimed orders, all-or-none.
|
|
127
|
+
const printers = await claimByFacets(supplyQuery, consumer, { limit: orders.length, allOrNone: true });
|
|
128
|
+
// 4. Pair, do the work, resolve both.
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
See the [print-routing example](../examples/workflows/print-routing/README.md) for the full
|
|
132
|
+
two-sided market, carry-forward under contention, and the inspection/convergence loop.
|
|
133
|
+
|
|
134
|
+
## Recording the outcome on resolve
|
|
135
|
+
|
|
136
|
+
A row is created carrying **intent** — the facets that route and claim it. Resolving it can
|
|
137
|
+
record the **outcome** onto the same row: pass a `metadata` patch and it is merged (not
|
|
138
|
+
replaced) into the GIN-indexed metadata in the same atomic UPDATE, on the winning resolve
|
|
139
|
+
only. "What actually happened" becomes `@>`-queryable next to "what was asked".
|
|
140
|
+
|
|
141
|
+
```typescript
|
|
142
|
+
import { resolveEscalation } from '@hotmeshio/long-tail';
|
|
143
|
+
|
|
144
|
+
// The work is done; resolve the row AND stamp the outcome onto it — one atomic call.
|
|
145
|
+
await resolveEscalation(rowId, { result: 'success' }, {
|
|
146
|
+
outcome: 'success',
|
|
147
|
+
unitsPrinted: 6,
|
|
148
|
+
});
|
|
149
|
+
// Duration needs no facet: the row's own created_at → resolved_at is the elapsed time.
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
The third argument is distinct from the second: `resolverPayload` (arg 2) is delivered to a
|
|
153
|
+
waiting workflow as `condition()`'s return value and is **not** indexed; the `metadata` patch
|
|
154
|
+
(arg 3) is the durable, queryable record on the row. With both, the row alone answers what was
|
|
155
|
+
asked, what happened, and how long it took:
|
|
156
|
+
|
|
157
|
+
```typescript
|
|
158
|
+
searchByFacets({ role: 'printer-pool-diabetic', facets: { state: 'printing', outcome: 'success' } });
|
|
159
|
+
// → every completed job: which consumer, which origin, how long — no side table to reconcile
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
The print farm leans on this: a printer resolves its in-flight `printing` row with the run's
|
|
163
|
+
result and units in one atomic call, so the supply pond is also the production log — and the
|
|
164
|
+
row's `created_at` → `resolved_at` is the print duration, no stored field required.
|
|
165
|
+
|
|
166
|
+
## The human / operations query
|
|
167
|
+
|
|
168
|
+
The same query language is also the operations team's read surface. Where the dispatcher
|
|
169
|
+
sections above target a **single pond** for a workflow consumer, a **person** — a superadmin, or
|
|
170
|
+
a member of several roles — queries "what work needs doing / was done" across **their** roles by
|
|
171
|
+
the same facets, through the scoped list API.
|
|
172
|
+
|
|
173
|
+
```typescript
|
|
174
|
+
import { createClient } from '@hotmeshio/long-tail/sdk';
|
|
175
|
+
const lt = createClient({ auth }); // auth carries the caller's userId
|
|
176
|
+
|
|
177
|
+
// Faceted, role-scoped, paginated — the filter AND the count run in SQL.
|
|
178
|
+
const res = await lt.escalations.list({
|
|
179
|
+
status: 'pending',
|
|
180
|
+
facets: { flags: 'too_short' }, // metadata @> (GIN-served)
|
|
181
|
+
range: [{ facet: 'confidence', op: '<=', value: 0.7 }],
|
|
182
|
+
exists: ['needsReview'],
|
|
183
|
+
block: [{ outcome: 'success' }], // exclude completed
|
|
184
|
+
orderBy: [{ field: 'metadata.confidence', numeric: true, direction: 'asc' }],
|
|
185
|
+
limit: 50, offset: 0,
|
|
186
|
+
});
|
|
187
|
+
// res.data → { escalations, total }
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
`list` and `listAvailable` accept the FacetQuery elements (`facets`, `block`, `range`, `exists`,
|
|
191
|
+
`roles`, `available`, `orderBy`) on top of the plain filters. When any are present the request
|
|
192
|
+
runs through the scoped faceted query; otherwise the simple list path is used.
|
|
193
|
+
|
|
194
|
+
**Role scope is enforced in SQL, not in the controller.** The result is the read-scope predicate
|
|
195
|
+
ANDed with the facets:
|
|
196
|
+
|
|
197
|
+
- **superadmin / admin-of-admin** (global) — every role's queue, no role filter.
|
|
198
|
+
- **`read_all` on a role** — the whole pond for that role.
|
|
199
|
+
- **`read_self` on a role** — only items assigned to the caller (`assigned_to = me`).
|
|
200
|
+
|
|
201
|
+
A caller may **narrow** within their scope (pass `role`/`roles`) but can never **widen** past it —
|
|
202
|
+
a `role` they don't hold simply matches nothing. The page total is computed by the same WHERE, so
|
|
203
|
+
pagination stays correct; the controller never filters a fetched page client-side.
|
|
204
|
+
|
|
205
|
+
Over HTTP the faceted elements are JSON-encoded query params, so the whole query rides on a GET
|
|
206
|
+
URL and the dashboard's copy-URL / copy-curl reproduces the exact request:
|
|
207
|
+
|
|
208
|
+
```
|
|
209
|
+
GET /api/escalations?status=pending&facets=%7B%22flags%22%3A%22too_short%22%7D&range=%5B%7B%22facet%22%3A%22confidence%22%2C%22op%22%3A%22%3C%3D%22%2C%22value%22%3A0.7%7D%5D
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
## Safety
|
|
213
|
+
|
|
214
|
+
- **Parameterized** — every value is a bound parameter; only audited column names, metadata
|
|
215
|
+
keys (strict `^[a-zA-Z0-9_]+$`), and a fixed operator set are interpolated.
|
|
216
|
+
- **Atomic** — claims are single transactions with `FOR UPDATE SKIP LOCKED`; group claims
|
|
217
|
+
re-check completeness *inside* the lock, so concurrent dispatchers never split a group or
|
|
218
|
+
double-claim a row.
|
|
219
|
+
- **Durable consumers** — because the surface is a query, a consumer that holds a claim it
|
|
220
|
+
cannot yet place can carry it across `continueAsNew` and converge later rather than
|
|
221
|
+
releasing — the durable workflow is the coordinator, not a distributed transaction.
|
package/docs/hitl-guide.md
CHANGED
|
@@ -158,6 +158,29 @@ export async function reviewWorkflow(envelope: LTEnvelope) {
|
|
|
158
158
|
|
|
159
159
|
---
|
|
160
160
|
|
|
161
|
+
## Recording the Outcome on Resolution
|
|
162
|
+
|
|
163
|
+
An escalation row is created carrying **intent** — what was asked, who it routed to. Resolving it can stamp the **outcome** onto the same row: every resolve surface takes an optional `metadata` patch, merged into the row's GIN-indexed metadata.
|
|
164
|
+
|
|
165
|
+
| Surface | How to pass it |
|
|
166
|
+
|---------|----------------|
|
|
167
|
+
| HTTP | `metadata` in the resolve body (`POST /api/escalations/:id/resolve`, `/resolve-by-signal-key`) |
|
|
168
|
+
| SDK facade | `lt.escalations.resolve({ id, resolverPayload, metadata })` |
|
|
169
|
+
| MCP | `metadata` arg on `claim_and_resolve` / `resolve_escalation` |
|
|
170
|
+
| In-process library | `resolveEscalation(id, payload, metadata)` / `resolveEscalationBySignalKey(signalKey, payload, metadata)` |
|
|
171
|
+
|
|
172
|
+
```typescript
|
|
173
|
+
await lt.escalations.resolve({
|
|
174
|
+
id,
|
|
175
|
+
resolverPayload: { approved: true }, // resumes the workflow; not indexed
|
|
176
|
+
metadata: { outcome: 'approved', reviewedBy: 'alice', durationMs: elapsed },
|
|
177
|
+
});
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
The patch is distinct from the resolver payload: the payload resumes the paused workflow and is not indexed; the metadata patch is the durable, queryable record on the row. Use it for the audit trail and analytics — disposition, reviewer, time-to-resolve — so the escalation table answers *what was asked, what was decided, and how long it took* without a parallel log.
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
161
184
|
## JSON Schema Form Authoring
|
|
162
185
|
|
|
163
186
|
The dashboard renders forms automatically from JSON Schema. No frontend code needed.
|
|
@@ -456,6 +479,33 @@ await ltCreateEscalation({
|
|
|
456
479
|
});
|
|
457
480
|
```
|
|
458
481
|
|
|
482
|
+
### Work-Surface Scope
|
|
483
|
+
|
|
484
|
+
A `member` of a role carries a work-surface scope that narrows what they see and act on within that queue: `read_scope` (`self` | `all`) governs which escalations they see, and `write_scope` (`none` | `self` | `all`) governs which they can claim, resolve, or cancel. `self` means escalations assigned to that member (`assigned_to = user`); `all` means the whole role queue. `admin` and `superadmin` always work the whole queue. See the [Roles API](api/http/roles.md#work-surface-scope) for the five member profiles and the assignment contract.
|
|
485
|
+
|
|
486
|
+
Scope is a property of the **membership** (`lt_user_roles`), not of the escalation. The escalation engine is unchanged: `condition()` / `conditionLT()`, `ltCreateEscalation`, and the interceptor write escalation rows (`hmsh_escalations`) exactly as before — an escalation carries a `role` and an optional `assigned_to`, with no scope column. Scope is resolved at read time, when a *user* lists or acts on the queue. Escalating to a role that does not exist yet still registers the role name only — roles are typeless; `type` and scope are set per user when the role is granted. The one place scope affects creation is the standalone `POST /api/escalations` HTTP endpoint, which requires `write_scope=all`; workflow-emitted escalations (`conditionLT`, interceptor returns, `ltCreateEscalation`) do not pass through that check.
|
|
487
|
+
|
|
488
|
+
### One-Time and Pre-Assigned Users
|
|
489
|
+
|
|
490
|
+
To route a single item to a named person, assign the escalation to them and provision them with `read_scope=self` + `write_scope=self`. The workflow sets `assigned_to` to the person's user ID (a pre-claim) when it creates the escalation, then provisions or updates that user as a `member` with self/self scope on the target role. They land directly on that one item — a just-in-time form scoped by RBAC, with no access to the rest of the queue and no direct table access. An update or follow-up is simply another workflow firing another escalation to that same person.
|
|
491
|
+
|
|
492
|
+
```typescript
|
|
493
|
+
// Pre-assign the escalation to a specific person and route a one-time form to them
|
|
494
|
+
await ltCreateEscalation({
|
|
495
|
+
role: 'customer-triage',
|
|
496
|
+
assigned_to: userId, // pre-claim — durable, keyed off the user, not the soft-lock TTL
|
|
497
|
+
description: 'Confirm your shipping address',
|
|
498
|
+
metadata: {
|
|
499
|
+
form_schema: {
|
|
500
|
+
title: 'Confirm Address',
|
|
501
|
+
properties: { address: { type: 'string' }, confirmed: { type: 'boolean' } },
|
|
502
|
+
required: ['confirmed'],
|
|
503
|
+
},
|
|
504
|
+
},
|
|
505
|
+
});
|
|
506
|
+
// The person is provisioned as a member of `customer-triage` with read_scope=self, write_scope=self.
|
|
507
|
+
```
|
|
508
|
+
|
|
459
509
|
### Escalation Chains
|
|
460
510
|
|
|
461
511
|
Users can escalate to other roles via the "Escalate" tab:
|