@hotmeshio/long-tail 0.5.6 → 0.5.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/api/diagnostics.d.ts +15 -0
- package/build/api/diagnostics.js +33 -0
- package/build/api/escalations/cancel.d.ts +20 -0
- package/build/api/escalations/cancel.js +93 -0
- package/build/api/escalations/index.d.ts +1 -0
- package/build/api/escalations/index.js +4 -1
- package/build/api/escalations/resolve.js +2 -0
- package/build/api/workflows/invocation.js +6 -0
- package/build/lib/events/publish.d.ts +1 -1
- package/build/routes/diagnostics.d.ts +2 -0
- package/build/routes/diagnostics.js +88 -0
- package/build/routes/escalations/bulk.js +4 -0
- package/build/routes/escalations/single.js +9 -0
- package/build/routes/index.js +2 -0
- package/build/services/diagnostics/index.d.ts +52 -0
- package/build/services/diagnostics/index.js +169 -0
- package/build/services/diagnostics/patterns.d.ts +19 -0
- package/build/services/diagnostics/patterns.js +147 -0
- package/build/services/escalation/bulk.d.ts +9 -0
- package/build/services/escalation/bulk.js +21 -0
- package/build/services/escalation/crud.d.ts +17 -0
- package/build/services/escalation/crud.js +57 -0
- package/build/services/orchestrator/condition.d.ts +1 -1
- package/build/services/orchestrator/condition.js +3 -0
- package/build/services/yaml-workflow/deployer.js +5 -0
- package/build/services/yaml-workflow/pipe-validator.d.ts +44 -0
- package/build/services/yaml-workflow/pipe-validator.js +215 -0
- package/build/system/mcp-servers/admin/diagnostics.d.ts +10 -0
- package/build/system/mcp-servers/admin/diagnostics.js +94 -0
- package/build/system/mcp-servers/admin/index.js +4 -2
- package/build/system/mcp-servers/admin/schemas.d.ts +36 -0
- package/build/system/mcp-servers/admin/schemas.js +16 -1
- package/build/system/workflows/mcp-workflow-builder/index.js +15 -2
- package/build/tsconfig.tsbuildinfo +1 -1
- package/dashboard/dist/assets/AdminDashboard-0LSslIQ7.js +2 -0
- package/dashboard/dist/assets/{AdminDashboard-DIQEViVY.js.map → AdminDashboard-0LSslIQ7.js.map} +1 -1
- package/dashboard/dist/assets/{AgentConfigPage-Caf5-th-.js → AgentConfigPage-hqBr1540.js} +2 -2
- package/dashboard/dist/assets/{AgentConfigPage-Caf5-th-.js.map → AgentConfigPage-hqBr1540.js.map} +1 -1
- package/dashboard/dist/assets/{AgentDetailPage-_a4tKaM3.js → AgentDetailPage-bnXR1ezD.js} +2 -2
- package/dashboard/dist/assets/{AgentDetailPage-_a4tKaM3.js.map → AgentDetailPage-bnXR1ezD.js.map} +1 -1
- package/dashboard/dist/assets/{AgentsPage-BNlAkEIK.js → AgentsPage-tJIlrnfM.js} +2 -2
- package/dashboard/dist/assets/{AgentsPage-BNlAkEIK.js.map → AgentsPage-tJIlrnfM.js.map} +1 -1
- package/dashboard/dist/assets/AvailableEscalationsPage-B7gEfcw3.js +2 -0
- package/dashboard/dist/assets/AvailableEscalationsPage-B7gEfcw3.js.map +1 -0
- package/dashboard/dist/assets/{BotPicker-DBvd1yFL.js → BotPicker-fFE4GAaO.js} +2 -2
- package/dashboard/dist/assets/{BotPicker-DBvd1yFL.js.map → BotPicker-fFE4GAaO.js.map} +1 -1
- package/dashboard/dist/assets/{CapabilitiesPage-BIdG9RTG.js → CapabilitiesPage-DotZ7iQc.js} +2 -2
- package/dashboard/dist/assets/{CapabilitiesPage-BIdG9RTG.js.map → CapabilitiesPage-DotZ7iQc.js.map} +1 -1
- package/dashboard/dist/assets/{CollapsibleSection-CggyKhyZ.js → CollapsibleSection-D97SYypF.js} +2 -2
- package/dashboard/dist/assets/{CollapsibleSection-CggyKhyZ.js.map → CollapsibleSection-D97SYypF.js.map} +1 -1
- package/dashboard/dist/assets/{CredentialsPage-B4sXGLRY.js → CredentialsPage-C08U6dlv.js} +2 -2
- package/dashboard/dist/assets/{CredentialsPage-B4sXGLRY.js.map → CredentialsPage-C08U6dlv.js.map} +1 -1
- package/dashboard/dist/assets/{CronLabel-GcS5qzZr.js → CronLabel-C2TuCl4h.js} +2 -2
- package/dashboard/dist/assets/{CronLabel-GcS5qzZr.js.map → CronLabel-C2TuCl4h.js.map} +1 -1
- package/dashboard/dist/assets/CustomDurationPicker-C3zGsESx.js +2 -0
- package/dashboard/dist/assets/CustomDurationPicker-C3zGsESx.js.map +1 -0
- package/dashboard/dist/assets/{ElapsedCell-BFOMsgwF.js → ElapsedCell-DF4m0cv_.js} +2 -2
- package/dashboard/dist/assets/{ElapsedCell-BFOMsgwF.js.map → ElapsedCell-DF4m0cv_.js.map} +1 -1
- package/dashboard/dist/assets/{EscalationsOverview-BfjW6haL.js → EscalationsOverview-blGOYKtJ.js} +2 -2
- package/dashboard/dist/assets/{EscalationsOverview-BfjW6haL.js.map → EscalationsOverview-blGOYKtJ.js.map} +1 -1
- package/dashboard/dist/assets/{EventTable-N0XmLk3C.js → EventTable-DzrMH-ux.js} +2 -2
- package/dashboard/dist/assets/{EventTable-N0XmLk3C.js.map → EventTable-DzrMH-ux.js.map} +1 -1
- package/dashboard/dist/assets/{GraphInvokePage-DHiGHiCk.js → GraphInvokePage-QVAUHmjI.js} +2 -2
- package/dashboard/dist/assets/{GraphInvokePage-DHiGHiCk.js.map → GraphInvokePage-QVAUHmjI.js.map} +1 -1
- package/dashboard/dist/assets/{HomePage-jckQolbx.js → HomePage-DvYgvCcO.js} +2 -2
- package/dashboard/dist/assets/{HomePage-jckQolbx.js.map → HomePage-DvYgvCcO.js.map} +1 -1
- package/dashboard/dist/assets/{ListToolbar-B71DqaXr.js → ListToolbar-D6tQ3ZG2.js} +2 -2
- package/dashboard/dist/assets/{ListToolbar-B71DqaXr.js.map → ListToolbar-D6tQ3ZG2.js.map} +1 -1
- package/dashboard/dist/assets/{McpOverview-CPD6NRGt.js → McpOverview-BClbGYb8.js} +2 -2
- package/dashboard/dist/assets/{McpOverview-CPD6NRGt.js.map → McpOverview-BClbGYb8.js.map} +1 -1
- package/dashboard/dist/assets/{McpQueryDetailPage-YyqXw6Dt.js → McpQueryDetailPage-DHjfDRdm.js} +2 -2
- package/dashboard/dist/assets/{McpQueryDetailPage-YyqXw6Dt.js.map → McpQueryDetailPage-DHjfDRdm.js.map} +1 -1
- package/dashboard/dist/assets/{McpQueryPage-BWf45EYD.js → McpQueryPage-qtEuMDrd.js} +2 -2
- package/dashboard/dist/assets/{McpQueryPage-BWf45EYD.js.map → McpQueryPage-qtEuMDrd.js.map} +1 -1
- package/dashboard/dist/assets/{McpRunDetailPage-Cv43tkjz.js → McpRunDetailPage-DRbIZMU2.js} +2 -2
- package/dashboard/dist/assets/{McpRunDetailPage-Cv43tkjz.js.map → McpRunDetailPage-DRbIZMU2.js.map} +1 -1
- package/dashboard/dist/assets/{McpRunsPage-BFards4K.js → McpRunsPage-CMfwXQ4R.js} +2 -2
- package/dashboard/dist/assets/{McpRunsPage-BFards4K.js.map → McpRunsPage-CMfwXQ4R.js.map} +1 -1
- package/dashboard/dist/assets/{OperatorDashboard-BJZxuOKI.js → OperatorDashboard-Dq2pfV68.js} +2 -2
- package/dashboard/dist/assets/{OperatorDashboard-BJZxuOKI.js.map → OperatorDashboard-Dq2pfV68.js.map} +1 -1
- package/dashboard/dist/assets/{ProcessDetailPage-BjkLfyrm.js → ProcessDetailPage-DjQuNERz.js} +2 -2
- package/dashboard/dist/assets/{ProcessDetailPage-BjkLfyrm.js.map → ProcessDetailPage-DjQuNERz.js.map} +1 -1
- package/dashboard/dist/assets/{ProcessesListPage-DOzeduP3.js → ProcessesListPage-vJimii8-.js} +2 -2
- package/dashboard/dist/assets/{ProcessesListPage-DOzeduP3.js.map → ProcessesListPage-vJimii8-.js.map} +1 -1
- package/dashboard/dist/assets/{RolesPage-DSSQrGTh.js → RolesPage-CFCN_mRF.js} +2 -2
- package/dashboard/dist/assets/{RolesPage-DSSQrGTh.js.map → RolesPage-CFCN_mRF.js.map} +1 -1
- package/dashboard/dist/assets/{RunAsSelector-CM7lk1YB.js → RunAsSelector-sBxAvlRf.js} +2 -2
- package/dashboard/dist/assets/{RunAsSelector-CM7lk1YB.js.map → RunAsSelector-sBxAvlRf.js.map} +1 -1
- package/dashboard/dist/assets/{StatusBadge-XQlNFwmH.js → StatusBadge-Bl3FXVmF.js} +2 -2
- package/dashboard/dist/assets/{StatusBadge-XQlNFwmH.js.map → StatusBadge-Bl3FXVmF.js.map} +1 -1
- package/dashboard/dist/assets/{SwimlaneTimeline-DtCgJNpV.js → SwimlaneTimeline-2Eet4OOL.js} +2 -2
- package/dashboard/dist/assets/{SwimlaneTimeline-DtCgJNpV.js.map → SwimlaneTimeline-2Eet4OOL.js.map} +1 -1
- package/dashboard/dist/assets/{TaskDetailPage-BC9xnyLT.js → TaskDetailPage-CgFN5uWn.js} +2 -2
- package/dashboard/dist/assets/{TaskDetailPage-BC9xnyLT.js.map → TaskDetailPage-CgFN5uWn.js.map} +1 -1
- package/dashboard/dist/assets/{TasksListPage-DtbLfUS-.js → TasksListPage-qj25rRZL.js} +2 -2
- package/dashboard/dist/assets/{TasksListPage-DtbLfUS-.js.map → TasksListPage-qj25rRZL.js.map} +1 -1
- package/dashboard/dist/assets/{TimeAgo-xrDlFnPy.js → TimeAgo-C7LylVjP.js} +2 -2
- package/dashboard/dist/assets/{TimeAgo-xrDlFnPy.js.map → TimeAgo-C7LylVjP.js.map} +1 -1
- package/dashboard/dist/assets/{TimestampCell-BneATvjR.js → TimestampCell-CrCtoBlo.js} +2 -2
- package/dashboard/dist/assets/{TimestampCell-BneATvjR.js.map → TimestampCell-CrCtoBlo.js.map} +1 -1
- package/dashboard/dist/assets/{ToolTestPanel-1lQ_GQod.js → ToolTestPanel-DYGlB-QW.js} +2 -2
- package/dashboard/dist/assets/{ToolTestPanel-1lQ_GQod.js.map → ToolTestPanel-DYGlB-QW.js.map} +1 -1
- package/dashboard/dist/assets/{TopicDetailPage-CDtlXyut.js → TopicDetailPage-DLY6X_R6.js} +2 -2
- package/dashboard/dist/assets/{TopicDetailPage-CDtlXyut.js.map → TopicDetailPage-DLY6X_R6.js.map} +1 -1
- package/dashboard/dist/assets/{TopicsPage-COX2kDKK.js → TopicsPage-C5nOLsLx.js} +2 -2
- package/dashboard/dist/assets/{TopicsPage-COX2kDKK.js.map → TopicsPage-C5nOLsLx.js.map} +1 -1
- package/dashboard/dist/assets/{UserName-Cb2WvhCN.js → UserName-CgUZyiwm.js} +2 -2
- package/dashboard/dist/assets/{UserName-Cb2WvhCN.js.map → UserName-CgUZyiwm.js.map} +1 -1
- package/dashboard/dist/assets/{WorkflowExecutionPage-BWCTOTXF.js → WorkflowExecutionPage-CDr5CTM2.js} +2 -2
- package/dashboard/dist/assets/{WorkflowExecutionPage-BWCTOTXF.js.map → WorkflowExecutionPage-CDr5CTM2.js.map} +1 -1
- package/dashboard/dist/assets/{WorkflowsDashboard-DOaDSC98.js → WorkflowsDashboard-tCBP0i0Y.js} +2 -2
- package/dashboard/dist/assets/{WorkflowsDashboard-DOaDSC98.js.map → WorkflowsDashboard-tCBP0i0Y.js.map} +1 -1
- package/dashboard/dist/assets/{WorkflowsOverview-CeE7a51J.js → WorkflowsOverview-CaMeZwde.js} +2 -2
- package/dashboard/dist/assets/{WorkflowsOverview-CeE7a51J.js.map → WorkflowsOverview-CaMeZwde.js.map} +1 -1
- package/dashboard/dist/assets/{YamlWorkflowDetailPage-cNpR0PHg.js → YamlWorkflowDetailPage-C3uyCiIX.js} +2 -2
- package/dashboard/dist/assets/{YamlWorkflowDetailPage-cNpR0PHg.js.map → YamlWorkflowDetailPage-C3uyCiIX.js.map} +1 -1
- package/dashboard/dist/assets/{YamlWorkflowsPage-DWvNfzji.js → YamlWorkflowsPage-DRnYgsvA.js} +2 -2
- package/dashboard/dist/assets/{YamlWorkflowsPage-DWvNfzji.js.map → YamlWorkflowsPage-DRnYgsvA.js.map} +1 -1
- package/dashboard/dist/assets/{agents-BvTiBKBQ.js → agents-HMkTc425.js} +2 -2
- package/dashboard/dist/assets/{agents-BvTiBKBQ.js.map → agents-HMkTc425.js.map} +1 -1
- package/dashboard/dist/assets/{bots-CeMR1Lnp.js → bots-CDe2jXNx.js} +2 -2
- package/dashboard/dist/assets/{bots-CeMR1Lnp.js.map → bots-CDe2jXNx.js.map} +1 -1
- package/dashboard/dist/assets/{capabilities-D7uJlnxj.js → capabilities-DhnM7Ztb.js} +2 -2
- package/dashboard/dist/assets/{capabilities-D7uJlnxj.js.map → capabilities-DhnM7Ztb.js.map} +1 -1
- package/dashboard/dist/assets/{controlplane-Cf6lZsSF.js → controlplane-BwIJ9ZC_.js} +2 -2
- package/dashboard/dist/assets/{controlplane-Cf6lZsSF.js.map → controlplane-BwIJ9ZC_.js.map} +1 -1
- package/dashboard/dist/assets/{escalation-BCFBWGP-.js → escalation-Djk5XKmX.js} +2 -2
- package/dashboard/dist/assets/{escalation-BCFBWGP-.js.map → escalation-Djk5XKmX.js.map} +1 -1
- package/dashboard/dist/assets/{escalation-columns-Do7k4fte.js → escalation-columns-BKqSywVA.js} +2 -2
- package/dashboard/dist/assets/{escalation-columns-Do7k4fte.js.map → escalation-columns-BKqSywVA.js.map} +1 -1
- package/dashboard/dist/assets/{index-eWNx4UQ8.js → index-BFQ2t-p1.js} +22 -22
- package/dashboard/dist/assets/index-BFQ2t-p1.js.map +1 -0
- package/dashboard/dist/assets/{index-D3SjB3UM.js → index-BNkpns_E.js} +2 -2
- package/dashboard/dist/assets/{index-D3SjB3UM.js.map → index-BNkpns_E.js.map} +1 -1
- package/dashboard/dist/assets/{index-BS5pcmBR.js → index-BW-fIquS.js} +2 -2
- package/dashboard/dist/assets/{index-BS5pcmBR.js.map → index-BW-fIquS.js.map} +1 -1
- package/dashboard/dist/assets/{index-C51Jzvoz.js → index-BYpWOUEe.js} +2 -2
- package/dashboard/dist/assets/{index-C51Jzvoz.js.map → index-BYpWOUEe.js.map} +1 -1
- package/dashboard/dist/assets/{index-BDhEpETz.js → index-CPIYW81T.js} +2 -2
- package/dashboard/dist/assets/{index-BDhEpETz.js.map → index-CPIYW81T.js.map} +1 -1
- package/dashboard/dist/assets/{index-B216fPN_.js → index-Cydgj_Co.js} +2 -2
- package/dashboard/dist/assets/{index-B216fPN_.js.map → index-Cydgj_Co.js.map} +1 -1
- package/dashboard/dist/assets/{index-C1p_rvkY.js → index-DBi9Q9Hw.js} +2 -2
- package/dashboard/dist/assets/{index-C1p_rvkY.js.map → index-DBi9Q9Hw.js.map} +1 -1
- package/dashboard/dist/assets/{index-8C9YLM_u.js → index-DRIJM1L-.js} +2 -2
- package/dashboard/dist/assets/{index-8C9YLM_u.js.map → index-DRIJM1L-.js.map} +1 -1
- package/dashboard/dist/assets/index-DSki_Ub6.css +1 -0
- package/dashboard/dist/assets/{index-gKm3HAgO.js → index-Dhy2PVAw.js} +2 -2
- package/dashboard/dist/assets/{index-gKm3HAgO.js.map → index-Dhy2PVAw.js.map} +1 -1
- package/dashboard/dist/assets/index-E2hQ8DIi.js +6 -0
- package/dashboard/dist/assets/index-E2hQ8DIi.js.map +1 -0
- package/dashboard/dist/assets/{index-D0fjUKvx.js → index-mEQ6kCnt.js} +2 -2
- package/dashboard/dist/assets/{index-D0fjUKvx.js.map → index-mEQ6kCnt.js.map} +1 -1
- package/dashboard/dist/assets/{index-oulO44Iy.js → index-rUST2YsO.js} +2 -2
- package/dashboard/dist/assets/{index-oulO44Iy.js.map → index-rUST2YsO.js.map} +1 -1
- package/dashboard/dist/assets/{index-DSjbScPX.js → index-u08vQUaw.js} +2 -2
- package/dashboard/dist/assets/{index-DSjbScPX.js.map → index-u08vQUaw.js.map} +1 -1
- package/dashboard/dist/assets/{knowledge-Bje8_ndK.js → knowledge-jQE3dBVi.js} +2 -2
- package/dashboard/dist/assets/{knowledge-Bje8_ndK.js.map → knowledge-jQE3dBVi.js.map} +1 -1
- package/dashboard/dist/assets/{mcp-Bpl6p11J.js → mcp-DtEGfZiz.js} +2 -2
- package/dashboard/dist/assets/{mcp-Bpl6p11J.js.map → mcp-DtEGfZiz.js.map} +1 -1
- package/dashboard/dist/assets/{mcp-query-DMXj-t5f.js → mcp-query-CiTLVuDC.js} +2 -2
- package/dashboard/dist/assets/{mcp-query-DMXj-t5f.js.map → mcp-query-CiTLVuDC.js.map} +1 -1
- package/dashboard/dist/assets/{pipelines-CFh9nBxn.js → pipelines-ClNdzuUK.js} +2 -2
- package/dashboard/dist/assets/{pipelines-CFh9nBxn.js.map → pipelines-ClNdzuUK.js.map} +1 -1
- package/dashboard/dist/assets/{roles-CtjQQWSQ.js → roles-BmHbRaPT.js} +2 -2
- package/dashboard/dist/assets/{roles-CtjQQWSQ.js.map → roles-BmHbRaPT.js.map} +1 -1
- package/dashboard/dist/assets/{tasks-Nz9lVCT1.js → tasks-BPONxLcq.js} +2 -2
- package/dashboard/dist/assets/{tasks-Nz9lVCT1.js.map → tasks-BPONxLcq.js.map} +1 -1
- package/dashboard/dist/assets/{topics-CiG2N8Z_.js → topics-XREPRxwo.js} +2 -2
- package/dashboard/dist/assets/{topics-CiG2N8Z_.js.map → topics-XREPRxwo.js.map} +1 -1
- package/dashboard/dist/assets/{useEventHooks-Bimqt7hU.js → useEventHooks-M7SaEfel.js} +2 -2
- package/dashboard/dist/assets/{useEventHooks-Bimqt7hU.js.map → useEventHooks-M7SaEfel.js.map} +1 -1
- package/dashboard/dist/assets/{useNamespace-FrG0Monz.js → useNamespace-BQHGkdBv.js} +2 -2
- package/dashboard/dist/assets/{useNamespace-FrG0Monz.js.map → useNamespace-BQHGkdBv.js.map} +1 -1
- package/dashboard/dist/assets/{useYamlActivityEvents-JmD4dH5n.js → useYamlActivityEvents-B7JtUGSE.js} +2 -2
- package/dashboard/dist/assets/{useYamlActivityEvents-JmD4dH5n.js.map → useYamlActivityEvents-B7JtUGSE.js.map} +1 -1
- package/dashboard/dist/assets/{users-DgyonYq6.js → users-BpjSNhQe.js} +2 -2
- package/dashboard/dist/assets/{users-DgyonYq6.js.map → users-BpjSNhQe.js.map} +1 -1
- package/dashboard/dist/assets/{workflows-BOwGINtF.js → workflows-k4jE0ECC.js} +2 -2
- package/dashboard/dist/assets/{workflows-BOwGINtF.js.map → workflows-k4jE0ECC.js.map} +1 -1
- package/dashboard/dist/assets/{yaml-workflows-I1tcjCFK.js → yaml-workflows-DpviSLEL.js} +2 -2
- package/dashboard/dist/assets/{yaml-workflows-I1tcjCFK.js.map → yaml-workflows-DpviSLEL.js.map} +1 -1
- package/dashboard/dist/index.html +2 -2
- package/docs/api/http/escalations.md +89 -5
- package/docs/api/sdk/escalations.md +60 -1
- package/docs/hitl-guide.md +65 -1
- package/package.json +3 -2
- package/dashboard/dist/assets/AdminDashboard-DIQEViVY.js +0 -2
- package/dashboard/dist/assets/AvailableEscalationsPage-Cqy5ECm4.js +0 -2
- package/dashboard/dist/assets/AvailableEscalationsPage-Cqy5ECm4.js.map +0 -1
- package/dashboard/dist/assets/CustomDurationPicker-CUYE36-K.js +0 -2
- package/dashboard/dist/assets/CustomDurationPicker-CUYE36-K.js.map +0 -1
- package/dashboard/dist/assets/index-BCfzac3h.css +0 -1
- package/dashboard/dist/assets/index-BUu3WKvk.js +0 -6
- package/dashboard/dist/assets/index-BUu3WKvk.js.map +0 -1
- package/dashboard/dist/assets/index-eWNx4UQ8.js.map +0 -1
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.matchPatterns = matchPatterns;
|
|
4
|
+
/**
|
|
5
|
+
* Match event stream + stream messages against known failure patterns.
|
|
6
|
+
* Returns findings ordered by severity (critical first).
|
|
7
|
+
*/
|
|
8
|
+
function matchPatterns(execution, workerMessages, engineMessages, escalationRow) {
|
|
9
|
+
const findings = [];
|
|
10
|
+
const events = execution.events ?? [];
|
|
11
|
+
const now = Date.now();
|
|
12
|
+
// ── Derive open signals ──────────────────────────────────────────────────
|
|
13
|
+
const signalCompletions = new Set(events
|
|
14
|
+
.filter(e => e.event_type === 'workflow_execution_signaled')
|
|
15
|
+
.map(e => e.attributes.timeline_key)
|
|
16
|
+
.filter(Boolean));
|
|
17
|
+
const openSignals = events.filter(e => e.event_type === 'signal_wait_started' &&
|
|
18
|
+
!signalCompletions.has(e.attributes.timeline_key));
|
|
19
|
+
// ── Dead-lettered messages ────────────────────────────────────────────────
|
|
20
|
+
const deadWorker = workerMessages.filter(m => m.dead_lettered_at);
|
|
21
|
+
const deadEngine = engineMessages.filter(m => m.dead_lettered_at);
|
|
22
|
+
// ── Reservation leaks: claimed but not ACK'd for > 30s ───────────────────
|
|
23
|
+
const leaks = workerMessages.filter(m => m.reserved_at && !m.expired_at && !m.dead_lettered_at &&
|
|
24
|
+
now - new Date(m.reserved_at).getTime() > 30_000);
|
|
25
|
+
// ── Pattern: orphaned_signal ──────────────────────────────────────────────
|
|
26
|
+
if (openSignals.length > 0 && !escalationRow) {
|
|
27
|
+
const workerMsg = workerMessages.find(m => m.expired_at && m.aid?.endsWith('/worker'));
|
|
28
|
+
const payload = safeParseJson(workerMsg?.message);
|
|
29
|
+
const queueConfig = payload?.data?.queueConfig;
|
|
30
|
+
const signalId = payload?.data?.signalId;
|
|
31
|
+
findings.push({
|
|
32
|
+
condition: 'orphaned_signal',
|
|
33
|
+
confidence: 0.98,
|
|
34
|
+
severity: 'critical',
|
|
35
|
+
evidence: [
|
|
36
|
+
`${openSignals.length} open signal(s) with no matching resolution`,
|
|
37
|
+
`No escalation row in hmsh_escalations for this workflow`,
|
|
38
|
+
queueConfig == null
|
|
39
|
+
? `Worker result missing queueConfig — escalation INSERT was skipped (pre-0.22 SDK)`
|
|
40
|
+
: `Worker result has queueConfig but escalation INSERT did not complete`,
|
|
41
|
+
...(signalId ? [`Signal key: ${signalId}`] : []),
|
|
42
|
+
],
|
|
43
|
+
treatment: [
|
|
44
|
+
{ action: 'create_escalation_row', note: 'INSERT missing row into public.hmsh_escalations' },
|
|
45
|
+
...(signalId ? [{ action: 'resolve_by_signal_key', signal_key: signalId, note: 'POST /api/escalations/resolve-by-signal-key after inserting escalation row' }] : []),
|
|
46
|
+
],
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
// ── Pattern: normal_wait ──────────────────────────────────────────────────
|
|
50
|
+
// Two paths reach here:
|
|
51
|
+
// 1. Open signal in event stream (signal_wait_started) + pending escalation
|
|
52
|
+
// 2. No terminal event + pending escalation (conditionLT path — no signal_wait_started in export)
|
|
53
|
+
const isRunning = !events.some(e => e.event_type === 'workflow_execution_completed' || e.event_type === 'workflow_execution_failed');
|
|
54
|
+
const isNormalWait = (openSignals.length > 0 && escalationRow?.status === 'pending') ||
|
|
55
|
+
(isRunning && events.length > 0 && escalationRow?.status === 'pending');
|
|
56
|
+
if (isNormalWait) {
|
|
57
|
+
findings.push({
|
|
58
|
+
condition: 'normal_wait',
|
|
59
|
+
confidence: 0.99,
|
|
60
|
+
severity: 'info',
|
|
61
|
+
evidence: [
|
|
62
|
+
`Workflow suspended — escalation exists (status: pending)`,
|
|
63
|
+
...(escalationRow.role ? [`Role: ${escalationRow.role}, Type: ${escalationRow.type}`] : []),
|
|
64
|
+
...(escalationRow.id ? [`Escalation id: ${escalationRow.id}`] : []),
|
|
65
|
+
],
|
|
66
|
+
treatment: [{ action: 'none', note: 'Waiting for human to claim and resolve escalation' }],
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
// ── Pattern: dead_lettered_activity ──────────────────────────────────────
|
|
70
|
+
if (deadWorker.length > 0 || deadEngine.length > 0) {
|
|
71
|
+
const samples = [...deadWorker, ...deadEngine].slice(0, 3);
|
|
72
|
+
findings.push({
|
|
73
|
+
condition: 'dead_lettered_activity',
|
|
74
|
+
confidence: 0.99,
|
|
75
|
+
severity: 'critical',
|
|
76
|
+
evidence: [
|
|
77
|
+
`${deadWorker.length} worker message(s) dead-lettered, ${deadEngine.length} engine message(s) dead-lettered`,
|
|
78
|
+
...samples.map(m => `aid: ${m.aid ?? 'engine'}, retries: ${m.retry_attempt}/${m.max_retry_attempts}, stream: ${m.stream_name}`),
|
|
79
|
+
],
|
|
80
|
+
treatment: [{
|
|
81
|
+
action: 'investigate_dead_letter',
|
|
82
|
+
count: deadWorker.length + deadEngine.length,
|
|
83
|
+
note: 'Message exhausted retries — check worker health and DB connectivity',
|
|
84
|
+
}],
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
// ── Pattern: reservation_leak ─────────────────────────────────────────────
|
|
88
|
+
if (leaks.length > 0) {
|
|
89
|
+
findings.push({
|
|
90
|
+
condition: 'reservation_leak',
|
|
91
|
+
confidence: 0.85,
|
|
92
|
+
severity: 'warning',
|
|
93
|
+
evidence: leaks.map(m => `aid: ${m.aid}, claimed by: ${m.reserved_by ?? 'unknown'}, open for ${Math.round((now - new Date(m.reserved_at).getTime()) / 1000)}s`),
|
|
94
|
+
treatment: [{ action: 'check_worker_health', note: 'Worker claimed message but never ACKd — check for crashed workers' }],
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
// ── Pattern: terminal_failure ─────────────────────────────────────────────
|
|
98
|
+
const lastEvent = events.at(-1);
|
|
99
|
+
if (lastEvent?.event_type === 'workflow_execution_failed') {
|
|
100
|
+
const attrs = lastEvent.attributes;
|
|
101
|
+
findings.push({
|
|
102
|
+
condition: 'terminal_failure',
|
|
103
|
+
confidence: 0.99,
|
|
104
|
+
severity: 'critical',
|
|
105
|
+
evidence: [
|
|
106
|
+
`Workflow terminated with failure`,
|
|
107
|
+
...(attrs?.error ? [`Error: ${attrs.error}`] : []),
|
|
108
|
+
],
|
|
109
|
+
treatment: [{ action: 'none', note: 'Workflow terminated — review error and restart if needed' }],
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
// ── Pattern: never_started ────────────────────────────────────────────────
|
|
113
|
+
const nonStartEvents = events.filter(e => e.event_type !== 'workflow_execution_started');
|
|
114
|
+
if (events.length > 0 && nonStartEvents.length === 0) {
|
|
115
|
+
findings.push({
|
|
116
|
+
condition: 'never_started',
|
|
117
|
+
confidence: 0.90,
|
|
118
|
+
severity: 'critical',
|
|
119
|
+
evidence: [`Workflow created but no activities have run — engine may not be consuming this stream`],
|
|
120
|
+
treatment: [{ action: 'check_engine_health', note: 'Check engine health and stream backlog for this workflow type' }],
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
// ── Default: healthy ──────────────────────────────────────────────────────
|
|
124
|
+
if (findings.length === 0) {
|
|
125
|
+
const isCompleted = events.some(e => e.event_type === 'workflow_execution_completed' || e.event_type === 'workflow_execution_failed');
|
|
126
|
+
findings.push({
|
|
127
|
+
condition: isCompleted ? 'completed' : 'running',
|
|
128
|
+
confidence: 0.95,
|
|
129
|
+
severity: 'info',
|
|
130
|
+
evidence: [isCompleted ? 'Workflow completed normally' : 'Workflow is running — no anomalies detected'],
|
|
131
|
+
treatment: [{ action: 'none', note: 'No intervention required' }],
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
// Critical first
|
|
135
|
+
const order = { critical: 0, warning: 1, info: 2 };
|
|
136
|
+
return findings.sort((a, b) => order[a.severity] - order[b.severity]);
|
|
137
|
+
}
|
|
138
|
+
function safeParseJson(raw) {
|
|
139
|
+
if (!raw)
|
|
140
|
+
return null;
|
|
141
|
+
try {
|
|
142
|
+
return JSON.parse(raw);
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
@@ -20,6 +20,15 @@ export declare function bulkAssignEscalations(ids: string[], targetUserId: strin
|
|
|
20
20
|
* Clears assignment on all affected rows.
|
|
21
21
|
*/
|
|
22
22
|
export declare function bulkEscalateToRole(ids: string[], targetRole: string): Promise<number>;
|
|
23
|
+
/**
|
|
24
|
+
* Bulk cancel escalations. Each row is cancelled individually; rows already in
|
|
25
|
+
* a terminal state are silently skipped by the SDK. Returns the count
|
|
26
|
+
* successfully cancelled.
|
|
27
|
+
*/
|
|
28
|
+
export declare function bulkCancelEscalations(ids: string[]): Promise<{
|
|
29
|
+
cancelled: number;
|
|
30
|
+
skipped: number;
|
|
31
|
+
}>;
|
|
23
32
|
/**
|
|
24
33
|
* Bulk resolve escalations for AI triage.
|
|
25
34
|
* Returns full records so the caller can start triage workflows. No signal is
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.bulkClaimEscalations = bulkClaimEscalations;
|
|
4
4
|
exports.bulkAssignEscalations = bulkAssignEscalations;
|
|
5
5
|
exports.bulkEscalateToRole = bulkEscalateToRole;
|
|
6
|
+
exports.bulkCancelEscalations = bulkCancelEscalations;
|
|
6
7
|
exports.bulkResolveForTriage = bulkResolveForTriage;
|
|
7
8
|
const client_1 = require("./client");
|
|
8
9
|
const map_1 = require("./map");
|
|
@@ -41,6 +42,26 @@ async function bulkEscalateToRole(ids, targetRole) {
|
|
|
41
42
|
const client = await (0, client_1.escalations)();
|
|
42
43
|
return client.escalateManyToRole({ ids, targetRole });
|
|
43
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* Bulk cancel escalations. Each row is cancelled individually; rows already in
|
|
47
|
+
* a terminal state are silently skipped by the SDK. Returns the count
|
|
48
|
+
* successfully cancelled.
|
|
49
|
+
*/
|
|
50
|
+
async function bulkCancelEscalations(ids) {
|
|
51
|
+
if (ids.length === 0)
|
|
52
|
+
return { cancelled: 0, skipped: 0 };
|
|
53
|
+
const client = await (0, client_1.escalations)();
|
|
54
|
+
let cancelled = 0;
|
|
55
|
+
let skipped = 0;
|
|
56
|
+
await Promise.all(ids.map(async (id) => {
|
|
57
|
+
const result = await client.cancel(id);
|
|
58
|
+
if (result.ok)
|
|
59
|
+
cancelled++;
|
|
60
|
+
else
|
|
61
|
+
skipped++;
|
|
62
|
+
}));
|
|
63
|
+
return { cancelled, skipped };
|
|
64
|
+
}
|
|
44
65
|
/**
|
|
45
66
|
* Bulk resolve escalations for AI triage.
|
|
46
67
|
* Returns full records so the caller can start triage workflows. No signal is
|
|
@@ -32,6 +32,13 @@ export declare function getEscalationBySignalKey(signalKey: string): Promise<LTE
|
|
|
32
32
|
* pre-check (that would be a TOCTOU window) — the atomic resolve is the arbiter.
|
|
33
33
|
*/
|
|
34
34
|
export declare function resolveEscalationBySignalKey(signalKey: string, resolverPayload: Record<string, any>): Promise<LTEscalationRecord | null>;
|
|
35
|
+
/**
|
|
36
|
+
* Mark an escalation cancelled — used when the tied workflow has terminated and
|
|
37
|
+
* can never receive the resolution signal. The escalation is removed from the
|
|
38
|
+
* active queue but preserved for audit. Returns null when the row is missing or
|
|
39
|
+
* already terminal.
|
|
40
|
+
*/
|
|
41
|
+
export declare function cancelEscalation(id: string): Promise<LTEscalationRecord | null>;
|
|
35
42
|
/**
|
|
36
43
|
* Bulk update priority for a set of escalations. Only pending escalations are
|
|
37
44
|
* updated.
|
|
@@ -62,6 +69,16 @@ export declare function escalateToRole(id: string, targetRole: string): Promise<
|
|
|
62
69
|
export declare function getEscalation(id: string): Promise<LTEscalationRecord | null>;
|
|
63
70
|
export declare function getEscalationsByTaskId(taskId: string): Promise<LTEscalationRecord[]>;
|
|
64
71
|
export declare function getEscalationsByWorkflowId(workflowId: string): Promise<LTEscalationRecord[]>;
|
|
72
|
+
/**
|
|
73
|
+
* Cancel all pending escalations tied to a workflow in one atomic UPDATE.
|
|
74
|
+
* Called after workflow termination so escalations don't remain in the queue.
|
|
75
|
+
*
|
|
76
|
+
* HotMesh's interrupt() is supposed to do this atomically, but it filters by
|
|
77
|
+
* `app_id = <engine-appId>`. Long-tail's Durable client uses app_id='durable'
|
|
78
|
+
* while escalations are stored with app_id='hmsh' — the WHERE never matches.
|
|
79
|
+
* This single UPDATE bypasses the SDK entirely and closes that gap.
|
|
80
|
+
*/
|
|
81
|
+
export declare function cancelEscalationsByWorkflowId(workflowId: string): Promise<number>;
|
|
65
82
|
export declare function updateEscalationMetadata(id: string, patch: Record<string, any>): Promise<LTEscalationRecord | null>;
|
|
66
83
|
export declare function enrichEscalationRouting(id: string, metadataPatch: Record<string, any>, workflowFields: {
|
|
67
84
|
workflowType?: string;
|
|
@@ -5,6 +5,7 @@ exports.claimEscalation = claimEscalation;
|
|
|
5
5
|
exports.resolveEscalation = resolveEscalation;
|
|
6
6
|
exports.getEscalationBySignalKey = getEscalationBySignalKey;
|
|
7
7
|
exports.resolveEscalationBySignalKey = resolveEscalationBySignalKey;
|
|
8
|
+
exports.cancelEscalation = cancelEscalation;
|
|
8
9
|
exports.updateEscalationsPriority = updateEscalationsPriority;
|
|
9
10
|
exports.getEscalationRoles = getEscalationRoles;
|
|
10
11
|
exports.releaseEscalation = releaseEscalation;
|
|
@@ -13,6 +14,7 @@ exports.escalateToRole = escalateToRole;
|
|
|
13
14
|
exports.getEscalation = getEscalation;
|
|
14
15
|
exports.getEscalationsByTaskId = getEscalationsByTaskId;
|
|
15
16
|
exports.getEscalationsByWorkflowId = getEscalationsByWorkflowId;
|
|
17
|
+
exports.cancelEscalationsByWorkflowId = cancelEscalationsByWorkflowId;
|
|
16
18
|
exports.updateEscalationMetadata = updateEscalationMetadata;
|
|
17
19
|
exports.enrichEscalationRouting = enrichEscalationRouting;
|
|
18
20
|
exports.getEscalationsByOriginId = getEscalationsByOriginId;
|
|
@@ -138,6 +140,30 @@ async function resolveEscalationBySignalKey(signalKey, resolverPayload) {
|
|
|
138
140
|
return null;
|
|
139
141
|
return resolveEscalation(escalation.id, resolverPayload);
|
|
140
142
|
}
|
|
143
|
+
/**
|
|
144
|
+
* Mark an escalation cancelled — used when the tied workflow has terminated and
|
|
145
|
+
* can never receive the resolution signal. The escalation is removed from the
|
|
146
|
+
* active queue but preserved for audit. Returns null when the row is missing or
|
|
147
|
+
* already terminal.
|
|
148
|
+
*/
|
|
149
|
+
async function cancelEscalation(id) {
|
|
150
|
+
const client = await (0, client_1.escalations)();
|
|
151
|
+
const result = await client.cancel(id);
|
|
152
|
+
if (!result.ok)
|
|
153
|
+
return null;
|
|
154
|
+
const cancelled = (0, map_1.toEscalationRecord)(result.entry);
|
|
155
|
+
(0, publish_1.publishEscalationEvent)({
|
|
156
|
+
type: 'escalation.cancelled',
|
|
157
|
+
source: 'service',
|
|
158
|
+
workflowId: cancelled.workflow_id || '',
|
|
159
|
+
workflowName: cancelled.workflow_type || '',
|
|
160
|
+
taskQueue: cancelled.task_queue || '',
|
|
161
|
+
escalationId: cancelled.id,
|
|
162
|
+
status: 'cancelled',
|
|
163
|
+
data: { reason: 'workflow_terminated' },
|
|
164
|
+
});
|
|
165
|
+
return cancelled;
|
|
166
|
+
}
|
|
141
167
|
/**
|
|
142
168
|
* Bulk update priority for a set of escalations. Only pending escalations are
|
|
143
169
|
* updated.
|
|
@@ -227,6 +253,37 @@ async function getEscalationsByWorkflowId(workflowId) {
|
|
|
227
253
|
});
|
|
228
254
|
return (0, map_1.toEscalationRecords)(rows);
|
|
229
255
|
}
|
|
256
|
+
/**
|
|
257
|
+
* Cancel all pending escalations tied to a workflow in one atomic UPDATE.
|
|
258
|
+
* Called after workflow termination so escalations don't remain in the queue.
|
|
259
|
+
*
|
|
260
|
+
* HotMesh's interrupt() is supposed to do this atomically, but it filters by
|
|
261
|
+
* `app_id = <engine-appId>`. Long-tail's Durable client uses app_id='durable'
|
|
262
|
+
* while escalations are stored with app_id='hmsh' — the WHERE never matches.
|
|
263
|
+
* This single UPDATE bypasses the SDK entirely and closes that gap.
|
|
264
|
+
*/
|
|
265
|
+
async function cancelEscalationsByWorkflowId(workflowId) {
|
|
266
|
+
await (0, client_1.ensureEscalationCompatView)();
|
|
267
|
+
const pool = (0, db_1.getPool)();
|
|
268
|
+
const { rows } = await pool.query(`UPDATE public.hmsh_escalations
|
|
269
|
+
SET status = 'cancelled', updated_at = NOW()
|
|
270
|
+
WHERE workflow_id = $1
|
|
271
|
+
AND status = 'pending'
|
|
272
|
+
RETURNING id, workflow_id, workflow_type, task_queue`, [workflowId]);
|
|
273
|
+
for (const row of rows) {
|
|
274
|
+
(0, publish_1.publishEscalationEvent)({
|
|
275
|
+
type: 'escalation.cancelled',
|
|
276
|
+
source: 'service',
|
|
277
|
+
workflowId: row.workflow_id || '',
|
|
278
|
+
workflowName: row.workflow_type || '',
|
|
279
|
+
taskQueue: row.task_queue || '',
|
|
280
|
+
escalationId: row.id,
|
|
281
|
+
status: 'cancelled',
|
|
282
|
+
data: { reason: 'workflow_terminated' },
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
return rows.length;
|
|
286
|
+
}
|
|
230
287
|
async function updateEscalationMetadata(id, patch) {
|
|
231
288
|
const client = await (0, client_1.escalations)();
|
|
232
289
|
const entry = await client.update({ id, metadata: patch });
|
|
@@ -36,4 +36,4 @@ import type { Types } from '@hotmeshio/hotmesh';
|
|
|
36
36
|
* const decision = await conditionLT<{ approved: boolean }>(signalId);
|
|
37
37
|
* ```
|
|
38
38
|
*/
|
|
39
|
-
export declare function conditionLT<T = Record<string, any>>(signalId: string, escalation?: Types.ConditionQueueConfig): Promise<T>;
|
|
39
|
+
export declare function conditionLT<T = Record<string, any>>(signalId: string, escalation?: Types.ConditionQueueConfig): Promise<T | false | null>;
|
|
@@ -76,6 +76,9 @@ const LT_ACTIVITY_QUEUE = 'lt-interceptor';
|
|
|
76
76
|
*/
|
|
77
77
|
async function conditionLT(signalId, escalation) {
|
|
78
78
|
const raw = await hotmesh_1.Durable.workflow.condition(signalId, escalation);
|
|
79
|
+
// false = timeout, null = escalation was cancelled — propagate both as-is
|
|
80
|
+
if (raw === null || raw === false)
|
|
81
|
+
return raw;
|
|
79
82
|
const escalationId = raw.$escalation_id;
|
|
80
83
|
if (escalationId) {
|
|
81
84
|
// Resolve the escalation as a durable activity (crash-safe)
|
|
@@ -49,6 +49,7 @@ const logger_1 = require("../../lib/logger");
|
|
|
49
49
|
const system_events_1 = require("../../lib/events/system-events");
|
|
50
50
|
const namespaceService = __importStar(require("../namespace"));
|
|
51
51
|
const deployer_helpers_1 = require("./deployer-helpers");
|
|
52
|
+
const pipe_validator_1 = require("./pipe-validator");
|
|
52
53
|
// Re-export helpers so existing `import * from './deployer'` consumers keep working
|
|
53
54
|
var deployer_helpers_2 = require("./deployer-helpers");
|
|
54
55
|
Object.defineProperty(exports, "buildMergedYaml", { enumerable: true, get: function () { return deployer_helpers_2.buildMergedYaml; } });
|
|
@@ -90,6 +91,10 @@ async function deployAppId(appId, version) {
|
|
|
90
91
|
await namespaceService.registerNamespace(appId);
|
|
91
92
|
const mergedYaml = await (0, deployer_helpers_1.buildMergedYaml)(appId, version);
|
|
92
93
|
logger_1.loggerRegistry.debug(`[yaml-workflow] merged YAML for ${appId} v${version}:\n${mergedYaml}`);
|
|
94
|
+
// Structural guard: reject YAML with malformed @pipe rows before they reach
|
|
95
|
+
// the HotMesh engine. A bad pipe passes deploy() silently but causes
|
|
96
|
+
// collation errors on every execution, poisoning the stream.
|
|
97
|
+
(0, pipe_validator_1.validatePipeStructure)(mergedYaml);
|
|
93
98
|
const engine = await getEngine(appId);
|
|
94
99
|
try {
|
|
95
100
|
const manifest = await engine.deploy(mergedYaml);
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structural validator for HotMesh YAML pipe syntax.
|
|
3
|
+
*
|
|
4
|
+
* HotMesh collation errors are silent at deploy time but catastrophic at
|
|
5
|
+
* runtime — a single malformed @pipe in an activated workflow poisons the
|
|
6
|
+
* engine's stream, causing every execution attempt to fail with a
|
|
7
|
+
* collation-error until the database is wiped.
|
|
8
|
+
*
|
|
9
|
+
* This module provides:
|
|
10
|
+
* validatePipeStructure — throws on any structurally invalid @pipe
|
|
11
|
+
* repairPipeStructure — auto-repairs safe-to-fix patterns, then validates
|
|
12
|
+
*
|
|
13
|
+
* Call repairPipeStructure before storing or deploying any YAML.
|
|
14
|
+
*
|
|
15
|
+
* ## What constitutes a valid @pipe row
|
|
16
|
+
*
|
|
17
|
+
* An @pipe value is a YAML sequence. Each element (row) must be one of:
|
|
18
|
+
* 1. A YAML sequence (array) — operands row OR function call row
|
|
19
|
+
* 2. A YAML mapping with '@pipe' key — nested sub-pipe
|
|
20
|
+
*
|
|
21
|
+
* Invalid row types that cause collation errors:
|
|
22
|
+
* - A bare string: '{@object.create}' → must be ['{@object.create}']
|
|
23
|
+
* - A number/bool: 42, true → must be wrapped in an array
|
|
24
|
+
* - A double-nested sequence: [[...]] → row itself is an array of arrays
|
|
25
|
+
*/
|
|
26
|
+
export interface PipeViolation {
|
|
27
|
+
path: string;
|
|
28
|
+
rowIndex: number;
|
|
29
|
+
rowValue: unknown;
|
|
30
|
+
message: string;
|
|
31
|
+
repairable: boolean;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Parse the YAML and check every @pipe for structural violations.
|
|
35
|
+
* Throws a descriptive error listing all violations if any are found.
|
|
36
|
+
*/
|
|
37
|
+
export declare function validatePipeStructure(yamlContent: string): void;
|
|
38
|
+
/**
|
|
39
|
+
* Auto-repair safe violations (bare strings/scalars → array rows), then
|
|
40
|
+
* validate that no unfixable violations remain.
|
|
41
|
+
*
|
|
42
|
+
* Returns the repaired YAML string. Throws if unfixable violations exist.
|
|
43
|
+
*/
|
|
44
|
+
export declare function repairPipeStructure(yamlContent: string): string;
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Structural validator for HotMesh YAML pipe syntax.
|
|
4
|
+
*
|
|
5
|
+
* HotMesh collation errors are silent at deploy time but catastrophic at
|
|
6
|
+
* runtime — a single malformed @pipe in an activated workflow poisons the
|
|
7
|
+
* engine's stream, causing every execution attempt to fail with a
|
|
8
|
+
* collation-error until the database is wiped.
|
|
9
|
+
*
|
|
10
|
+
* This module provides:
|
|
11
|
+
* validatePipeStructure — throws on any structurally invalid @pipe
|
|
12
|
+
* repairPipeStructure — auto-repairs safe-to-fix patterns, then validates
|
|
13
|
+
*
|
|
14
|
+
* Call repairPipeStructure before storing or deploying any YAML.
|
|
15
|
+
*
|
|
16
|
+
* ## What constitutes a valid @pipe row
|
|
17
|
+
*
|
|
18
|
+
* An @pipe value is a YAML sequence. Each element (row) must be one of:
|
|
19
|
+
* 1. A YAML sequence (array) — operands row OR function call row
|
|
20
|
+
* 2. A YAML mapping with '@pipe' key — nested sub-pipe
|
|
21
|
+
*
|
|
22
|
+
* Invalid row types that cause collation errors:
|
|
23
|
+
* - A bare string: '{@object.create}' → must be ['{@object.create}']
|
|
24
|
+
* - A number/bool: 42, true → must be wrapped in an array
|
|
25
|
+
* - A double-nested sequence: [[...]] → row itself is an array of arrays
|
|
26
|
+
*/
|
|
27
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
28
|
+
if (k2 === undefined) k2 = k;
|
|
29
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
30
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
31
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
32
|
+
}
|
|
33
|
+
Object.defineProperty(o, k2, desc);
|
|
34
|
+
}) : (function(o, m, k, k2) {
|
|
35
|
+
if (k2 === undefined) k2 = k;
|
|
36
|
+
o[k2] = m[k];
|
|
37
|
+
}));
|
|
38
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
39
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
40
|
+
}) : function(o, v) {
|
|
41
|
+
o["default"] = v;
|
|
42
|
+
});
|
|
43
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
44
|
+
var ownKeys = function(o) {
|
|
45
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
46
|
+
var ar = [];
|
|
47
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
48
|
+
return ar;
|
|
49
|
+
};
|
|
50
|
+
return ownKeys(o);
|
|
51
|
+
};
|
|
52
|
+
return function (mod) {
|
|
53
|
+
if (mod && mod.__esModule) return mod;
|
|
54
|
+
var result = {};
|
|
55
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
56
|
+
__setModuleDefault(result, mod);
|
|
57
|
+
return result;
|
|
58
|
+
};
|
|
59
|
+
})();
|
|
60
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
61
|
+
exports.validatePipeStructure = validatePipeStructure;
|
|
62
|
+
exports.repairPipeStructure = repairPipeStructure;
|
|
63
|
+
const jsYaml = __importStar(require("js-yaml"));
|
|
64
|
+
// ── internal traversal ────────────────────────────────────────────────────────
|
|
65
|
+
function isPlainObject(v) {
|
|
66
|
+
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
67
|
+
}
|
|
68
|
+
function collectPipeViolations(node, path, violations) {
|
|
69
|
+
if (Array.isArray(node)) {
|
|
70
|
+
node.forEach((item, i) => collectPipeViolations(item, `${path}[${i}]`, violations));
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (!isPlainObject(node))
|
|
74
|
+
return;
|
|
75
|
+
for (const [key, value] of Object.entries(node)) {
|
|
76
|
+
const childPath = `${path}.${key}`;
|
|
77
|
+
if (key === '@pipe') {
|
|
78
|
+
if (!Array.isArray(value)) {
|
|
79
|
+
violations.push({
|
|
80
|
+
path: childPath,
|
|
81
|
+
rowIndex: -1,
|
|
82
|
+
rowValue: value,
|
|
83
|
+
message: `@pipe value must be a sequence (array), got ${typeof value}`,
|
|
84
|
+
repairable: false,
|
|
85
|
+
});
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
value.forEach((row, i) => {
|
|
89
|
+
if (Array.isArray(row)) {
|
|
90
|
+
// Valid row — but check for double-nested arrays: [[...]]
|
|
91
|
+
if (row.length > 0 && row.every((el) => Array.isArray(el))) {
|
|
92
|
+
violations.push({
|
|
93
|
+
path: childPath,
|
|
94
|
+
rowIndex: i,
|
|
95
|
+
rowValue: row,
|
|
96
|
+
message: `row ${i} is a double-nested array (array of arrays) — each @pipe row must be a flat array`,
|
|
97
|
+
repairable: false,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
else if (isPlainObject(row) && '@pipe' in row) {
|
|
102
|
+
// Valid nested sub-pipe — recurse into it
|
|
103
|
+
collectPipeViolations(row, `${childPath}[${i}]`, violations);
|
|
104
|
+
}
|
|
105
|
+
else if (typeof row === 'string') {
|
|
106
|
+
// '{@fn}' as bare string — repairable: wrap in array
|
|
107
|
+
violations.push({
|
|
108
|
+
path: childPath,
|
|
109
|
+
rowIndex: i,
|
|
110
|
+
rowValue: row,
|
|
111
|
+
message: `row ${i} is a bare string "${row}" — must be an array row like ['${row}']`,
|
|
112
|
+
repairable: true,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
else if (typeof row === 'number' || typeof row === 'boolean') {
|
|
116
|
+
violations.push({
|
|
117
|
+
path: childPath,
|
|
118
|
+
rowIndex: i,
|
|
119
|
+
rowValue: row,
|
|
120
|
+
message: `row ${i} is a bare scalar ${JSON.stringify(row)} — must be wrapped in an array`,
|
|
121
|
+
repairable: true,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
violations.push({
|
|
126
|
+
path: childPath,
|
|
127
|
+
rowIndex: i,
|
|
128
|
+
rowValue: row,
|
|
129
|
+
message: `row ${i} has unexpected type ${typeof row}`,
|
|
130
|
+
repairable: false,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
collectPipeViolations(value, childPath, violations);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
// ── public API ────────────────────────────────────────────────────────────────
|
|
141
|
+
/**
|
|
142
|
+
* Parse the YAML and check every @pipe for structural violations.
|
|
143
|
+
* Throws a descriptive error listing all violations if any are found.
|
|
144
|
+
*/
|
|
145
|
+
function validatePipeStructure(yamlContent) {
|
|
146
|
+
let parsed;
|
|
147
|
+
try {
|
|
148
|
+
parsed = jsYaml.load(yamlContent);
|
|
149
|
+
}
|
|
150
|
+
catch (err) {
|
|
151
|
+
throw new Error(`YAML parse error: ${err.message}`);
|
|
152
|
+
}
|
|
153
|
+
const violations = [];
|
|
154
|
+
collectPipeViolations(parsed, 'root', violations);
|
|
155
|
+
if (violations.length > 0) {
|
|
156
|
+
const detail = violations
|
|
157
|
+
.map((v) => ` • ${v.path}[${v.rowIndex}]: ${v.message}`)
|
|
158
|
+
.join('\n');
|
|
159
|
+
throw new Error(`Malformed @pipe structure — ${violations.length} violation(s) detected. ` +
|
|
160
|
+
`This YAML will cause HotMesh collation errors at runtime and must not be deployed.\n${detail}`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Auto-repair safe violations (bare strings/scalars → array rows), then
|
|
165
|
+
* validate that no unfixable violations remain.
|
|
166
|
+
*
|
|
167
|
+
* Returns the repaired YAML string. Throws if unfixable violations exist.
|
|
168
|
+
*/
|
|
169
|
+
function repairPipeStructure(yamlContent) {
|
|
170
|
+
let parsed;
|
|
171
|
+
try {
|
|
172
|
+
parsed = jsYaml.load(yamlContent);
|
|
173
|
+
}
|
|
174
|
+
catch (err) {
|
|
175
|
+
throw new Error(`YAML parse error: ${err.message}`);
|
|
176
|
+
}
|
|
177
|
+
const repairCount = { n: 0 };
|
|
178
|
+
repairNode(parsed, repairCount);
|
|
179
|
+
if (repairCount.n > 0) {
|
|
180
|
+
// Re-serialize — use lineWidth: -1 to prevent wrapping
|
|
181
|
+
yamlContent = jsYaml.dump(parsed, { lineWidth: -1, noRefs: true, quotingType: "'" });
|
|
182
|
+
}
|
|
183
|
+
// Final validation pass — throws if unfixable violations remain
|
|
184
|
+
validatePipeStructure(yamlContent);
|
|
185
|
+
return yamlContent;
|
|
186
|
+
}
|
|
187
|
+
function repairNode(node, count) {
|
|
188
|
+
if (Array.isArray(node)) {
|
|
189
|
+
node.forEach((item) => repairNode(item, count));
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
if (!isPlainObject(node))
|
|
193
|
+
return;
|
|
194
|
+
for (const [key, value] of Object.entries(node)) {
|
|
195
|
+
if (key === '@pipe' && Array.isArray(value)) {
|
|
196
|
+
const rows = value;
|
|
197
|
+
for (let i = 0; i < rows.length; i++) {
|
|
198
|
+
const row = rows[i];
|
|
199
|
+
if (typeof row === 'string' || typeof row === 'number' || typeof row === 'boolean') {
|
|
200
|
+
rows[i] = [row];
|
|
201
|
+
count.n++;
|
|
202
|
+
}
|
|
203
|
+
else if (isPlainObject(row) && '@pipe' in row) {
|
|
204
|
+
repairNode(row, count);
|
|
205
|
+
}
|
|
206
|
+
else if (Array.isArray(row)) {
|
|
207
|
+
repairNode(row, count);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
repairNode(value, count);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Diagnostics tools — mirrors routes/diagnostics.ts
|
|
3
|
+
*
|
|
4
|
+
* Three tools:
|
|
5
|
+
* diagnose_job — full per-job diagnosis: events + stream messages + findings
|
|
6
|
+
* find_stalled_jobs — fleet-level: running jobs with no recent progress
|
|
7
|
+
* find_orphaned_signals — fleet-level: suspended jobs with no escalation row
|
|
8
|
+
*/
|
|
9
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
10
|
+
export declare function registerDiagnosticsTools(server: McpServer): void;
|