@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.
Files changed (337) hide show
  1. package/README.md +1 -0
  2. package/build/api/escalations/bulk.js +9 -1
  3. package/build/api/escalations/cancel.js +5 -7
  4. package/build/api/escalations/claim.js +18 -15
  5. package/build/api/escalations/create.d.ts +6 -0
  6. package/build/api/escalations/create.js +11 -8
  7. package/build/api/escalations/facets.d.ts +18 -0
  8. package/build/api/escalations/facets.js +115 -0
  9. package/build/api/escalations/helpers.d.ts +70 -1
  10. package/build/api/escalations/helpers.js +101 -7
  11. package/build/api/escalations/index.d.ts +3 -2
  12. package/build/api/escalations/index.js +7 -1
  13. package/build/api/escalations/list.d.ts +21 -2
  14. package/build/api/escalations/list.js +109 -10
  15. package/build/api/escalations/metadata.js +22 -27
  16. package/build/api/escalations/resolve.d.ts +19 -1
  17. package/build/api/escalations/resolve.js +111 -26
  18. package/build/api/escalations/single.js +8 -8
  19. package/build/api/users.d.ts +6 -0
  20. package/build/api/users.js +29 -1
  21. package/build/bin/ltc.js +40 -0
  22. package/build/lib/cli/commands/escalations.d.ts +36 -0
  23. package/build/lib/cli/commands/escalations.js +98 -0
  24. package/build/lib/db/schemas/001_schema.sql +10 -5
  25. package/build/lib/db/schemas/012_lt_tasks_workflow_id_unique.sql +20 -0
  26. package/build/lib/db/schemas/013_role_scope.sql +37 -0
  27. package/build/modules/version.d.ts +10 -5
  28. package/build/modules/version.js +27 -11
  29. package/build/routes/escalations/facets.d.ts +7 -0
  30. package/build/routes/escalations/facets.js +81 -0
  31. package/build/routes/escalations/index.js +3 -0
  32. package/build/routes/escalations/list.js +46 -0
  33. package/build/routes/escalations/resolve.js +22 -4
  34. package/build/routes/users.js +9 -5
  35. package/build/sdk/index.d.ts +33 -2
  36. package/build/sdk/index.js +7 -0
  37. package/build/services/escalation/crud.d.ts +56 -6
  38. package/build/services/escalation/crud.js +102 -31
  39. package/build/services/escalation/facet-sql.d.ts +18 -0
  40. package/build/services/escalation/facet-sql.js +118 -0
  41. package/build/services/escalation/facets.d.ts +45 -0
  42. package/build/services/escalation/facets.js +218 -0
  43. package/build/services/escalation/index.d.ts +1 -0
  44. package/build/services/escalation/index.js +8 -1
  45. package/build/services/escalation/queries.d.ts +45 -0
  46. package/build/services/escalation/queries.js +107 -6
  47. package/build/services/escalation/sql.d.ts +10 -4
  48. package/build/services/escalation/sql.js +36 -8
  49. package/build/services/interceptor/activities/escalation.d.ts +2 -0
  50. package/build/services/interceptor/activities/escalation.js +1 -1
  51. package/build/services/interceptor/lifecycle.js +4 -0
  52. package/build/services/mcp/server-tools.js +10 -4
  53. package/build/services/orchestrator/condition.js +6 -2
  54. package/build/services/role/index.js +2 -3
  55. package/build/services/role/sql.d.ts +8 -0
  56. package/build/services/role/sql.js +17 -1
  57. package/build/services/task/crud.js +22 -14
  58. package/build/services/task/process.js +3 -21
  59. package/build/services/task/sql.d.ts +24 -1
  60. package/build/services/task/sql.js +65 -1
  61. package/build/services/user/crud.d.ts +1 -0
  62. package/build/services/user/crud.js +29 -11
  63. package/build/services/user/index.d.ts +2 -1
  64. package/build/services/user/index.js +12 -1
  65. package/build/services/user/rbac.d.ts +18 -0
  66. package/build/services/user/rbac.js +27 -0
  67. package/build/services/user/roles.d.ts +7 -2
  68. package/build/services/user/roles.js +21 -5
  69. package/build/services/user/scope.d.ts +22 -0
  70. package/build/services/user/scope.js +50 -0
  71. package/build/services/user/seed-admin.js +34 -17
  72. package/build/services/user/sql.d.ts +18 -7
  73. package/build/services/user/sql.js +38 -13
  74. package/build/services/user/sso-provision.js +41 -24
  75. package/build/services/user/types.d.ts +9 -5
  76. package/build/system/mcp-servers/admin/escalations.js +65 -0
  77. package/build/system/mcp-servers/admin/schemas.d.ts +325 -0
  78. package/build/system/mcp-servers/admin/schemas.js +63 -2
  79. package/build/system/mcp-servers/admin/users.js +28 -2
  80. package/build/tsconfig.tsbuildinfo +1 -1
  81. package/build/types/envelope.d.ts +7 -0
  82. package/build/types/facets.d.ts +60 -0
  83. package/build/types/facets.js +10 -0
  84. package/build/types/index.d.ts +2 -1
  85. package/build/types/user.d.ts +11 -0
  86. package/dashboard/dist/assets/AdminDashboard-C5gfVEeL.js +2 -0
  87. package/dashboard/dist/assets/{AdminDashboard-LTiYLuzq.js.map → AdminDashboard-C5gfVEeL.js.map} +1 -1
  88. package/dashboard/dist/assets/{AgentConfigPage-nlOudfpZ.js → AgentConfigPage-CWEnuiGN.js} +6 -6
  89. package/dashboard/dist/assets/{AgentConfigPage-nlOudfpZ.js.map → AgentConfigPage-CWEnuiGN.js.map} +1 -1
  90. package/dashboard/dist/assets/{AgentDetailPage-BSUnDvGp.js → AgentDetailPage-B6IE27iK.js} +3 -3
  91. package/dashboard/dist/assets/AgentDetailPage-B6IE27iK.js.map +1 -0
  92. package/dashboard/dist/assets/AgentsPage-Blhh2RsK.js +2 -0
  93. package/dashboard/dist/assets/{AgentsPage-DpS0cCYe.js.map → AgentsPage-Blhh2RsK.js.map} +1 -1
  94. package/dashboard/dist/assets/AvailableEscalationsPage-ClwAle76.js +2 -0
  95. package/dashboard/dist/assets/AvailableEscalationsPage-ClwAle76.js.map +1 -0
  96. package/dashboard/dist/assets/{BotPicker-BhKmq7M7.js → BotPicker-DSozotGk.js} +2 -2
  97. package/dashboard/dist/assets/{BotPicker-BhKmq7M7.js.map → BotPicker-DSozotGk.js.map} +1 -1
  98. package/dashboard/dist/assets/CapabilitiesPage-CrS_DTA1.js +2 -0
  99. package/dashboard/dist/assets/CapabilitiesPage-CrS_DTA1.js.map +1 -0
  100. package/dashboard/dist/assets/{CollapsibleSection-D7SVwNVk.js → CollapsibleSection-1i6bOQ1L.js} +2 -2
  101. package/dashboard/dist/assets/{CollapsibleSection-D7SVwNVk.js.map → CollapsibleSection-1i6bOQ1L.js.map} +1 -1
  102. package/dashboard/dist/assets/CopyableId-blg-gZvT.js +2 -0
  103. package/dashboard/dist/assets/CopyableId-blg-gZvT.js.map +1 -0
  104. package/dashboard/dist/assets/{CredentialsPage-fat5Z2U9.js → CredentialsPage-BxJio06E.js} +2 -2
  105. package/dashboard/dist/assets/{CredentialsPage-fat5Z2U9.js.map → CredentialsPage-BxJio06E.js.map} +1 -1
  106. package/dashboard/dist/assets/{CronLabel-OOP4P2mJ.js → CronLabel-BAfIHHz4.js} +2 -2
  107. package/dashboard/dist/assets/{CronLabel-OOP4P2mJ.js.map → CronLabel-BAfIHHz4.js.map} +1 -1
  108. package/dashboard/dist/assets/{CustomDurationPicker-COacSAXO.js → CustomDurationPicker-BLF0NFJK.js} +2 -2
  109. package/dashboard/dist/assets/{CustomDurationPicker-COacSAXO.js.map → CustomDurationPicker-BLF0NFJK.js.map} +1 -1
  110. package/dashboard/dist/assets/{DataTable-D9yuBv0w.js → DataTable-ZWgVQg7F.js} +2 -2
  111. package/dashboard/dist/assets/DataTable-ZWgVQg7F.js.map +1 -0
  112. package/dashboard/dist/assets/{DropZone-Bfw2YLVS.js → DropZone-CEx_wk-g.js} +2 -2
  113. package/dashboard/dist/assets/{DropZone-Bfw2YLVS.js.map → DropZone-CEx_wk-g.js.map} +1 -1
  114. package/dashboard/dist/assets/{ElapsedCell-CkfCD-m7.js → ElapsedCell-nKGJvznH.js} +2 -2
  115. package/dashboard/dist/assets/{ElapsedCell-CkfCD-m7.js.map → ElapsedCell-nKGJvznH.js.map} +1 -1
  116. package/dashboard/dist/assets/{EscalationsOverview-BmI8jxlh.js → EscalationsOverview-bNNVA6R2.js} +2 -2
  117. package/dashboard/dist/assets/{EscalationsOverview-BmI8jxlh.js.map → EscalationsOverview-bNNVA6R2.js.map} +1 -1
  118. package/dashboard/dist/assets/{EventTable-CeKfFk3m.js → EventTable-BCuDdqlU.js} +2 -2
  119. package/dashboard/dist/assets/{EventTable-CeKfFk3m.js.map → EventTable-BCuDdqlU.js.map} +1 -1
  120. package/dashboard/dist/assets/FilterBar-BQ2JGg8U.js +2 -0
  121. package/dashboard/dist/assets/FilterBar-BQ2JGg8U.js.map +1 -0
  122. package/dashboard/dist/assets/GraphInvokePage-BPAvqHls.js +2 -0
  123. package/dashboard/dist/assets/GraphInvokePage-BPAvqHls.js.map +1 -0
  124. package/dashboard/dist/assets/HomePage-BZ1YTrjR.js +2 -0
  125. package/dashboard/dist/assets/HomePage-BZ1YTrjR.js.map +1 -0
  126. package/dashboard/dist/assets/{ListToolbar-BJaCzAXS.js → ListToolbar-DIJZfEgu.js} +2 -2
  127. package/dashboard/dist/assets/{ListToolbar-BJaCzAXS.js.map → ListToolbar-DIJZfEgu.js.map} +1 -1
  128. package/dashboard/dist/assets/{McpOverview-CvnDG5OB.js → McpOverview-DCfSCfPg.js} +2 -2
  129. package/dashboard/dist/assets/{McpOverview-CvnDG5OB.js.map → McpOverview-DCfSCfPg.js.map} +1 -1
  130. package/dashboard/dist/assets/{McpQueryDetailPage-DwmSIcIL.js → McpQueryDetailPage-mb7rmtNe.js} +2 -2
  131. package/dashboard/dist/assets/{McpQueryDetailPage-DwmSIcIL.js.map → McpQueryDetailPage-mb7rmtNe.js.map} +1 -1
  132. package/dashboard/dist/assets/{McpQueryPage-CU6FMEEk.js → McpQueryPage-D9b1T0I3.js} +2 -2
  133. package/dashboard/dist/assets/{McpQueryPage-CU6FMEEk.js.map → McpQueryPage-D9b1T0I3.js.map} +1 -1
  134. package/dashboard/dist/assets/{McpRunDetailPage-8a6lsehv.js → McpRunDetailPage-Bz2UyuNm.js} +2 -2
  135. package/dashboard/dist/assets/{McpRunDetailPage-8a6lsehv.js.map → McpRunDetailPage-Bz2UyuNm.js.map} +1 -1
  136. package/dashboard/dist/assets/McpRunsPage-Dw7w_ZO-.js +2 -0
  137. package/dashboard/dist/assets/McpRunsPage-Dw7w_ZO-.js.map +1 -0
  138. package/dashboard/dist/assets/{NamespacePill-D6btr6e8.js → NamespacePill-D27vqDXy.js} +2 -2
  139. package/dashboard/dist/assets/{NamespacePill-D6btr6e8.js.map → NamespacePill-D27vqDXy.js.map} +1 -1
  140. package/dashboard/dist/assets/OperatorDashboard-CTzSDW3N.js +2 -0
  141. package/dashboard/dist/assets/{OperatorDashboard-g9o7I925.js.map → OperatorDashboard-CTzSDW3N.js.map} +1 -1
  142. package/dashboard/dist/assets/PageHeader-B_1zTx2q.js +2 -0
  143. package/dashboard/dist/assets/PageHeader-B_1zTx2q.js.map +1 -0
  144. package/dashboard/dist/assets/{PageHeaderWithStats-DJQCZ0FR.js → PageHeaderWithStats-D-cPg7UD.js} +2 -2
  145. package/dashboard/dist/assets/{PageHeaderWithStats-DJQCZ0FR.js.map → PageHeaderWithStats-D-cPg7UD.js.map} +1 -1
  146. package/dashboard/dist/assets/PriorityBadge-B4ykBH7f.js +2 -0
  147. package/dashboard/dist/assets/PriorityBadge-B4ykBH7f.js.map +1 -0
  148. package/dashboard/dist/assets/{ProcessDetailPage-ByyWCs2A.js → ProcessDetailPage-olG2CS-h.js} +2 -2
  149. package/dashboard/dist/assets/{ProcessDetailPage-ByyWCs2A.js.map → ProcessDetailPage-olG2CS-h.js.map} +1 -1
  150. package/dashboard/dist/assets/{ProcessesListPage-BTsXds4l.js → ProcessesListPage-CUZSL06Y.js} +2 -2
  151. package/dashboard/dist/assets/{ProcessesListPage-BTsXds4l.js.map → ProcessesListPage-CUZSL06Y.js.map} +1 -1
  152. package/dashboard/dist/assets/RolePill-B-V-31lC.js +2 -0
  153. package/dashboard/dist/assets/RolePill-B-V-31lC.js.map +1 -0
  154. package/dashboard/dist/assets/RolesPage-DLnaW6iD.js +2 -0
  155. package/dashboard/dist/assets/RolesPage-DLnaW6iD.js.map +1 -0
  156. package/dashboard/dist/assets/RowActions-BA9L9djf.js +2 -0
  157. package/dashboard/dist/assets/RowActions-BA9L9djf.js.map +1 -0
  158. package/dashboard/dist/assets/{RunAsSelector-BzkZXpSX.js → RunAsSelector-BoLHF4fy.js} +2 -2
  159. package/dashboard/dist/assets/{RunAsSelector-BzkZXpSX.js.map → RunAsSelector-BoLHF4fy.js.map} +1 -1
  160. package/dashboard/dist/assets/StickyPagination-BEMY1L-H.js +2 -0
  161. package/dashboard/dist/assets/StickyPagination-BEMY1L-H.js.map +1 -0
  162. package/dashboard/dist/assets/{StreamMessageDetail-B6ch-6SY.js → StreamMessageDetail-CVRVWXcU.js} +2 -2
  163. package/dashboard/dist/assets/{StreamMessageDetail-B6ch-6SY.js.map → StreamMessageDetail-CVRVWXcU.js.map} +1 -1
  164. package/dashboard/dist/assets/{SwimlaneTimeline-CjvfUkls.js → SwimlaneTimeline-DDpU0-Jn.js} +2 -2
  165. package/dashboard/dist/assets/{SwimlaneTimeline-CjvfUkls.js.map → SwimlaneTimeline-DDpU0-Jn.js.map} +1 -1
  166. package/dashboard/dist/assets/{TagInput-CZSeyesi.js → TagInput-BHVfG307.js} +2 -2
  167. package/dashboard/dist/assets/{TagInput-CZSeyesi.js.map → TagInput-BHVfG307.js.map} +1 -1
  168. package/dashboard/dist/assets/{TaskDetailPage-BjHU6opi.js → TaskDetailPage-B_xQtJMg.js} +2 -2
  169. package/dashboard/dist/assets/{TaskDetailPage-BjHU6opi.js.map → TaskDetailPage-B_xQtJMg.js.map} +1 -1
  170. package/dashboard/dist/assets/{TaskQueuePill-CQrXrSxk.js → TaskQueuePill-CGlxXqWt.js} +2 -2
  171. package/dashboard/dist/assets/{TaskQueuePill-CQrXrSxk.js.map → TaskQueuePill-CGlxXqWt.js.map} +1 -1
  172. package/dashboard/dist/assets/{TasksListPage-Bem44zd0.js → TasksListPage-CsrdIOKk.js} +2 -2
  173. package/dashboard/dist/assets/{TasksListPage-Bem44zd0.js.map → TasksListPage-CsrdIOKk.js.map} +1 -1
  174. package/dashboard/dist/assets/TimeAgo-DmicaxjT.js +2 -0
  175. package/dashboard/dist/assets/{TimeAgo-CUIYqSt_.js.map → TimeAgo-DmicaxjT.js.map} +1 -1
  176. package/dashboard/dist/assets/{TimestampCell--tnreI1i.js → TimestampCell-DeK89eRV.js} +2 -2
  177. package/dashboard/dist/assets/{TimestampCell--tnreI1i.js.map → TimestampCell-DeK89eRV.js.map} +1 -1
  178. package/dashboard/dist/assets/{ToolPill-O09jsTQM.js → ToolPill-NV7PxSpA.js} +2 -2
  179. package/dashboard/dist/assets/{ToolPill-O09jsTQM.js.map → ToolPill-NV7PxSpA.js.map} +1 -1
  180. package/dashboard/dist/assets/{ToolTestPanel-OD99H7Pk.js → ToolTestPanel-D3s2n9-9.js} +2 -2
  181. package/dashboard/dist/assets/{ToolTestPanel-OD99H7Pk.js.map → ToolTestPanel-D3s2n9-9.js.map} +1 -1
  182. package/dashboard/dist/assets/{TopicDetailPage-rh_dsTJI.js → TopicDetailPage-qyv7UWT_.js} +3 -3
  183. package/dashboard/dist/assets/TopicDetailPage-qyv7UWT_.js.map +1 -0
  184. package/dashboard/dist/assets/TopicsPage-CEUfMoed.js +2 -0
  185. package/dashboard/dist/assets/TopicsPage-CEUfMoed.js.map +1 -0
  186. package/dashboard/dist/assets/{UserName-C__c8BN0.js → UserName-Bn_BxI8K.js} +2 -2
  187. package/dashboard/dist/assets/{UserName-C__c8BN0.js.map → UserName-Bn_BxI8K.js.map} +1 -1
  188. package/dashboard/dist/assets/{WorkflowExecutionPage-Bw6QdtUO.js → WorkflowExecutionPage-DAk2RqpB.js} +2 -2
  189. package/dashboard/dist/assets/{WorkflowExecutionPage-Bw6QdtUO.js.map → WorkflowExecutionPage-DAk2RqpB.js.map} +1 -1
  190. package/dashboard/dist/assets/{WorkflowPill-CS-IsR0U.js → WorkflowPill-DDR0B6DN.js} +2 -2
  191. package/dashboard/dist/assets/{WorkflowPill-CS-IsR0U.js.map → WorkflowPill-DDR0B6DN.js.map} +1 -1
  192. package/dashboard/dist/assets/WorkflowsDashboard-DfBgM80t.js +2 -0
  193. package/dashboard/dist/assets/WorkflowsDashboard-DfBgM80t.js.map +1 -0
  194. package/dashboard/dist/assets/{WorkflowsOverview-BRc0QbA7.js → WorkflowsOverview--Dkoo9Jq.js} +2 -2
  195. package/dashboard/dist/assets/{WorkflowsOverview-BRc0QbA7.js.map → WorkflowsOverview--Dkoo9Jq.js.map} +1 -1
  196. package/dashboard/dist/assets/{YamlWorkflowDetailPage-DdzspNQR.js → YamlWorkflowDetailPage-B6HYbfFj.js} +25 -25
  197. package/dashboard/dist/assets/{YamlWorkflowDetailPage-DdzspNQR.js.map → YamlWorkflowDetailPage-B6HYbfFj.js.map} +1 -1
  198. package/dashboard/dist/assets/{YamlWorkflowsPage-Djw5ZbgQ.js → YamlWorkflowsPage-CJE-PxC0.js} +2 -2
  199. package/dashboard/dist/assets/{YamlWorkflowsPage-Djw5ZbgQ.js.map → YamlWorkflowsPage-CJE-PxC0.js.map} +1 -1
  200. package/dashboard/dist/assets/{agents-xstNkF6c.js → agents-DUciUZpt.js} +2 -2
  201. package/dashboard/dist/assets/{agents-xstNkF6c.js.map → agents-DUciUZpt.js.map} +1 -1
  202. package/dashboard/dist/assets/{bots-BztAI0s_.js → bots-BnyqCfuC.js} +2 -2
  203. package/dashboard/dist/assets/{bots-BztAI0s_.js.map → bots-BnyqCfuC.js.map} +1 -1
  204. package/dashboard/dist/assets/{capabilities-CEDwtxNL.js → capabilities-CkTWWT0q.js} +2 -2
  205. package/dashboard/dist/assets/{capabilities-CEDwtxNL.js.map → capabilities-CkTWWT0q.js.map} +1 -1
  206. package/dashboard/dist/assets/{controlplane-BlGEsRKe.js → controlplane-D9HaFZsC.js} +2 -2
  207. package/dashboard/dist/assets/{controlplane-BlGEsRKe.js.map → controlplane-D9HaFZsC.js.map} +1 -1
  208. package/dashboard/dist/assets/{escalation-iI4QKlP-.js → escalation-De9JnTKI.js} +2 -2
  209. package/dashboard/dist/assets/{escalation-iI4QKlP-.js.map → escalation-De9JnTKI.js.map} +1 -1
  210. package/dashboard/dist/assets/escalation-columns-Df7Pluzg.js +2 -0
  211. package/dashboard/dist/assets/escalation-columns-Df7Pluzg.js.map +1 -0
  212. package/dashboard/dist/assets/index-7hqtQI43.js +2 -0
  213. package/dashboard/dist/assets/index-7hqtQI43.js.map +1 -0
  214. package/dashboard/dist/assets/{index-CAqEy2Db.js → index-B07xLlyz.js} +2 -2
  215. package/dashboard/dist/assets/{index-CAqEy2Db.js.map → index-B07xLlyz.js.map} +1 -1
  216. package/dashboard/dist/assets/{index-D1xVY4JR.js → index-B1kc6itM.js} +24 -24
  217. package/dashboard/dist/assets/index-B1kc6itM.js.map +1 -0
  218. package/dashboard/dist/assets/index-BAhkWcOq.js +9 -0
  219. package/dashboard/dist/assets/index-BAhkWcOq.js.map +1 -0
  220. package/dashboard/dist/assets/{index-Dsi5o7dx.js → index-BsIaUbKD.js} +2 -2
  221. package/dashboard/dist/assets/{index-Dsi5o7dx.js.map → index-BsIaUbKD.js.map} +1 -1
  222. package/dashboard/dist/assets/index-BxdiKgnH.js +6 -0
  223. package/dashboard/dist/assets/index-BxdiKgnH.js.map +1 -0
  224. package/dashboard/dist/assets/{index-B7VC0vjf.js → index-BzMsh6Ui.js} +2 -2
  225. package/dashboard/dist/assets/{index-B7VC0vjf.js.map → index-BzMsh6Ui.js.map} +1 -1
  226. package/dashboard/dist/assets/index-CN0efCE9.js +6 -0
  227. package/dashboard/dist/assets/index-CN0efCE9.js.map +1 -0
  228. package/dashboard/dist/assets/{index-B5qsp8lb.js → index-CgD9fA9P.js} +2 -2
  229. package/dashboard/dist/assets/{index-B5qsp8lb.js.map → index-CgD9fA9P.js.map} +1 -1
  230. package/dashboard/dist/assets/{index-CUfgt3mV.js → index-Cqjf-yVP.js} +2 -2
  231. package/dashboard/dist/assets/{index-CUfgt3mV.js.map → index-Cqjf-yVP.js.map} +1 -1
  232. package/dashboard/dist/assets/{index-DjJ6VCuc.js → index-DNugmBs7.js} +2 -2
  233. package/dashboard/dist/assets/{index-DjJ6VCuc.js.map → index-DNugmBs7.js.map} +1 -1
  234. package/dashboard/dist/assets/{index-BUmnJ4eT.js → index-DjxTV3cn.js} +2 -2
  235. package/dashboard/dist/assets/{index-BUmnJ4eT.js.map → index-DjxTV3cn.js.map} +1 -1
  236. package/dashboard/dist/assets/index-DpAS4-DA.js +2 -0
  237. package/dashboard/dist/assets/index-DpAS4-DA.js.map +1 -0
  238. package/dashboard/dist/assets/index-nO0cim3_.css +1 -0
  239. package/dashboard/dist/assets/{knowledge-BKg0cr39.js → knowledge-D4SvdNv5.js} +2 -2
  240. package/dashboard/dist/assets/{knowledge-BKg0cr39.js.map → knowledge-D4SvdNv5.js.map} +1 -1
  241. package/dashboard/dist/assets/{mcp-CVdHpKlz.js → mcp-DVxt4T9J.js} +2 -2
  242. package/dashboard/dist/assets/{mcp-CVdHpKlz.js.map → mcp-DVxt4T9J.js.map} +1 -1
  243. package/dashboard/dist/assets/{mcp-query-CYxZcbwQ.js → mcp-query-CLP9EnnU.js} +2 -2
  244. package/dashboard/dist/assets/{mcp-query-CYxZcbwQ.js.map → mcp-query-CLP9EnnU.js.map} +1 -1
  245. package/dashboard/dist/assets/{pipelines-BOEv7P6O.js → pipelines-C_x0LTBN.js} +2 -2
  246. package/dashboard/dist/assets/{pipelines-BOEv7P6O.js.map → pipelines-C_x0LTBN.js.map} +1 -1
  247. package/dashboard/dist/assets/{roles-c5cV2QYQ.js → roles-QaGFZQ58.js} +2 -2
  248. package/dashboard/dist/assets/{roles-c5cV2QYQ.js.map → roles-QaGFZQ58.js.map} +1 -1
  249. package/dashboard/dist/assets/{tasks-JV3BjeEF.js → tasks-1eTuP02d.js} +2 -2
  250. package/dashboard/dist/assets/{tasks-JV3BjeEF.js.map → tasks-1eTuP02d.js.map} +1 -1
  251. package/dashboard/dist/assets/{topics-BDaPnhnQ.js → topics-B3p8K0PW.js} +2 -2
  252. package/dashboard/dist/assets/{topics-BDaPnhnQ.js.map → topics-B3p8K0PW.js.map} +1 -1
  253. package/dashboard/dist/assets/{useEventHooks-a7Dnh8G4.js → useEventHooks-BNZky4xM.js} +2 -2
  254. package/dashboard/dist/assets/{useEventHooks-a7Dnh8G4.js.map → useEventHooks-BNZky4xM.js.map} +1 -1
  255. package/dashboard/dist/assets/{useNamespace-BSwEV0e9.js → useNamespace-Dx81_JM0.js} +2 -2
  256. package/dashboard/dist/assets/{useNamespace-BSwEV0e9.js.map → useNamespace-Dx81_JM0.js.map} +1 -1
  257. package/dashboard/dist/assets/{useYamlActivityEvents-BuqvL-cn.js → useYamlActivityEvents-D5T3TbE7.js} +2 -2
  258. package/dashboard/dist/assets/{useYamlActivityEvents-BuqvL-cn.js.map → useYamlActivityEvents-D5T3TbE7.js.map} +1 -1
  259. package/dashboard/dist/assets/users-BEMysNdR.js +2 -0
  260. package/dashboard/dist/assets/users-BEMysNdR.js.map +1 -0
  261. package/dashboard/dist/assets/{vendor-icons-D5ycKPA2.js → vendor-icons--Ivmp-RX.js} +132 -112
  262. package/dashboard/dist/assets/vendor-icons--Ivmp-RX.js.map +1 -0
  263. package/dashboard/dist/assets/{workflows-DF-9Voyi.js → workflows-C4rjvQbI.js} +2 -2
  264. package/dashboard/dist/assets/{workflows-DF-9Voyi.js.map → workflows-C4rjvQbI.js.map} +1 -1
  265. package/dashboard/dist/assets/{yaml-workflows-2roKaSSK.js → yaml-workflows-4rJ5v5rJ.js} +2 -2
  266. package/dashboard/dist/assets/{yaml-workflows-2roKaSSK.js.map → yaml-workflows-4rJ5v5rJ.js.map} +1 -1
  267. package/dashboard/dist/index.html +3 -3
  268. package/docs/api/http/escalations.md +131 -10
  269. package/docs/api/http/roles.md +44 -9
  270. package/docs/api/http/users.md +15 -6
  271. package/docs/api/mcp/admin.md +8 -2
  272. package/docs/api/mcp/human-queue.md +34 -2
  273. package/docs/api/sdk/escalations.md +155 -2
  274. package/docs/api/sdk/roles.md +11 -0
  275. package/docs/api/sdk/users.md +26 -1
  276. package/docs/auth.md +1 -1
  277. package/docs/dashboard.md +5 -4
  278. package/docs/data.md +9 -2
  279. package/docs/faceted-routing.md +221 -0
  280. package/docs/hitl-guide.md +50 -0
  281. package/docs/iam.md +31 -0
  282. package/docs/mcp.md +8 -0
  283. package/package.json +2 -2
  284. package/dashboard/dist/assets/AdminDashboard-LTiYLuzq.js +0 -2
  285. package/dashboard/dist/assets/AgentDetailPage-BSUnDvGp.js.map +0 -1
  286. package/dashboard/dist/assets/AgentsPage-DpS0cCYe.js +0 -2
  287. package/dashboard/dist/assets/AvailableEscalationsPage-BEylRqjP.js +0 -2
  288. package/dashboard/dist/assets/AvailableEscalationsPage-BEylRqjP.js.map +0 -1
  289. package/dashboard/dist/assets/CapabilitiesPage-DLIleX2L.js +0 -2
  290. package/dashboard/dist/assets/CapabilitiesPage-DLIleX2L.js.map +0 -1
  291. package/dashboard/dist/assets/CopyableId-DaT0ZRHg.js +0 -2
  292. package/dashboard/dist/assets/CopyableId-DaT0ZRHg.js.map +0 -1
  293. package/dashboard/dist/assets/DataTable-D9yuBv0w.js.map +0 -1
  294. package/dashboard/dist/assets/FilterBar-Ck4K4rzu.js +0 -2
  295. package/dashboard/dist/assets/FilterBar-Ck4K4rzu.js.map +0 -1
  296. package/dashboard/dist/assets/GraphInvokePage-BZBPBCSq.js +0 -2
  297. package/dashboard/dist/assets/GraphInvokePage-BZBPBCSq.js.map +0 -1
  298. package/dashboard/dist/assets/HomePage-dM7U2tl6.js +0 -2
  299. package/dashboard/dist/assets/HomePage-dM7U2tl6.js.map +0 -1
  300. package/dashboard/dist/assets/McpRunsPage-CPMYWgm_.js +0 -2
  301. package/dashboard/dist/assets/McpRunsPage-CPMYWgm_.js.map +0 -1
  302. package/dashboard/dist/assets/OperatorDashboard-g9o7I925.js +0 -2
  303. package/dashboard/dist/assets/PageHeader-Dk2Pe44Q.js +0 -2
  304. package/dashboard/dist/assets/PageHeader-Dk2Pe44Q.js.map +0 -1
  305. package/dashboard/dist/assets/PriorityBadge-DfQY9St9.js +0 -2
  306. package/dashboard/dist/assets/PriorityBadge-DfQY9St9.js.map +0 -1
  307. package/dashboard/dist/assets/RolePill-Du-aNnjR.js +0 -2
  308. package/dashboard/dist/assets/RolePill-Du-aNnjR.js.map +0 -1
  309. package/dashboard/dist/assets/RolesPage-B2Kdf-L9.js +0 -2
  310. package/dashboard/dist/assets/RolesPage-B2Kdf-L9.js.map +0 -1
  311. package/dashboard/dist/assets/RowActions-Dg-Fsm5O.js +0 -2
  312. package/dashboard/dist/assets/RowActions-Dg-Fsm5O.js.map +0 -1
  313. package/dashboard/dist/assets/StickyPagination-BWhFSr2d.js +0 -2
  314. package/dashboard/dist/assets/StickyPagination-BWhFSr2d.js.map +0 -1
  315. package/dashboard/dist/assets/TimeAgo-CUIYqSt_.js +0 -2
  316. package/dashboard/dist/assets/TopicDetailPage-rh_dsTJI.js.map +0 -1
  317. package/dashboard/dist/assets/TopicsPage-CD_40_v-.js +0 -2
  318. package/dashboard/dist/assets/TopicsPage-CD_40_v-.js.map +0 -1
  319. package/dashboard/dist/assets/WorkflowsDashboard-BgI-iOP6.js +0 -2
  320. package/dashboard/dist/assets/WorkflowsDashboard-BgI-iOP6.js.map +0 -1
  321. package/dashboard/dist/assets/escalation-columns-nVYVt7Gl.js +0 -2
  322. package/dashboard/dist/assets/escalation-columns-nVYVt7Gl.js.map +0 -1
  323. package/dashboard/dist/assets/index-CMviPjXQ.js +0 -9
  324. package/dashboard/dist/assets/index-CMviPjXQ.js.map +0 -1
  325. package/dashboard/dist/assets/index-CP0JEQhZ.js +0 -6
  326. package/dashboard/dist/assets/index-CP0JEQhZ.js.map +0 -1
  327. package/dashboard/dist/assets/index-ClvYSNZd.css +0 -1
  328. package/dashboard/dist/assets/index-D1xVY4JR.js.map +0 -1
  329. package/dashboard/dist/assets/index-DPLy-jwO.js +0 -2
  330. package/dashboard/dist/assets/index-DPLy-jwO.js.map +0 -1
  331. package/dashboard/dist/assets/index-DzVYjoex.js +0 -6
  332. package/dashboard/dist/assets/index-DzVYjoex.js.map +0 -1
  333. package/dashboard/dist/assets/index-FuhiJuHL.js +0 -2
  334. package/dashboard/dist/assets/index-FuhiJuHL.js.map +0 -1
  335. package/dashboard/dist/assets/users-Pn85K_3C.js +0 -2
  336. package/dashboard/dist/assets/users-Pn85K_3C.js.map +0 -1
  337. 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
+ }
@@ -3,3 +3,4 @@ export * from './crud';
3
3
  export * from './bulk';
4
4
  export * from './queries';
5
5
  export { ensureEscalationCompatView } from './client';
6
+ export { ensureFacetReady, searchByFacets, searchGroups, countByFacets, claimGroups, claimByFacets, } from './facets';
@@ -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
- // Free-text search → server-side SQL path (the SDK list() has no free-text).
84
- if (filters.search) {
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
- // Free-text search → server-side SQL path (available = pending + no active claim).
131
- if (filters.search) {
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–$9). */
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 = allowed roles (text[], null = no filter)
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 OR role = ANY($5))\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";
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
- * $1 status, $2 role, $3 roles (text[] visibleRoles), $4 type, $5 subtype,
51
- * $6 priority, $7 assigned_to, $8 available (bool), $9 search.
52
- * The list query adds $10 limit, $11 offset.
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 ($3::text[] IS NULL OR role = ANY($3))
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–$9). */
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 $10 OFFSET $11`;
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 = allowed roles (text[], null = no filter)
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 ($5::text[] IS NULL OR role = ANY($5))
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
  }