@hotmeshio/long-tail 0.6.0 → 0.7.0

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