@resolveio/server-lib 22.3.221 → 22.3.222

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 (745) hide show
  1. package/ai/assistant-core-heuristics.d.ts +11 -0
  2. package/ai/assistant-core-heuristics.js +356 -0
  3. package/ai/assistant-core-heuristics.js.map +1 -0
  4. package/ai/resolveio-platform-intelligence-memory-corpus.d.ts +3 -0
  5. package/ai/resolveio-platform-intelligence-memory-corpus.js +214 -0
  6. package/ai/resolveio-platform-intelligence-memory-corpus.js.map +1 -0
  7. package/ai/resolveio-platform-intelligence-memory.d.ts +20 -0
  8. package/ai/resolveio-platform-intelligence-memory.js +341 -0
  9. package/ai/resolveio-platform-intelligence-memory.js.map +1 -0
  10. package/{src/ai/resolveio-platform-intelligence-types.ts → ai/resolveio-platform-intelligence-types.d.ts} +15 -20
  11. package/ai/resolveio-platform-intelligence-types.js +4 -0
  12. package/ai/resolveio-platform-intelligence-types.js.map +1 -0
  13. package/ai/resolveio-platform-intelligence.d.ts +6 -0
  14. package/ai/resolveio-platform-intelligence.js +463 -0
  15. package/ai/resolveio-platform-intelligence.js.map +1 -0
  16. package/client-server-app.d.ts +1 -0
  17. package/client-server-app.js +68 -0
  18. package/client-server-app.js.map +1 -0
  19. package/collections/ai-run.collection.d.ts +3 -0
  20. package/collections/ai-run.collection.js +170 -0
  21. package/collections/ai-run.collection.js.map +1 -0
  22. package/collections/ai-terminal-conversation.collection.d.ts +2 -0
  23. package/collections/ai-terminal-conversation.collection.js +140 -0
  24. package/collections/ai-terminal-conversation.collection.js.map +1 -0
  25. package/collections/ai-terminal-issue-report.collection.d.ts +2 -0
  26. package/collections/ai-terminal-issue-report.collection.js +148 -0
  27. package/collections/ai-terminal-issue-report.collection.js.map +1 -0
  28. package/collections/ai-terminal-message.collection.d.ts +2 -0
  29. package/collections/ai-terminal-message.collection.js +121 -0
  30. package/collections/ai-terminal-message.collection.js.map +1 -0
  31. package/collections/app-setting.collection.d.ts +3 -0
  32. package/collections/app-setting.collection.js +103 -0
  33. package/collections/app-setting.collection.js.map +1 -0
  34. package/collections/app-status.collection.d.ts +3 -0
  35. package/collections/app-status.collection.js +57 -0
  36. package/collections/app-status.collection.js.map +1 -0
  37. package/collections/communication-metric.collection.d.ts +2 -0
  38. package/collections/communication-metric.collection.js +133 -0
  39. package/collections/communication-metric.collection.js.map +1 -0
  40. package/collections/counter.collection.d.ts +3 -0
  41. package/collections/counter.collection.js +56 -0
  42. package/collections/counter.collection.js.map +1 -0
  43. package/collections/cron-job-history.collection.d.ts +3 -0
  44. package/collections/cron-job-history.collection.js +137 -0
  45. package/collections/cron-job-history.collection.js.map +1 -0
  46. package/collections/cron-job.collection.d.ts +3 -0
  47. package/collections/cron-job.collection.js +92 -0
  48. package/collections/cron-job.collection.js.map +1 -0
  49. package/collections/customer-notification.collection.d.ts +3 -0
  50. package/collections/customer-notification.collection.js +130 -0
  51. package/collections/customer-notification.collection.js.map +1 -0
  52. package/collections/customer-portal-password.collection.d.ts +3 -0
  53. package/collections/customer-portal-password.collection.js +75 -0
  54. package/collections/customer-portal-password.collection.js.map +1 -0
  55. package/collections/email-history.collection.d.ts +3 -0
  56. package/collections/email-history.collection.js +134 -0
  57. package/collections/email-history.collection.js.map +1 -0
  58. package/collections/email-verified.collection.d.ts +3 -0
  59. package/collections/email-verified.collection.js +62 -0
  60. package/collections/email-verified.collection.js.map +1 -0
  61. package/collections/file.collection.d.ts +3 -0
  62. package/collections/file.collection.js +74 -0
  63. package/collections/file.collection.js.map +1 -0
  64. package/collections/flag-update.collection.d.ts +3 -0
  65. package/collections/flag-update.collection.js +57 -0
  66. package/collections/flag-update.collection.js.map +1 -0
  67. package/collections/flag.collection.d.ts +3 -0
  68. package/collections/flag.collection.js +57 -0
  69. package/collections/flag.collection.js.map +1 -0
  70. package/collections/log-method-latency.collection.d.ts +3 -0
  71. package/collections/log-method-latency.collection.js +77 -0
  72. package/collections/log-method-latency.collection.js.map +1 -0
  73. package/collections/log-subscription.collection.d.ts +3 -0
  74. package/collections/log-subscription.collection.js +80 -0
  75. package/collections/log-subscription.collection.js.map +1 -0
  76. package/collections/log.collection.d.ts +3 -0
  77. package/collections/log.collection.js +93 -0
  78. package/collections/log.collection.js.map +1 -0
  79. package/collections/logged-in-users.collection.d.ts +3 -0
  80. package/collections/logged-in-users.collection.js +67 -0
  81. package/collections/logged-in-users.collection.js.map +1 -0
  82. package/collections/monitor-cpu.collection.d.ts +3 -0
  83. package/collections/monitor-cpu.collection.js +65 -0
  84. package/collections/monitor-cpu.collection.js.map +1 -0
  85. package/collections/monitor-function.collection.d.ts +3 -0
  86. package/collections/monitor-function.collection.js +74 -0
  87. package/collections/monitor-function.collection.js.map +1 -0
  88. package/collections/monitor-memory.collection.d.ts +3 -0
  89. package/collections/monitor-memory.collection.js +77 -0
  90. package/collections/monitor-memory.collection.js.map +1 -0
  91. package/collections/monitor-mongo.collection.d.ts +3 -0
  92. package/collections/monitor-mongo.collection.js +71 -0
  93. package/collections/monitor-mongo.collection.js.map +1 -0
  94. package/collections/notification.collection.d.ts +3 -0
  95. package/collections/notification.collection.js +57 -0
  96. package/collections/notification.collection.js.map +1 -0
  97. package/collections/openai-usage-ledger.collection.d.ts +2 -0
  98. package/collections/openai-usage-ledger.collection.js +188 -0
  99. package/collections/openai-usage-ledger.collection.js.map +1 -0
  100. package/collections/report-builder-dashboard-builder.collection.d.ts +3 -0
  101. package/collections/report-builder-dashboard-builder.collection.js +109 -0
  102. package/collections/report-builder-dashboard-builder.collection.js.map +1 -0
  103. package/collections/report-builder-library.collection.d.ts +3 -0
  104. package/collections/report-builder-library.collection.js +87 -0
  105. package/collections/report-builder-library.collection.js.map +1 -0
  106. package/collections/report-builder-report.collection.d.ts +4 -0
  107. package/collections/report-builder-report.collection.js +184 -0
  108. package/collections/report-builder-report.collection.js.map +1 -0
  109. package/collections/user-group.collection.d.ts +4 -0
  110. package/collections/user-group.collection.js +89 -0
  111. package/collections/user-group.collection.js.map +1 -0
  112. package/collections/user-guide.collection.d.ts +3 -0
  113. package/collections/user-guide.collection.js +57 -0
  114. package/collections/user-guide.collection.js.map +1 -0
  115. package/collections/user.collection.d.ts +4 -0
  116. package/collections/user.collection.js +180 -0
  117. package/collections/user.collection.js.map +1 -0
  118. package/cron/cron.d.ts +14 -0
  119. package/cron/cron.js +216 -0
  120. package/cron/cron.js.map +1 -0
  121. package/fixtures/cron-jobs.d.ts +1 -0
  122. package/fixtures/cron-jobs.js +150 -0
  123. package/fixtures/cron-jobs.js.map +1 -0
  124. package/fixtures/init.d.ts +1 -0
  125. package/fixtures/init.js +91 -0
  126. package/fixtures/init.js.map +1 -0
  127. package/http/auth.d.ts +2 -0
  128. package/http/auth.js +951 -0
  129. package/http/auth.js.map +1 -0
  130. package/http/health.d.ts +1 -0
  131. package/http/health.js +11 -0
  132. package/http/health.js.map +1 -0
  133. package/http/home.d.ts +1 -0
  134. package/http/home.js +134 -0
  135. package/http/home.js.map +1 -0
  136. package/http/slow-query-publication.d.ts +2 -0
  137. package/http/slow-query-publication.js +99 -0
  138. package/http/slow-query-publication.js.map +1 -0
  139. package/index.d.ts +1 -0
  140. package/index.js +19 -0
  141. package/index.js.map +1 -0
  142. package/managers/ai-assistant-codex-manager.manager.d.ts +67 -0
  143. package/managers/ai-assistant-codex-manager.manager.js +1113 -0
  144. package/managers/ai-assistant-codex-manager.manager.js.map +1 -0
  145. package/managers/ai-run-evidence.manager.d.ts +36 -0
  146. package/managers/ai-run-evidence.manager.js +377 -0
  147. package/managers/ai-run-evidence.manager.js.map +1 -0
  148. package/managers/communication-metric.manager.d.ts +16 -0
  149. package/managers/communication-metric.manager.js +134 -0
  150. package/managers/communication-metric.manager.js.map +1 -0
  151. package/managers/cron.manager.d.ts +20 -0
  152. package/managers/cron.manager.js +534 -0
  153. package/managers/cron.manager.js.map +1 -0
  154. package/managers/customer-notification-content.manager.d.ts +55 -0
  155. package/managers/customer-notification-content.manager.js +158 -0
  156. package/managers/customer-notification-content.manager.js.map +1 -0
  157. package/managers/diagnostic-manager-bootstrap.d.ts +9 -0
  158. package/managers/diagnostic-manager-bootstrap.js +260 -0
  159. package/managers/diagnostic-manager-bootstrap.js.map +1 -0
  160. package/managers/error-auto-fix.manager.d.ts +149 -0
  161. package/managers/error-auto-fix.manager.js +3064 -0
  162. package/managers/error-auto-fix.manager.js.map +1 -0
  163. package/managers/local-log.manager.d.ts +18 -0
  164. package/managers/local-log.manager.js +88 -0
  165. package/managers/local-log.manager.js.map +1 -0
  166. package/managers/method.manager.d.ts +84 -0
  167. package/managers/method.manager.js +1964 -0
  168. package/managers/method.manager.js.map +1 -0
  169. package/managers/mongo.manager.d.ts +224 -0
  170. package/managers/mongo.manager.js +5000 -0
  171. package/managers/mongo.manager.js.map +1 -0
  172. package/managers/monitor.manager.d.ts +70 -0
  173. package/managers/monitor.manager.js +550 -0
  174. package/managers/monitor.manager.js.map +1 -0
  175. package/managers/openai-usage-ledger.manager.d.ts +30 -0
  176. package/managers/openai-usage-ledger.manager.js +142 -0
  177. package/managers/openai-usage-ledger.manager.js.map +1 -0
  178. package/managers/slow-query-verifier.manager.d.ts +144 -0
  179. package/managers/slow-query-verifier.manager.js +3857 -0
  180. package/managers/slow-query-verifier.manager.js.map +1 -0
  181. package/managers/slow-query.manager.d.ts +28 -0
  182. package/managers/slow-query.manager.js +468 -0
  183. package/managers/slow-query.manager.js.map +1 -0
  184. package/managers/subscription.manager.d.ts +169 -0
  185. package/managers/subscription.manager.js +3434 -0
  186. package/managers/subscription.manager.js.map +1 -0
  187. package/managers/websocket.manager.d.ts +73 -0
  188. package/managers/websocket.manager.js +673 -0
  189. package/managers/websocket.manager.js.map +1 -0
  190. package/managers/worker-dispatcher.manager.d.ts +120 -0
  191. package/managers/worker-dispatcher.manager.js +1266 -0
  192. package/managers/worker-dispatcher.manager.js.map +1 -0
  193. package/managers/worker-server.manager.d.ts +35 -0
  194. package/managers/worker-server.manager.js +582 -0
  195. package/managers/worker-server.manager.js.map +1 -0
  196. package/methods/accounts.d.ts +2 -0
  197. package/methods/accounts.js +624 -0
  198. package/methods/accounts.js.map +1 -0
  199. package/methods/ai-terminal.d.ts +458 -0
  200. package/methods/ai-terminal.js +27991 -0
  201. package/methods/ai-terminal.js.map +1 -0
  202. package/methods/app-settings.d.ts +2 -0
  203. package/methods/app-settings.js +169 -0
  204. package/methods/app-settings.js.map +1 -0
  205. package/methods/aws.d.ts +2 -0
  206. package/methods/aws.js +877 -0
  207. package/methods/aws.js.map +1 -0
  208. package/methods/collections.d.ts +2 -0
  209. package/methods/collections.js +719 -0
  210. package/methods/collections.js.map +1 -0
  211. package/methods/counters.d.ts +2 -0
  212. package/methods/counters.js +113 -0
  213. package/methods/counters.js.map +1 -0
  214. package/methods/cron-jobs.d.ts +2 -0
  215. package/methods/cron-jobs.js +2475 -0
  216. package/methods/cron-jobs.js.map +1 -0
  217. package/methods/customer-notifications.d.ts +2 -0
  218. package/methods/customer-notifications.js +528 -0
  219. package/methods/customer-notifications.js.map +1 -0
  220. package/methods/diagnostics.d.ts +2 -0
  221. package/methods/diagnostics.js +703 -0
  222. package/methods/diagnostics.js.map +1 -0
  223. package/methods/flag-updates.d.ts +2 -0
  224. package/methods/flag-updates.js +8 -0
  225. package/methods/flag-updates.js.map +1 -0
  226. package/methods/flags.d.ts +2 -0
  227. package/methods/flags.js +8 -0
  228. package/methods/flags.js.map +1 -0
  229. package/methods/logs.d.ts +2 -0
  230. package/methods/logs.js +751 -0
  231. package/methods/logs.js.map +1 -0
  232. package/methods/mongo-explorer.d.ts +2 -0
  233. package/methods/mongo-explorer.js +1808 -0
  234. package/methods/mongo-explorer.js.map +1 -0
  235. package/methods/monitor.d.ts +2 -0
  236. package/methods/monitor.js +543 -0
  237. package/methods/monitor.js.map +1 -0
  238. package/methods/pdf.d.ts +2 -0
  239. package/methods/pdf.js +1216 -0
  240. package/methods/pdf.js.map +1 -0
  241. package/methods/publications.d.ts +1 -0
  242. package/methods/publications.js +183 -0
  243. package/methods/publications.js.map +1 -0
  244. package/methods/report-builder.d.ts +2 -0
  245. package/methods/report-builder.js +3094 -0
  246. package/methods/report-builder.js.map +1 -0
  247. package/methods/support.d.ts +2 -0
  248. package/methods/support.js +430 -0
  249. package/methods/support.js.map +1 -0
  250. package/models/ai-run.model.d.ts +19 -0
  251. package/models/ai-run.model.js +4 -0
  252. package/models/ai-run.model.js.map +1 -0
  253. package/models/ai-terminal-conversation.model.d.ts +17 -0
  254. package/models/ai-terminal-conversation.model.js +4 -0
  255. package/models/ai-terminal-conversation.model.js.map +1 -0
  256. package/models/ai-terminal-issue-report.model.d.ts +19 -0
  257. package/models/ai-terminal-issue-report.model.js +4 -0
  258. package/models/ai-terminal-issue-report.model.js.map +1 -0
  259. package/models/ai-terminal-message.model.d.ts +22 -0
  260. package/models/ai-terminal-message.model.js +4 -0
  261. package/models/ai-terminal-message.model.js.map +1 -0
  262. package/models/app-setting.model.d.ts +16 -0
  263. package/models/app-setting.model.js +4 -0
  264. package/models/app-setting.model.js.map +1 -0
  265. package/{src/models/app-status.model.ts → models/app-status.model.d.ts} +2 -3
  266. package/models/app-status.model.js +4 -0
  267. package/models/app-status.model.js.map +1 -0
  268. package/{src/models/billing-logged-in-users.model.ts → models/billing-logged-in-users.model.d.ts} +4 -5
  269. package/models/billing-logged-in-users.model.js +4 -0
  270. package/models/billing-logged-in-users.model.js.map +1 -0
  271. package/models/collection-document.model.d.ts +21 -0
  272. package/models/collection-document.model.js +4 -0
  273. package/models/collection-document.model.js.map +1 -0
  274. package/models/communication-metric.model.d.ts +20 -0
  275. package/models/communication-metric.model.js +4 -0
  276. package/models/communication-metric.model.js.map +1 -0
  277. package/{src/models/counter.model.ts → models/counter.model.d.ts} +3 -4
  278. package/models/counter.model.js +4 -0
  279. package/models/counter.model.js.map +1 -0
  280. package/models/cron-job-history.model.d.ts +15 -0
  281. package/models/cron-job-history.model.js +4 -0
  282. package/models/cron-job-history.model.js.map +1 -0
  283. package/models/cron-job.model.d.ts +14 -0
  284. package/models/cron-job.model.js +4 -0
  285. package/models/cron-job.model.js.map +1 -0
  286. package/models/customer-notification.model.d.ts +26 -0
  287. package/models/customer-notification.model.js +4 -0
  288. package/models/customer-notification.model.js.map +1 -0
  289. package/models/customer-portal-password.model.d.ts +11 -0
  290. package/models/customer-portal-password.model.js +4 -0
  291. package/models/customer-portal-password.model.js.map +1 -0
  292. package/models/dialog.model.d.ts +23 -0
  293. package/models/dialog.model.js +4 -0
  294. package/models/dialog.model.js.map +1 -0
  295. package/models/email-history.model.d.ts +32 -0
  296. package/{src/models/email-history.model.ts → models/email-history.model.js} +4 -36
  297. package/models/email-history.model.js.map +1 -0
  298. package/{src/models/email-verified.model.ts → models/email-verified.model.d.ts} +5 -6
  299. package/models/email-verified.model.js +4 -0
  300. package/models/email-verified.model.js.map +1 -0
  301. package/{src/models/file.model.ts → models/file.model.d.ts} +7 -8
  302. package/models/file.model.js +4 -0
  303. package/models/file.model.js.map +1 -0
  304. package/{src/models/flag-update.model.ts → models/flag-update.model.d.ts} +3 -4
  305. package/models/flag-update.model.js +4 -0
  306. package/models/flag-update.model.js.map +1 -0
  307. package/{src/models/flag.model.ts → models/flag.model.d.ts} +3 -4
  308. package/models/flag.model.js +4 -0
  309. package/models/flag.model.js.map +1 -0
  310. package/models/log-method-latency.model.d.ts +10 -0
  311. package/models/log-method-latency.model.js +4 -0
  312. package/models/log-method-latency.model.js.map +1 -0
  313. package/{src/models/log-subscription.model.ts → models/log-subscription.model.d.ts} +9 -11
  314. package/models/log-subscription.model.js +4 -0
  315. package/models/log-subscription.model.js.map +1 -0
  316. package/models/log.model.d.ts +17 -0
  317. package/models/log.model.js +4 -0
  318. package/models/log.model.js.map +1 -0
  319. package/{src/models/logged-in-users.model.ts → models/logged-in-users.model.d.ts} +5 -6
  320. package/models/logged-in-users.model.js +4 -0
  321. package/models/logged-in-users.model.js.map +1 -0
  322. package/{src/models/method-response.model.ts → models/method-response.model.d.ts} +6 -7
  323. package/models/method-response.model.js +4 -0
  324. package/models/method-response.model.js.map +1 -0
  325. package/models/method.model.d.ts +26 -0
  326. package/models/method.model.js +4 -0
  327. package/models/method.model.js.map +1 -0
  328. package/{src/models/monitor-cpu.model.ts → models/monitor-cpu.model.d.ts} +7 -9
  329. package/models/monitor-cpu.model.js +4 -0
  330. package/models/monitor-cpu.model.js.map +1 -0
  331. package/models/monitor-function.model.d.ts +14 -0
  332. package/models/monitor-function.model.js +4 -0
  333. package/models/monitor-function.model.js.map +1 -0
  334. package/models/monitor-memory.model.d.ts +15 -0
  335. package/models/monitor-memory.model.js +4 -0
  336. package/models/monitor-memory.model.js.map +1 -0
  337. package/models/monitor-mongo.model.d.ts +13 -0
  338. package/models/monitor-mongo.model.js +4 -0
  339. package/models/monitor-mongo.model.js.map +1 -0
  340. package/{src/models/notification.model.ts → models/notification.model.d.ts} +4 -6
  341. package/models/notification.model.js +4 -0
  342. package/models/notification.model.js.map +1 -0
  343. package/models/openai-usage-ledger.model.d.ts +30 -0
  344. package/models/openai-usage-ledger.model.js +4 -0
  345. package/models/openai-usage-ledger.model.js.map +1 -0
  346. package/models/pagination.model.d.ts +11 -0
  347. package/models/pagination.model.js +28 -0
  348. package/models/pagination.model.js.map +1 -0
  349. package/models/permission.model.d.ts +12 -0
  350. package/models/permission.model.js +4 -0
  351. package/models/permission.model.js.map +1 -0
  352. package/models/report-builder-dashboard-builder.model.d.ts +25 -0
  353. package/models/report-builder-dashboard-builder.model.js +4 -0
  354. package/models/report-builder-dashboard-builder.model.js.map +1 -0
  355. package/models/report-builder-library.model.d.ts +17 -0
  356. package/models/report-builder-library.model.js +4 -0
  357. package/models/report-builder-library.model.js.map +1 -0
  358. package/models/report-builder-report.model.d.ts +121 -0
  359. package/models/report-builder-report.model.js +4 -0
  360. package/models/report-builder-report.model.js.map +1 -0
  361. package/models/report-builder.model.d.ts +61 -0
  362. package/models/report-builder.model.js +4 -0
  363. package/models/report-builder.model.js.map +1 -0
  364. package/models/select-data-label.model.d.ts +9 -0
  365. package/models/select-data-label.model.js +4 -0
  366. package/models/select-data-label.model.js.map +1 -0
  367. package/models/server-message.model.d.ts +32 -0
  368. package/models/server-message.model.js +4 -0
  369. package/models/server-message.model.js.map +1 -0
  370. package/models/slow-query-report.model.d.ts +23 -0
  371. package/models/slow-query-report.model.js +4 -0
  372. package/models/slow-query-report.model.js.map +1 -0
  373. package/models/subscription.model.d.ts +31 -0
  374. package/models/subscription.model.js +4 -0
  375. package/models/subscription.model.js.map +1 -0
  376. package/models/support-ticket.model.d.ts +87 -0
  377. package/models/support-ticket.model.js +4 -0
  378. package/models/support-ticket.model.js.map +1 -0
  379. package/models/user-group.model.d.ts +20 -0
  380. package/models/user-group.model.js +4 -0
  381. package/models/user-group.model.js.map +1 -0
  382. package/{src/models/user-guide.model.ts → models/user-guide.model.d.ts} +4 -5
  383. package/models/user-guide.model.js +4 -0
  384. package/models/user-guide.model.js.map +1 -0
  385. package/models/user.model.d.ts +84 -0
  386. package/models/user.model.js +4 -0
  387. package/models/user.model.js.map +1 -0
  388. package/package.json +1 -1
  389. package/private/images/ResolveIO.png +0 -0
  390. package/public_api.js +127 -0
  391. package/public_api.js.map +1 -0
  392. package/publications/ai-terminal.d.ts +1 -0
  393. package/publications/ai-terminal.js +122 -0
  394. package/publications/ai-terminal.js.map +1 -0
  395. package/publications/app-settings.d.ts +2 -0
  396. package/publications/app-settings.js +28 -0
  397. package/publications/app-settings.js.map +1 -0
  398. package/publications/app-status.d.ts +2 -0
  399. package/publications/app-status.js +16 -0
  400. package/publications/app-status.js.map +1 -0
  401. package/publications/cron-jobs.d.ts +2 -0
  402. package/publications/cron-jobs.js +88 -0
  403. package/publications/cron-jobs.js.map +1 -0
  404. package/publications/customer-notifications.d.ts +2 -0
  405. package/publications/customer-notifications.js +161 -0
  406. package/publications/customer-notifications.js.map +1 -0
  407. package/publications/files.d.ts +2 -0
  408. package/publications/files.js +36 -0
  409. package/publications/files.js.map +1 -0
  410. package/publications/flags-update.d.ts +2 -0
  411. package/publications/flags-update.js +22 -0
  412. package/publications/flags-update.js.map +1 -0
  413. package/publications/flags.d.ts +2 -0
  414. package/publications/flags.js +22 -0
  415. package/publications/flags.js.map +1 -0
  416. package/publications/logs.d.ts +2 -0
  417. package/publications/logs.js +164 -0
  418. package/publications/logs.js.map +1 -0
  419. package/publications/notifications.d.ts +2 -0
  420. package/publications/notifications.js +16 -0
  421. package/publications/notifications.js.map +1 -0
  422. package/publications/report-builder-dashboard-builders.d.ts +2 -0
  423. package/publications/report-builder-dashboard-builders.js +42 -0
  424. package/publications/report-builder-dashboard-builders.js.map +1 -0
  425. package/publications/report-builder-libraries.d.ts +2 -0
  426. package/publications/report-builder-libraries.js +90 -0
  427. package/publications/report-builder-libraries.js.map +1 -0
  428. package/publications/report-builder-reports.d.ts +2 -0
  429. package/publications/report-builder-reports.js +50 -0
  430. package/publications/report-builder-reports.js.map +1 -0
  431. package/publications/super-admin.d.ts +2 -0
  432. package/publications/super-admin.js +16 -0
  433. package/publications/super-admin.js.map +1 -0
  434. package/publications/user-groups.d.ts +1 -0
  435. package/publications/user-groups.js +16 -0
  436. package/publications/user-groups.js.map +1 -0
  437. package/publications/user-guides.d.ts +1 -0
  438. package/publications/user-guides.js +16 -0
  439. package/publications/user-guides.js.map +1 -0
  440. package/resolveio-server-app.d.ts +70 -0
  441. package/resolveio-server-app.js +801 -0
  442. package/resolveio-server-app.js.map +1 -0
  443. package/server-app.d.ts +228 -0
  444. package/server-app.js +3566 -0
  445. package/server-app.js.map +1 -0
  446. package/services/codex-client.d.ts +128 -0
  447. package/services/codex-client.js +1629 -0
  448. package/services/codex-client.js.map +1 -0
  449. package/services/openai-client.d.ts +46 -0
  450. package/services/openai-client.js +318 -0
  451. package/services/openai-client.js.map +1 -0
  452. package/types/error-report.d.ts +25 -0
  453. package/types/error-report.js +4 -0
  454. package/types/error-report.js.map +1 -0
  455. package/types/slow-query-report.d.ts +27 -0
  456. package/types/slow-query-report.js +6 -0
  457. package/types/slow-query-report.js.map +1 -0
  458. package/util/ai-qa-policy.d.ts +124 -0
  459. package/util/ai-qa-policy.js +736 -0
  460. package/util/ai-qa-policy.js.map +1 -0
  461. package/util/ai-run-evidence-adapters.d.ts +109 -0
  462. package/util/ai-run-evidence-adapters.js +7234 -0
  463. package/util/ai-run-evidence-adapters.js.map +1 -0
  464. package/util/ai-run-evidence-dashboard.d.ts +88 -0
  465. package/util/ai-run-evidence-dashboard.js +343 -0
  466. package/util/ai-run-evidence-dashboard.js.map +1 -0
  467. package/util/ai-run-evidence-eval.d.ts +86 -0
  468. package/util/ai-run-evidence-eval.js +1018 -0
  469. package/util/ai-run-evidence-eval.js.map +1 -0
  470. package/util/ai-run-evidence.d.ts +244 -0
  471. package/util/ai-run-evidence.js +1096 -0
  472. package/util/ai-run-evidence.js.map +1 -0
  473. package/util/ai-runner-artifacts.d.ts +82 -0
  474. package/util/ai-runner-artifacts.js +713 -0
  475. package/util/ai-runner-artifacts.js.map +1 -0
  476. package/util/ai-runner-manager-autopilot.d.ts +210 -0
  477. package/util/ai-runner-manager-autopilot.js +642 -0
  478. package/util/ai-runner-manager-autopilot.js.map +1 -0
  479. package/util/ai-runner-manager-policy.d.ts +807 -0
  480. package/util/ai-runner-manager-policy.js +3501 -0
  481. package/util/ai-runner-manager-policy.js.map +1 -0
  482. package/util/ai-runner-qa-auth.d.ts +5 -0
  483. package/util/ai-runner-qa-auth.js +839 -0
  484. package/util/ai-runner-qa-auth.js.map +1 -0
  485. package/util/ai-runner-qa-tools.d.ts +26 -0
  486. package/util/ai-runner-qa-tools.js +3520 -0
  487. package/util/ai-runner-qa-tools.js.map +1 -0
  488. package/util/aicoder-runner-v6.d.ts +426 -0
  489. package/util/aicoder-runner-v6.js +2464 -0
  490. package/util/aicoder-runner-v6.js.map +1 -0
  491. package/util/common.d.ts +31 -0
  492. package/util/common.js +683 -0
  493. package/util/common.js.map +1 -0
  494. package/util/customer-portal-password.d.ts +13 -0
  495. package/util/customer-portal-password.js +209 -0
  496. package/util/customer-portal-password.js.map +1 -0
  497. package/util/error-reporter.d.ts +52 -0
  498. package/util/error-reporter.js +326 -0
  499. package/util/error-reporter.js.map +1 -0
  500. package/util/error-tracking.d.ts +13 -0
  501. package/util/error-tracking.js +120 -0
  502. package/util/error-tracking.js.map +1 -0
  503. package/util/openai-usage-cost.d.ts +6 -0
  504. package/util/openai-usage-cost.js +103 -0
  505. package/util/openai-usage-cost.js.map +1 -0
  506. package/util/report-builder-unwinds.d.ts +15 -0
  507. package/util/report-builder-unwinds.js +156 -0
  508. package/util/report-builder-unwinds.js.map +1 -0
  509. package/util/runner-process-janitor.d.ts +27 -0
  510. package/util/runner-process-janitor.js +208 -0
  511. package/util/runner-process-janitor.js.map +1 -0
  512. package/util/schema-report-builder.d.ts +6 -0
  513. package/util/schema-report-builder.js +481 -0
  514. package/util/schema-report-builder.js.map +1 -0
  515. package/util/slow-query-reporter.d.ts +28 -0
  516. package/util/slow-query-reporter.js +226 -0
  517. package/util/slow-query-reporter.js.map +1 -0
  518. package/util/subscription-dependency-context.d.ts +34 -0
  519. package/util/subscription-dependency-context.js +1283 -0
  520. package/util/subscription-dependency-context.js.map +1 -0
  521. package/util/support-runner-v5.d.ts +1426 -0
  522. package/util/support-runner-v5.js +7643 -0
  523. package/util/support-runner-v5.js.map +1 -0
  524. package/util/tokenizer.d.ts +5 -0
  525. package/util/tokenizer.js +41 -0
  526. package/util/tokenizer.js.map +1 -0
  527. package/workers/codex-runner.worker.d.ts +1 -0
  528. package/workers/codex-runner.worker.js +192 -0
  529. package/workers/codex-runner.worker.js.map +1 -0
  530. package/.nodemon.json +0 -5
  531. package/.vscode/settings.json +0 -21
  532. package/AGENTS.md +0 -195
  533. package/README.md +0 -22
  534. package/build_package.sh +0 -5
  535. package/compileDTS.pl +0 -64
  536. package/docs/ai-assistant-nightly-eval.md +0 -65
  537. package/docs/ai-assistant-preflight-checklist.md +0 -23
  538. package/docs/ai-assistant-report-builder-bridge-playbook.md +0 -115
  539. package/eslint-plugin-custom/index.js +0 -7
  540. package/eslint-plugin-custom/rules/no-filter-zero-index.js +0 -44
  541. package/eslint.config.js +0 -103
  542. package/gulpfile.js +0 -216
  543. package/methodAndPublicationListGenerator.py +0 -375
  544. package/mongodbensurers.js +0 -2
  545. package/mongostop.js +0 -3
  546. package/scripts/cleanup-bypassed-callmethod-logs.js +0 -616
  547. package/settings.development.json +0 -25
  548. package/settings.development.redacted.json +0 -25
  549. package/src/.env +0 -12
  550. package/src/ai/assistant-core-heuristics.ts +0 -379
  551. package/src/ai/resolveio-platform-intelligence-memory-corpus.ts +0 -185
  552. package/src/ai/resolveio-platform-intelligence-memory.ts +0 -325
  553. package/src/ai/resolveio-platform-intelligence.ts +0 -462
  554. package/src/client-server-app.ts +0 -12
  555. package/src/collections/ai-run.collection.ts +0 -117
  556. package/src/collections/ai-terminal-conversation.collection.ts +0 -91
  557. package/src/collections/ai-terminal-issue-report.collection.ts +0 -99
  558. package/src/collections/ai-terminal-message.collection.ts +0 -77
  559. package/src/collections/app-setting.collection.ts +0 -104
  560. package/src/collections/app-status.collection.ts +0 -58
  561. package/src/collections/communication-metric.collection.ts +0 -84
  562. package/src/collections/counter.collection.ts +0 -56
  563. package/src/collections/cron-job-history.collection.ts +0 -94
  564. package/src/collections/cron-job.collection.ts +0 -92
  565. package/src/collections/customer-notification.collection.ts +0 -131
  566. package/src/collections/customer-portal-password.collection.ts +0 -76
  567. package/src/collections/email-history.collection.ts +0 -134
  568. package/src/collections/email-verified.collection.ts +0 -62
  569. package/src/collections/file.collection.ts +0 -74
  570. package/src/collections/flag-update.collection.ts +0 -57
  571. package/src/collections/flag.collection.ts +0 -57
  572. package/src/collections/log-method-latency.collection.ts +0 -77
  573. package/src/collections/log-subscription.collection.ts +0 -80
  574. package/src/collections/log.collection.ts +0 -93
  575. package/src/collections/logged-in-users.collection.ts +0 -67
  576. package/src/collections/monitor-cpu.collection.ts +0 -65
  577. package/src/collections/monitor-function.collection.ts +0 -74
  578. package/src/collections/monitor-memory.collection.ts +0 -77
  579. package/src/collections/monitor-mongo.collection.ts +0 -71
  580. package/src/collections/notification.collection.ts +0 -57
  581. package/src/collections/openai-usage-ledger.collection.ts +0 -131
  582. package/src/collections/report-builder-dashboard-builder.collection.ts +0 -109
  583. package/src/collections/report-builder-library.collection.ts +0 -89
  584. package/src/collections/report-builder-report.collection.ts +0 -184
  585. package/src/collections/user-group.collection.ts +0 -89
  586. package/src/collections/user-guide.collection.ts +0 -57
  587. package/src/collections/user.collection.ts +0 -181
  588. package/src/cron/cron.ts +0 -117
  589. package/src/fixtures/cron-jobs.ts +0 -95
  590. package/src/fixtures/init.ts +0 -35
  591. package/src/http/auth.ts +0 -818
  592. package/src/http/health.ts +0 -7
  593. package/src/http/home.ts +0 -90
  594. package/src/http/slow-query-publication.ts +0 -49
  595. package/src/index.ts +0 -1
  596. package/src/managers/ai-assistant-codex-manager.manager.ts +0 -1131
  597. package/src/managers/ai-run-evidence.manager.ts +0 -264
  598. package/src/managers/communication-metric.manager.ts +0 -82
  599. package/src/managers/cron.manager.ts +0 -333
  600. package/src/managers/customer-notification-content.manager.ts +0 -236
  601. package/src/managers/diagnostic-manager-bootstrap.ts +0 -165
  602. package/src/managers/error-auto-fix.manager.ts +0 -2767
  603. package/src/managers/local-log.manager.ts +0 -113
  604. package/src/managers/method.manager.ts +0 -1857
  605. package/src/managers/mongo.manager.ts +0 -4575
  606. package/src/managers/monitor.manager.ts +0 -507
  607. package/src/managers/openai-usage-ledger.manager.ts +0 -112
  608. package/src/managers/slow-query-verifier.manager.ts +0 -3590
  609. package/src/managers/slow-query.manager.ts +0 -519
  610. package/src/managers/subscription.manager.ts +0 -3128
  611. package/src/managers/websocket.manager.ts +0 -746
  612. package/src/managers/worker-dispatcher.manager.ts +0 -1360
  613. package/src/managers/worker-server.manager.ts +0 -536
  614. package/src/methods/accounts.ts +0 -532
  615. package/src/methods/ai-terminal.ts +0 -29070
  616. package/src/methods/app-settings.ts +0 -114
  617. package/src/methods/aws.ts +0 -649
  618. package/src/methods/collections.ts +0 -641
  619. package/src/methods/counters.ts +0 -69
  620. package/src/methods/cron-jobs.ts +0 -2614
  621. package/src/methods/customer-notifications.ts +0 -458
  622. package/src/methods/diagnostics.ts +0 -616
  623. package/src/methods/flag-updates.ts +0 -7
  624. package/src/methods/flags.ts +0 -7
  625. package/src/methods/logs.ts +0 -657
  626. package/src/methods/mongo-explorer.ts +0 -1880
  627. package/src/methods/monitor.ts +0 -540
  628. package/src/methods/pdf.ts +0 -1236
  629. package/src/methods/publications.ts +0 -129
  630. package/src/methods/report-builder.ts +0 -3300
  631. package/src/methods/support.ts +0 -335
  632. package/src/models/ai-run.model.ts +0 -27
  633. package/src/models/ai-terminal-conversation.model.ts +0 -19
  634. package/src/models/ai-terminal-issue-report.model.ts +0 -21
  635. package/src/models/ai-terminal-message.model.ts +0 -24
  636. package/src/models/app-setting.model.ts +0 -17
  637. package/src/models/collection-document.model.ts +0 -24
  638. package/src/models/communication-metric.model.ts +0 -23
  639. package/src/models/cron-job-history.model.ts +0 -16
  640. package/src/models/cron-job.model.ts +0 -15
  641. package/src/models/customer-notification.model.ts +0 -28
  642. package/src/models/customer-portal-password.model.ts +0 -12
  643. package/src/models/dialog.model.ts +0 -25
  644. package/src/models/log-method-latency.model.ts +0 -11
  645. package/src/models/log.model.ts +0 -19
  646. package/src/models/method.model.ts +0 -25
  647. package/src/models/monitor-function.model.ts +0 -16
  648. package/src/models/monitor-memory.model.ts +0 -17
  649. package/src/models/monitor-mongo.model.ts +0 -15
  650. package/src/models/openai-usage-ledger.model.ts +0 -56
  651. package/src/models/pagination.model.ts +0 -35
  652. package/src/models/permission.model.ts +0 -14
  653. package/src/models/report-builder-dashboard-builder.model.ts +0 -29
  654. package/src/models/report-builder-library.model.ts +0 -20
  655. package/src/models/report-builder-report.model.ts +0 -136
  656. package/src/models/report-builder.model.ts +0 -68
  657. package/src/models/select-data-label.model.ts +0 -9
  658. package/src/models/server-message.model.ts +0 -31
  659. package/src/models/slow-query-report.model.ts +0 -23
  660. package/src/models/subscription.model.ts +0 -73
  661. package/src/models/support-ticket.model.ts +0 -104
  662. package/src/models/user-group.model.ts +0 -24
  663. package/src/models/user.model.ts +0 -96
  664. package/src/private/images/ResolveIO.png +0 -0
  665. package/src/publications/ai-terminal.ts +0 -73
  666. package/src/publications/app-settings.ts +0 -25
  667. package/src/publications/app-status.ts +0 -13
  668. package/src/publications/cron-jobs.ts +0 -40
  669. package/src/publications/customer-notifications.ts +0 -101
  670. package/src/publications/files.ts +0 -33
  671. package/src/publications/flags-update.ts +0 -19
  672. package/src/publications/flags.ts +0 -19
  673. package/src/publications/logs.ts +0 -163
  674. package/src/publications/notifications.ts +0 -13
  675. package/src/publications/report-builder-dashboard-builders.ts +0 -39
  676. package/src/publications/report-builder-libraries.ts +0 -41
  677. package/src/publications/report-builder-reports.ts +0 -47
  678. package/src/publications/super-admin.ts +0 -13
  679. package/src/publications/user-groups.ts +0 -12
  680. package/src/publications/user-guides.ts +0 -12
  681. package/src/resolveio-server-app.ts +0 -617
  682. package/src/server-app.ts +0 -3354
  683. package/src/services/codex-client.ts +0 -1231
  684. package/src/services/openai-client.ts +0 -265
  685. package/src/types/error-report.ts +0 -26
  686. package/src/types/js-tiktoken.d.ts +0 -11
  687. package/src/types/slow-query-report.ts +0 -28
  688. package/src/util/ai-qa-policy.ts +0 -925
  689. package/src/util/ai-run-evidence-adapters.ts +0 -8347
  690. package/src/util/ai-run-evidence-dashboard.ts +0 -323
  691. package/src/util/ai-run-evidence-eval.ts +0 -1057
  692. package/src/util/ai-run-evidence.ts +0 -1430
  693. package/src/util/ai-runner-artifacts.ts +0 -586
  694. package/src/util/ai-runner-manager-autopilot.ts +0 -961
  695. package/src/util/ai-runner-manager-policy.ts +0 -5011
  696. package/src/util/ai-runner-qa-auth.ts +0 -838
  697. package/src/util/ai-runner-qa-tools.ts +0 -3536
  698. package/src/util/aicoder-runner-v6.ts +0 -3121
  699. package/src/util/common.ts +0 -649
  700. package/src/util/customer-portal-password.ts +0 -183
  701. package/src/util/error-reporter.ts +0 -332
  702. package/src/util/error-tracking.ts +0 -79
  703. package/src/util/openai-usage-cost.ts +0 -114
  704. package/src/util/report-builder-unwinds.ts +0 -180
  705. package/src/util/runner-process-janitor.ts +0 -219
  706. package/src/util/schema-report-builder.ts +0 -448
  707. package/src/util/slow-query-reporter.ts +0 -216
  708. package/src/util/subscription-dependency-context.ts +0 -1096
  709. package/src/util/support-runner-v5.ts +0 -10040
  710. package/src/util/tokenizer.ts +0 -38
  711. package/src/workers/codex-runner.worker.ts +0 -142
  712. package/start_server.sh +0 -5
  713. package/tests/ai-assistant-corpus-build.ts +0 -484
  714. package/tests/ai-assistant-corpus-replay-e2e.ts +0 -774
  715. package/tests/ai-assistant-data-parity-e2e.ts +0 -1989
  716. package/tests/ai-assistant-eval-triage.ts +0 -831
  717. package/tests/ai-assistant-openai-e2e.ts +0 -1061
  718. package/tests/ai-assistant-openai-git-e2e.ts +0 -155
  719. package/tests/ai-assistant-preflight-matrix.ts +0 -215
  720. package/tests/ai-assistant-routing-eval.test.ts +0 -585
  721. package/tests/ai-assistant-snf-live-eval.ts +0 -975
  722. package/tests/ai-assistant-utils.test.ts +0 -4834
  723. package/tests/ai-manager-autopilot-snapshot.test.ts +0 -193
  724. package/tests/ai-manager-recovery-checkpoint.test.ts +0 -1383
  725. package/tests/ai-run-eval.test.ts +0 -132
  726. package/tests/ai-run-evidence.test.ts +0 -3773
  727. package/tests/ai-runner-contract.test.ts +0 -515
  728. package/tests/aicoder-runner-v6.test.ts +0 -822
  729. package/tests/error-reporter.test.ts +0 -145
  730. package/tests/method-publication-generator.test.ts +0 -46
  731. package/tests/report-builder-linking.test.ts +0 -79
  732. package/tests/resolveio-platform-intelligence.test.ts +0 -352
  733. package/tests/server-app-cron-owner.test.ts +0 -127
  734. package/tests/subscription-connect-race.test.ts +0 -158
  735. package/tests/subscription-dependency-context.test.ts +0 -324
  736. package/tests/subscription-manager-collection-tracking.test.ts +0 -86
  737. package/tests/subscription-manager-invalidation.test.ts +0 -86
  738. package/tests/support-runner-v5.test.ts +0 -3201
  739. package/tsconfig.json +0 -34
  740. /package/{src/private → private}/email-templates/enrollment.html +0 -0
  741. /package/{src/private → private}/email-templates/forgot-password.html +0 -0
  742. /package/{src/private → private}/email-templates/support-ticket-deleted.html +0 -0
  743. /package/{src/private → private}/email-templates/support-ticket-modified.html +0 -0
  744. /package/{src/private → private}/email-templates/support-ticket.html +0 -0
  745. /package/{src/public_api.ts → public_api.d.ts} +0 -0
@@ -1,3590 +0,0 @@
1
- import { createHash } from 'crypto';
2
- import { ResolveIOServer } from '../resolveio-server-app';
3
- import { pad, round } from '../util/common';
4
- import { MongoClient } from 'mongodb';
5
- import { Users } from '../collections/user.collection';
6
- import { AppSettings } from '../collections/app-setting.collection';
7
- import { buildGeneratedSlowQuerySummary, buildResolveIOProjectSlowQueryDetails } from './customer-notification-content.manager';
8
-
9
- type AIDashboardJob = Record<string, any>;
10
- type AICoderAppModel = Record<string, any>;
11
- type ClientDBModel = Record<string, any>;
12
- type SlowQueryExecutionOverride = Record<string, any>;
13
- type SlowQueryVerificationStatus = 'pending' | 'passed' | 'failed';
14
- type SlowQueryVerificationRun = Record<string, any>;
15
- type SlowQueryLogModel = Record<string, any>;
16
-
17
- export interface TokenEligibilityResult {
18
- eligible: boolean;
19
- reason?: string;
20
- }
21
-
22
- export type CheckAICoderTokenEligibilityFn = Function;
23
-
24
- export interface SlowQueryVerifierDependencies {
25
- ClientDBs?: any;
26
- Clients?: any;
27
- SlowQueryLogs: any;
28
- AICoderApps?: any;
29
- AIDashboardJobs?: any;
30
- checkAICoderTokenEligibility?: CheckAICoderTokenEligibilityFn;
31
- }
32
-
33
- const OPTIONAL_COLLECTION = {
34
- findOne: () => Promise.resolve(null),
35
- find: () => Promise.resolve([])
36
- };
37
-
38
- let AICoderApps: any = OPTIONAL_COLLECTION;
39
- let AIDashboardJobs: any = OPTIONAL_COLLECTION;
40
- let ClientDBs: any = OPTIONAL_COLLECTION;
41
- let Clients: any = OPTIONAL_COLLECTION;
42
- let SlowQueryLogs: any = null;
43
- const DEFAULT_CHECK_AICODER_TOKEN_ELIGIBILITY: CheckAICoderTokenEligibilityFn = () => {
44
- return Promise.resolve({
45
- eligible: true
46
- });
47
- };
48
- let checkAICoderTokenEligibility: CheckAICoderTokenEligibilityFn = DEFAULT_CHECK_AICODER_TOKEN_ELIGIBILITY;
49
- let configuredSlowQueryVerifierDependencies: SlowQueryVerifierDependencies | null = null;
50
-
51
- function resolveSlowQueryVerifierDependencies(
52
- overrides?: Partial<SlowQueryVerifierDependencies>
53
- ): SlowQueryVerifierDependencies {
54
- const resolved: SlowQueryVerifierDependencies = {
55
- ...(configuredSlowQueryVerifierDependencies || {} as SlowQueryVerifierDependencies),
56
- ...(overrides || {})
57
- };
58
-
59
- if (!resolved.SlowQueryLogs) {
60
- throw new Error('SlowQueryVerifier dependencies are not configured.');
61
- }
62
-
63
- return resolved;
64
- }
65
-
66
- function applySlowQueryVerifierDependencies(dependencies: SlowQueryVerifierDependencies): void {
67
- configuredSlowQueryVerifierDependencies = dependencies;
68
- ClientDBs = dependencies.ClientDBs || OPTIONAL_COLLECTION;
69
- Clients = dependencies.Clients || OPTIONAL_COLLECTION;
70
- SlowQueryLogs = dependencies.SlowQueryLogs;
71
- AICoderApps = dependencies.AICoderApps || OPTIONAL_COLLECTION;
72
- AIDashboardJobs = dependencies.AIDashboardJobs || OPTIONAL_COLLECTION;
73
- checkAICoderTokenEligibility = dependencies.checkAICoderTokenEligibility || DEFAULT_CHECK_AICODER_TOKEN_ELIGIBILITY;
74
- }
75
-
76
- export function registerSlowQueryVerifierDependencies(dependencies: SlowQueryVerifierDependencies): void {
77
- applySlowQueryVerifierDependencies(dependencies);
78
- ensureSlowQueryVerifier();
79
- }
80
-
81
- const VERIFICATION_CHECK_INTERVAL_MS = 60 * 1000;
82
- const VERIFICATION_INTERVAL_MS = 5 * 60 * 1000;
83
- const VERIFICATION_ATTEMPTS = 3;
84
- const VERIFICATION_THRESHOLD_MS = 2000;
85
- const AUTO_OPTIMIZE_DEFAULT_IMPROVEMENT_RATIO = 0.7;
86
- const AUTO_OPTIMIZE_DEFAULT_RETURNED_DOC_TOLERANCE = 0.15;
87
- const AUTO_OPTIMIZE_DEFAULT_MAX_ATTEMPTS_PER_QUERY = 3;
88
- const AUTO_OPTIMIZE_DEFAULT_COOLDOWN_MINUTES = 120;
89
- const AUTO_OPTIMIZE_DEFAULT_MAX_ATTEMPTS_PER_FINGERPRINT = 4;
90
- const AUTO_OPTIMIZE_DEFAULT_FINGERPRINT_WINDOW_HOURS = 24;
91
- const AUTO_OPTIMIZE_DEFAULT_OUTPUT_COMPARE_MAX_DOCS = 10000;
92
- const DEFAULT_ERROR_ALERT_EMAIL = 'dev@resolveio.com';
93
- const MAX_LOCAL_NOTIFICATION_USERS = 5000;
94
-
95
- interface ExplainStageSummary {
96
- stage: string;
97
- path: string;
98
- executionTimeMs?: number;
99
- docsExamined?: number;
100
- keysExamined?: number;
101
- nReturned?: number;
102
- }
103
-
104
- export function ensureSlowQueryVerifier(serverConfig?: any): SlowQueryVerifier | null {
105
- const existing = ResolveIOServer['SlowQueryVerifier'] as SlowQueryVerifier | null;
106
- if (existing) {
107
- return existing;
108
- }
109
-
110
- if (!configuredSlowQueryVerifierDependencies) {
111
- return null;
112
- }
113
-
114
- const resolvedServerConfig = serverConfig || ResolveIOServer.getServerConfig();
115
- if (!resolvedServerConfig) {
116
- return null;
117
- }
118
-
119
- const verifier = new SlowQueryVerifier(resolvedServerConfig);
120
- ResolveIOServer['SlowQueryVerifier'] = verifier;
121
- return verifier;
122
- }
123
-
124
- interface ExecutionMetrics {
125
- durationMs?: number;
126
- docsExamined?: number;
127
- nReturned?: number;
128
- topStages: ExplainStageSummary[];
129
- }
130
-
131
- interface OutputFingerprint {
132
- explicitSort: boolean;
133
- docsCompared: number;
134
- truncated: boolean;
135
- orderedDigest: string;
136
- unorderedDigest: string;
137
- firstDocDigest: string;
138
- lastDocDigest: string;
139
- durationMs: number;
140
- maxDocs: number;
141
- }
142
-
143
- interface OutputEquivalenceResult {
144
- passed: boolean;
145
- reason: string;
146
- mode: 'ordered' | 'unordered' | 'unknown';
147
- baseline?: OutputFingerprint;
148
- after?: OutputFingerprint;
149
- }
150
-
151
- interface ExplainResult {
152
- durationMs: number;
153
- explainPlan: Record<string, any>;
154
- explainStats: Record<string, any>;
155
- stageSummaries: ExplainStageSummary[];
156
- }
157
-
158
- type SlowQueryVerifierErrorCode =
159
- 'client_db_not_found' |
160
- 'client_db_missing_uri' |
161
- 'client_db_missing_database' |
162
- 'main_db_unavailable' |
163
- 'aggregate_write_stage' |
164
- 'bson_too_large' |
165
- 'collection_missing';
166
-
167
- class SlowQueryVerifierError extends Error {
168
- public readonly code: SlowQueryVerifierErrorCode;
169
-
170
- constructor(code: SlowQueryVerifierErrorCode, message: string) {
171
- super(message);
172
- this.code = code;
173
- }
174
- }
175
-
176
- interface SlowQueryVerifierConfig {
177
- enabled: boolean;
178
- fallbackToMainDB: boolean;
179
- debugLogging: boolean;
180
- configSource: string;
181
- autofixRepoRoot: string;
182
- autofixGithubOwner: string;
183
- autofixGithubRepo: string;
184
- autoOptimizeEnabled: boolean;
185
- autoOptimizeWaitTimeoutMs: number;
186
- autoOptimizeDurationRatioTarget: number;
187
- autoOptimizeDocsRatioTarget: number;
188
- autoOptimizeReturnedDocsTolerance: number;
189
- autoOptimizeMaxAttemptsPerQuery: number;
190
- autoOptimizeCooldownMinutes: number;
191
- autoOptimizeMaxAttemptsPerFingerprint: number;
192
- autoOptimizeFingerprintWindowHours: number;
193
- autoOptimizeRequiredTokens: number;
194
- autoOptimizeOutputCompareEnabled: boolean;
195
- autoOptimizeRequireExactOutput: boolean;
196
- autoOptimizeOutputMismatchRetryCount: number;
197
- autoOptimizeOutputCompareMaxDocs: number;
198
- escalationEmails: string[];
199
- }
200
-
201
- interface ExplainTarget {
202
- type: 'client' | 'main';
203
- dbName: string;
204
- uri?: string;
205
- }
206
-
207
- type ExplainVerbosity = 'queryPlanner' | 'executionStats';
208
-
209
- export class SlowQueryVerifier {
210
- private readonly _timer?: NodeJS.Timeout;
211
- private readonly config: SlowQueryVerifierConfig;
212
- private readonly autoOptimizeInFlight = new Set<string>();
213
- private readonly autoOptimizeDependenciesAvailable: boolean;
214
- private static readonly APP_SETTINGS_CACHE_TTL_MS = 10000;
215
- private appSettingsAutoOptimizeCacheExpiresAt = 0;
216
- private appSettingsAutoOptimizeCacheValue: boolean | null = null;
217
-
218
- constructor(serverConfig?: any, dependencies?: Partial<SlowQueryVerifierDependencies>) {
219
- const resolvedDependencies = resolveSlowQueryVerifierDependencies(dependencies);
220
- applySlowQueryVerifierDependencies(resolvedDependencies);
221
-
222
- this.config = SlowQueryVerifier.resolveConfig(serverConfig);
223
- this.autoOptimizeDependenciesAvailable = !!(resolvedDependencies.AICoderApps && resolvedDependencies.AIDashboardJobs);
224
-
225
- if (this.config.enabled) {
226
- this._timer = setInterval(() => {
227
- // eslint-disable-next-line no-restricted-syntax
228
- this.poll().catch(err => {
229
- console.error('Slow query verifier poll failed', err);
230
- });
231
- }, VERIFICATION_CHECK_INTERVAL_MS);
232
-
233
- // eslint-disable-next-line no-restricted-syntax
234
- this.poll().catch(err => {
235
- console.error('Slow query verifier failed to start', err);
236
- });
237
- }
238
- }
239
-
240
- private static resolveConfig(serverConfig?: any): SlowQueryVerifierConfig {
241
- const slowQueryConfig = (serverConfig && (serverConfig.slowQuery || serverConfig.SLOW_QUERY)) || {};
242
- const verifierConfig = (slowQueryConfig && (slowQueryConfig.verifier || slowQueryConfig.slowQueryVerifier)) || {};
243
- const autofixConfig = (serverConfig && (serverConfig.autofix || serverConfig.AUTOFIX)) || {};
244
-
245
- const getBoolean = (envKey: string, configKey: string, fallback = true) => {
246
- if (typeof process.env[envKey] !== 'undefined') {
247
- return process.env[envKey] === 'true';
248
- }
249
- if (typeof verifierConfig[configKey] !== 'undefined') {
250
- return !!verifierConfig[configKey];
251
- }
252
- return fallback;
253
- };
254
-
255
- const getNumber = (envKey: string, configKey: string, fallback: number): number => {
256
- if (typeof process.env[envKey] !== 'undefined') {
257
- const parsedEnv = Number(process.env[envKey]);
258
- if (Number.isFinite(parsedEnv)) {
259
- return parsedEnv;
260
- }
261
- }
262
- if (typeof verifierConfig[configKey] !== 'undefined') {
263
- const parsedConfig = Number(verifierConfig[configKey]);
264
- if (Number.isFinite(parsedConfig)) {
265
- return parsedConfig;
266
- }
267
- }
268
- return fallback;
269
- };
270
-
271
- const getString = (envKey: string, configKey: string, fallback = ''): string => {
272
- if (typeof process.env[envKey] !== 'undefined') {
273
- return String(process.env[envKey] || '').trim();
274
- }
275
- if (typeof verifierConfig[configKey] !== 'undefined') {
276
- return String(verifierConfig[configKey] || '').trim();
277
- }
278
- if (typeof slowQueryConfig[configKey] !== 'undefined') {
279
- return String(slowQueryConfig[configKey] || '').trim();
280
- }
281
- if (typeof autofixConfig[configKey] !== 'undefined') {
282
- return String(autofixConfig[configKey] || '').trim();
283
- }
284
- return fallback;
285
- };
286
-
287
- const parseEmails = (value: any): string[] => {
288
- if (Array.isArray(value)) {
289
- return value.map(item => `${item || ''}`.trim().toLowerCase()).filter(Boolean);
290
- }
291
- if (typeof value === 'string') {
292
- return value.split(',').map(item => item.trim().toLowerCase()).filter(Boolean);
293
- }
294
- return [];
295
- };
296
-
297
- const clampRatio = (value: number, fallback: number): number => {
298
- if (!Number.isFinite(value)) {
299
- return fallback;
300
- }
301
- if (value <= 0 || value >= 1) {
302
- return fallback;
303
- }
304
- return value;
305
- };
306
-
307
- const normalizePositiveInt = (value: number, fallback: number): number => {
308
- if (!Number.isFinite(value) || value <= 0) {
309
- return fallback;
310
- }
311
- return Math.floor(value);
312
- };
313
-
314
- const normalizeNonNegativeInt = (value: number, fallback = 0): number => {
315
- if (!Number.isFinite(value) || value < 0) {
316
- return fallback;
317
- }
318
- return Math.floor(value);
319
- };
320
-
321
- const escalationEmails = parseEmails(
322
- process.env.SLOW_QUERY_ESCALATION_EMAILS
323
- || verifierConfig.escalationEmails
324
- || slowQueryConfig.escalationEmails
325
- || slowQueryConfig.notifyEmails
326
- );
327
-
328
- return {
329
- enabled: true,
330
- fallbackToMainDB: getBoolean('SLOW_QUERY_VERIFIER_FALLBACK_MAIN_DB', 'fallbackToMainDB', true),
331
- debugLogging: getBoolean('SLOW_QUERY_VERIFIER_DEBUG_LOGS', 'debugLogging', false),
332
- configSource: Object.keys(verifierConfig).length ? 'serverConfig' : 'defaults',
333
- autofixRepoRoot: getString('AUTOFIX_REPO_ROOT', 'repoRoot', '/var/app/current'),
334
- autofixGithubOwner: getString('AUTOFIX_GITHUB_OWNER', 'githubOwner', 'resolveio'),
335
- autofixGithubRepo: getString('AUTOFIX_GITHUB_REPO', 'githubRepo', ''),
336
- autoOptimizeEnabled: getBoolean('SLOW_QUERY_AUTO_OPTIMIZE_ENABLED', 'autoOptimizeEnabled', false),
337
- autoOptimizeWaitTimeoutMs: getNumber('SLOW_QUERY_AUTO_OPTIMIZE_WAIT_TIMEOUT_MS', 'autoOptimizeWaitTimeoutMs', 45 * 60 * 1000),
338
- autoOptimizeDurationRatioTarget: clampRatio(
339
- getNumber('SLOW_QUERY_AUTO_OPTIMIZE_DURATION_RATIO', 'autoOptimizeDurationRatioTarget', AUTO_OPTIMIZE_DEFAULT_IMPROVEMENT_RATIO),
340
- AUTO_OPTIMIZE_DEFAULT_IMPROVEMENT_RATIO
341
- ),
342
- autoOptimizeDocsRatioTarget: clampRatio(
343
- getNumber('SLOW_QUERY_AUTO_OPTIMIZE_DOCS_RATIO', 'autoOptimizeDocsRatioTarget', AUTO_OPTIMIZE_DEFAULT_IMPROVEMENT_RATIO),
344
- AUTO_OPTIMIZE_DEFAULT_IMPROVEMENT_RATIO
345
- ),
346
- autoOptimizeReturnedDocsTolerance: clampRatio(
347
- getNumber('SLOW_QUERY_AUTO_OPTIMIZE_RETURNED_DOCS_TOLERANCE', 'autoOptimizeReturnedDocsTolerance', AUTO_OPTIMIZE_DEFAULT_RETURNED_DOC_TOLERANCE),
348
- AUTO_OPTIMIZE_DEFAULT_RETURNED_DOC_TOLERANCE
349
- ),
350
- autoOptimizeMaxAttemptsPerQuery: normalizePositiveInt(
351
- getNumber('SLOW_QUERY_AUTO_OPTIMIZE_MAX_ATTEMPTS_PER_QUERY', 'autoOptimizeMaxAttemptsPerQuery', AUTO_OPTIMIZE_DEFAULT_MAX_ATTEMPTS_PER_QUERY),
352
- AUTO_OPTIMIZE_DEFAULT_MAX_ATTEMPTS_PER_QUERY
353
- ),
354
- autoOptimizeCooldownMinutes: normalizeNonNegativeInt(
355
- getNumber('SLOW_QUERY_AUTO_OPTIMIZE_COOLDOWN_MINUTES', 'autoOptimizeCooldownMinutes', AUTO_OPTIMIZE_DEFAULT_COOLDOWN_MINUTES),
356
- AUTO_OPTIMIZE_DEFAULT_COOLDOWN_MINUTES
357
- ),
358
- autoOptimizeMaxAttemptsPerFingerprint: normalizePositiveInt(
359
- getNumber(
360
- 'SLOW_QUERY_AUTO_OPTIMIZE_MAX_ATTEMPTS_PER_FINGERPRINT',
361
- 'autoOptimizeMaxAttemptsPerFingerprint',
362
- AUTO_OPTIMIZE_DEFAULT_MAX_ATTEMPTS_PER_FINGERPRINT
363
- ),
364
- AUTO_OPTIMIZE_DEFAULT_MAX_ATTEMPTS_PER_FINGERPRINT
365
- ),
366
- autoOptimizeFingerprintWindowHours: normalizePositiveInt(
367
- getNumber(
368
- 'SLOW_QUERY_AUTO_OPTIMIZE_FINGERPRINT_WINDOW_HOURS',
369
- 'autoOptimizeFingerprintWindowHours',
370
- AUTO_OPTIMIZE_DEFAULT_FINGERPRINT_WINDOW_HOURS
371
- ),
372
- AUTO_OPTIMIZE_DEFAULT_FINGERPRINT_WINDOW_HOURS
373
- ),
374
- autoOptimizeRequiredTokens: normalizeNonNegativeInt(
375
- getNumber('SLOW_QUERY_AUTO_OPTIMIZE_REQUIRED_TOKENS', 'autoOptimizeRequiredTokens', 0),
376
- 0
377
- ),
378
- autoOptimizeOutputCompareEnabled: getBoolean('SLOW_QUERY_AUTO_OPTIMIZE_OUTPUT_COMPARE_ENABLED', 'autoOptimizeOutputCompareEnabled', true),
379
- autoOptimizeRequireExactOutput: getBoolean('SLOW_QUERY_AUTO_OPTIMIZE_REQUIRE_EXACT_OUTPUT', 'autoOptimizeRequireExactOutput', true),
380
- autoOptimizeOutputMismatchRetryCount: normalizeNonNegativeInt(
381
- getNumber('SLOW_QUERY_AUTO_OPTIMIZE_OUTPUT_MISMATCH_RETRY_COUNT', 'autoOptimizeOutputMismatchRetryCount', 2),
382
- 2
383
- ),
384
- autoOptimizeOutputCompareMaxDocs: normalizePositiveInt(
385
- getNumber(
386
- 'SLOW_QUERY_AUTO_OPTIMIZE_OUTPUT_COMPARE_MAX_DOCS',
387
- 'autoOptimizeOutputCompareMaxDocs',
388
- AUTO_OPTIMIZE_DEFAULT_OUTPUT_COMPARE_MAX_DOCS
389
- ),
390
- AUTO_OPTIMIZE_DEFAULT_OUTPUT_COMPARE_MAX_DOCS
391
- ),
392
- escalationEmails
393
- };
394
- }
395
-
396
- private parseBooleanEnv(envKey: string): boolean | null {
397
- if (typeof process.env[envKey] === 'undefined') {
398
- return null;
399
- }
400
- return process.env[envKey] === 'true';
401
- }
402
-
403
- private async resolveAutoOptimizeEnabled(): Promise<boolean> {
404
- if (!this.autoOptimizeDependenciesAvailable) {
405
- return false;
406
- }
407
-
408
- const now = Date.now();
409
- if (this.appSettingsAutoOptimizeCacheValue !== null && now < this.appSettingsAutoOptimizeCacheExpiresAt) {
410
- return this.appSettingsAutoOptimizeCacheValue;
411
- }
412
-
413
- const envValue = this.parseBooleanEnv('SLOW_QUERY_AUTO_OPTIMIZE_ENABLED');
414
- let enabled = envValue !== null ? envValue : !!this.config.autoOptimizeEnabled;
415
- try {
416
- if (AppSettings && typeof AppSettings.findOne === 'function') {
417
- const activeSetting = await AppSettings.findOne({
418
- is_active: {
419
- $ne: false
420
- }
421
- }, {
422
- sort: {
423
- updatedAt: -1,
424
- createdAt: -1
425
- }
426
- }) || await AppSettings.findOne({}, {
427
- sort: {
428
- updatedAt: -1,
429
- createdAt: -1
430
- }
431
- });
432
-
433
- if (activeSetting && typeof activeSetting.enable_slow_query_optimizer === 'boolean') {
434
- enabled = !!activeSetting.enable_slow_query_optimizer;
435
- }
436
- }
437
- }
438
- catch (error) {
439
- if (this.config?.debugLogging) {
440
- console.warn('SlowQueryVerifier failed to read app settings slow-query optimizer toggle', error);
441
- }
442
- }
443
-
444
- this.appSettingsAutoOptimizeCacheValue = enabled;
445
- this.appSettingsAutoOptimizeCacheExpiresAt = now + SlowQueryVerifier.APP_SETTINGS_CACHE_TTL_MS;
446
- return enabled;
447
- }
448
-
449
- private async poll() {
450
- const autoOptimizeEnabled = await this.resolveAutoOptimizeEnabled();
451
- if (!autoOptimizeEnabled) {
452
- return;
453
- }
454
-
455
- const now = new Date();
456
- const candidates = await SlowQueryLogs.find({
457
- ignored: {
458
- $ne: true
459
- },
460
- verification_status: {
461
- $in: [null, 'pending']
462
- },
463
- $or: [
464
- {
465
- verification_next_run_at: {
466
- $exists: false
467
- }
468
- },
469
- {
470
- verification_next_run_at: {
471
- $lte: now
472
- }
473
- }
474
- ]
475
- }, {
476
- sort: {
477
- verification_next_run_at: 1,
478
- last_seen_at: -1
479
- },
480
- limit: 5
481
- });
482
-
483
- for (const candidate of candidates) {
484
- await this.processCandidate(candidate);
485
- }
486
- }
487
-
488
- private async processCandidate(candidate: SlowQueryLogModel) {
489
- if (!candidate || !candidate._id) {
490
- return;
491
- }
492
-
493
- const claimAt = new Date(Date.now() + VERIFICATION_INTERVAL_MS);
494
- await SlowQueryLogs.updateOne({_id: candidate._id}, {
495
- $set: {
496
- verification_next_run_at: claimAt
497
- }
498
- });
499
-
500
- try {
501
- const result = await this.runExplain(candidate);
502
- await this.handleExplainResult(candidate, result);
503
- }
504
- catch (err) {
505
- await this.handleExplainError(candidate, err);
506
- }
507
- }
508
-
509
- public async generateExplainForLog(logId: string): Promise<void> {
510
- if (!logId) {
511
- throw new Error('Slow query log ID is required.');
512
- }
513
-
514
- const log = await SlowQueryLogs.findOne({_id: logId});
515
-
516
- if (!log) {
517
- throw new Error('Slow query log not found.');
518
- }
519
-
520
- try {
521
- const result = await this.runExplain(log);
522
- await this.handleExplainResult(log, result);
523
- }
524
- catch (err) {
525
- await this.handleExplainError(log, err);
526
- throw err;
527
- }
528
- }
529
-
530
- public async runLog(logId: string, options?: { force?: boolean }): Promise<{
531
- status: 'in_progress' | 'success' | 'failed' | 'updated' | 'not_found';
532
- reason?: string;
533
- log?: SlowQueryLogModel;
534
- }> {
535
- if (!logId) {
536
- throw new Error('Slow query log ID is required.');
537
- }
538
-
539
- const existing = await SlowQueryLogs.findOne({_id: logId});
540
- if (!existing) {
541
- return {
542
- status: 'not_found',
543
- reason: 'Slow query log not found.'
544
- };
545
- }
546
-
547
- if (existing.ignored) {
548
- throw new Error('Slow query log is ignored.');
549
- }
550
-
551
- if (this.autoOptimizeInFlight.has(logId) || existing.auto_fix_status === 'running' || existing.status === 'queued') {
552
- return {
553
- status: 'in_progress',
554
- reason: 'Slow query optimization is already running.',
555
- log: existing
556
- };
557
- }
558
-
559
- this.autoOptimizeInFlight.add(logId);
560
- try {
561
- await this.runLogWithRetries(logId, {
562
- force: !!options?.force,
563
- retryOutputMismatch: true
564
- });
565
- }
566
- finally {
567
- this.autoOptimizeInFlight.delete(logId);
568
- }
569
-
570
- const updated = await SlowQueryLogs.findOne({_id: logId});
571
- if (!updated) {
572
- return {
573
- status: 'not_found',
574
- reason: 'Slow query log no longer exists.'
575
- };
576
- }
577
-
578
- if (updated.auto_fix_status === 'running' || updated.status === 'queued') {
579
- return {
580
- status: 'in_progress',
581
- reason: 'Slow query optimization queued.',
582
- log: updated
583
- };
584
- }
585
-
586
- if (updated.auto_fix_status === 'completed' || updated.status === 'optimized') {
587
- return {
588
- status: 'success',
589
- reason: 'Slow query optimization completed.',
590
- log: updated
591
- };
592
- }
593
-
594
- if (updated.auto_fix_status === 'failed') {
595
- return {
596
- status: 'failed',
597
- reason: updated.verification_notes || updated.auto_fix_disabled_reason || 'Slow query optimization failed.',
598
- log: updated
599
- };
600
- }
601
-
602
- return {
603
- status: 'updated',
604
- reason: updated.verification_notes || '',
605
- log: updated
606
- };
607
- }
608
-
609
- public async queueLogRun(logId: string, options?: { force?: boolean }): Promise<{
610
- status: 'in_progress' | 'not_found' | 'failed';
611
- reason?: string;
612
- log?: SlowQueryLogModel;
613
- }> {
614
- if (!logId) {
615
- throw new Error('Slow query log ID is required.');
616
- }
617
-
618
- const existing = await SlowQueryLogs.findOne({_id: logId});
619
- if (!existing) {
620
- return {
621
- status: 'not_found',
622
- reason: 'Slow query log not found.'
623
- };
624
- }
625
-
626
- if (existing.ignored) {
627
- throw new Error('Slow query log is ignored.');
628
- }
629
-
630
- if (this.autoOptimizeInFlight.has(logId) || existing.auto_fix_status === 'running' || existing.status === 'queued') {
631
- return {
632
- status: 'in_progress',
633
- reason: 'Slow query optimization is already running.',
634
- log: existing
635
- };
636
- }
637
-
638
- const queuedAt = new Date();
639
- await SlowQueryLogs.updateOne({_id: logId}, {
640
- $set: {
641
- status: 'queued',
642
- auto_fix_status: 'queued',
643
- verification_notes: 'Slow query optimization queued from super-admin.',
644
- last_triaged_by: 'super-admin',
645
- last_triaged_at: queuedAt
646
- }
647
- });
648
- const queuedLog = (await SlowQueryLogs.findOne({_id: logId})) || existing;
649
-
650
- this.autoOptimizeInFlight.add(logId);
651
- setImmediate(async () => {
652
- try {
653
- await this.runLogWithRetries(logId, {
654
- force: !!options?.force,
655
- retryOutputMismatch: true
656
- });
657
- }
658
- catch (error) {
659
- console.error('Slow query queued run failed', {logId, error: (error as Error)?.message || error});
660
- }
661
- finally {
662
- this.autoOptimizeInFlight.delete(logId);
663
- }
664
- });
665
-
666
- return {
667
- status: 'in_progress',
668
- reason: 'Slow query optimization queued.',
669
- log: queuedLog
670
- };
671
- }
672
-
673
- public async deployLog(logId: string): Promise<{
674
- status: 'success' | 'failed' | 'not_found';
675
- reason?: string;
676
- log?: SlowQueryLogModel;
677
- }> {
678
- if (!logId) {
679
- throw new Error('Slow query log ID is required.');
680
- }
681
-
682
- const log = await SlowQueryLogs.findOne({_id: logId});
683
- if (!log) {
684
- return {
685
- status: 'not_found',
686
- reason: 'Slow query log not found.'
687
- };
688
- }
689
-
690
- const jobId = String(log.openai_task_id || '').trim();
691
- if (!jobId) {
692
- throw new Error('Slow query log does not have a dashboard job id to deploy.');
693
- }
694
-
695
- try {
696
- const job = await this.publishDashboardJob(jobId);
697
- const publishOutcome = this.evaluateDashboardPublishOutcome(job);
698
- const now = new Date();
699
- const existingResult = (log.auto_fix_result && typeof log.auto_fix_result === 'object')
700
- ? {...log.auto_fix_result}
701
- : {};
702
- existingResult.manual_deploy = {
703
- job_id: jobId,
704
- requested_at: now,
705
- success: publishOutcome.success,
706
- message: publishOutcome.message,
707
- branch_name: publishOutcome.branchName || existingResult.publish_branch || ''
708
- };
709
- if (publishOutcome.branchName) {
710
- existingResult.publish_branch = publishOutcome.branchName;
711
- }
712
-
713
- await SlowQueryLogs.updateOne({_id: logId}, {
714
- $set: {
715
- auto_fix_result: existingResult,
716
- verification_notes: publishOutcome.success
717
- ? `Manual deploy completed: ${publishOutcome.message}`
718
- : `Manual deploy failed: ${publishOutcome.message}`,
719
- last_triaged_by: 'super-admin',
720
- last_triaged_at: now
721
- }
722
- });
723
-
724
- const refreshed = (await SlowQueryLogs.findOne({_id: logId})) || log;
725
- return {
726
- status: publishOutcome.success ? 'success' : 'failed',
727
- reason: publishOutcome.message,
728
- log: refreshed
729
- };
730
- }
731
- catch (error) {
732
- const message = (error as Error)?.message || 'Manual deploy failed.';
733
- await SlowQueryLogs.updateOne({_id: logId}, {
734
- $set: {
735
- verification_notes: `Manual deploy failed: ${message}`,
736
- last_triaged_by: 'super-admin',
737
- last_triaged_at: new Date()
738
- }
739
- });
740
- const refreshed = await SlowQueryLogs.findOne({_id: logId});
741
- return {
742
- status: 'failed',
743
- reason: message,
744
- log: refreshed || log
745
- };
746
- }
747
- }
748
-
749
- private async runLogWithRetries(
750
- logId: string,
751
- options: { force: boolean; retryOutputMismatch: boolean }
752
- ): Promise<SlowQueryLogModel | null> {
753
- const retryBudget = options.retryOutputMismatch
754
- ? (Number.isFinite(Number(this.config.autoOptimizeOutputMismatchRetryCount))
755
- ? Number(this.config.autoOptimizeOutputMismatchRetryCount)
756
- : 0)
757
- : 0;
758
- let retriesUsed = 0;
759
-
760
- while (true) {
761
- await this.runAutoOptimization(logId, options.force);
762
- const latest = await SlowQueryLogs.findOne({_id: logId});
763
- if (!latest) {
764
- return null;
765
- }
766
-
767
- if (
768
- !options.retryOutputMismatch
769
- || !this.shouldRetryForOutputMismatch(latest)
770
- || retriesUsed >= retryBudget
771
- ) {
772
- return latest;
773
- }
774
-
775
- const maxAttempts = Number.isFinite(Number(this.config.autoOptimizeMaxAttemptsPerQuery))
776
- ? Number(this.config.autoOptimizeMaxAttemptsPerQuery)
777
- : 0;
778
- const attemptsUsed = Number.isFinite(Number(latest.auto_fix_attempt_count))
779
- ? Number(latest.auto_fix_attempt_count)
780
- : 0;
781
- if (maxAttempts > 0 && attemptsUsed >= maxAttempts) {
782
- return latest;
783
- }
784
-
785
- retriesUsed += 1;
786
- await SlowQueryLogs.updateOne({_id: logId}, {
787
- $set: {
788
- verification_notes: `Output equivalence mismatch detected. Retrying (${retriesUsed}/${retryBudget}).`,
789
- last_triaged_by: 'auto-slow-query',
790
- last_triaged_at: new Date()
791
- }
792
- });
793
- }
794
- }
795
-
796
- private shouldRetryForOutputMismatch(log: SlowQueryLogModel): boolean {
797
- if (!log || log.ignored || log.auto_fix_status !== 'failed') {
798
- return false;
799
- }
800
-
801
- const result = (log.auto_fix_result && typeof log.auto_fix_result === 'object')
802
- ? log.auto_fix_result
803
- : {};
804
- const validation = (result.validation && typeof result.validation === 'object')
805
- ? result.validation
806
- : {};
807
- const outputEquivalence = result.output_equivalence;
808
- const validationReason = String(validation.reason || '').toLowerCase();
809
- const notes = String(log.verification_notes || '').toLowerCase();
810
-
811
- if (outputEquivalence && outputEquivalence.passed === false) {
812
- return true;
813
- }
814
-
815
- return validationReason.includes('output equivalence')
816
- || notes.includes('output equivalence');
817
- }
818
-
819
- private async runExplain(log: SlowQueryLogModel, overrides?: SlowQueryExecutionOverride): Promise<ExplainResult> {
820
- const collectionName = log.collection;
821
-
822
- if (!collectionName) {
823
- throw new Error('Slow query missing collection name.');
824
- }
825
-
826
- const target = await this.resolveExplainTarget(log);
827
- let client: MongoClient | undefined;
828
- let db: any;
829
-
830
- try {
831
- if (target.type === 'client') {
832
- if (!target.uri) {
833
- throw new SlowQueryVerifierError('client_db_missing_uri', 'Client DB missing uri.');
834
- }
835
-
836
- client = await MongoClient.connect(target.uri, {
837
- connectTimeoutMS: 10000,
838
- serverSelectionTimeoutMS: 10000,
839
- readPreference: 'secondaryPreferred'
840
- });
841
- db = client.db(target.dbName);
842
- }
843
- else {
844
- db = ResolveIOServer.getMainDB();
845
- }
846
-
847
- if (!db) {
848
- throw new SlowQueryVerifierError('main_db_unavailable', 'Main server DB is not available.');
849
- }
850
-
851
- const effectiveLog = SlowQueryVerifier.applyQueryOverrides(log, overrides);
852
- const pipeline = SlowQueryVerifier.extractPipelineFromLog(effectiveLog);
853
- const filter = effectiveLog.filter ?? {};
854
- const findOptions = SlowQueryVerifier.extractFindOptions(effectiveLog.options);
855
- const aggregateOptions = SlowQueryVerifier.extractAggregateOptions(effectiveLog.options);
856
- let explainResponse: Record<string, any>;
857
- let usedVerbosity: ExplainVerbosity = 'executionStats';
858
-
859
- const explainAggregate = async (verbosity: ExplainVerbosity): Promise<Record<string, any>> => {
860
- if (SlowQueryVerifier.pipelineHasWriteStage(pipeline as any[])) {
861
- throw new SlowQueryVerifierError('aggregate_write_stage', 'Aggregate pipeline includes a write stage; verification skipped.');
862
- }
863
-
864
- try {
865
- return await db.collection(collectionName)
866
- .aggregate(pipeline, {
867
- ...(aggregateOptions || {}),
868
- readPreference: 'secondaryPreferred'
869
- })
870
- .explain(verbosity);
871
- }
872
- catch (err: any) {
873
- if (SlowQueryVerifier.isAggregateExplainWriteConcernError(err)) {
874
- return await db.command(
875
- SlowQueryVerifier.buildAggregateExplainCommand(collectionName, pipeline as any[], aggregateOptions, verbosity),
876
- {readPreference: 'secondaryPreferred'}
877
- );
878
- }
879
-
880
- throw err;
881
- }
882
- };
883
-
884
- try {
885
- if (pipeline) {
886
- explainResponse = await explainAggregate('executionStats');
887
- }
888
- else {
889
- const cursor = SlowQueryVerifier.buildFindCursor(db.collection(collectionName), filter, findOptions);
890
- explainResponse = await cursor.explain('executionStats');
891
- }
892
- }
893
- catch (err: any) {
894
- const code = err?.code;
895
- const codeName = `${err?.codeName || ''}`.trim();
896
- const message = `${err?.message || ''}`.toLowerCase();
897
-
898
- if (code === 26 || codeName === 'NamespaceNotFound' || message.includes('ns does not exist')) {
899
- throw new SlowQueryVerifierError('collection_missing', `Collection '${collectionName}' not found in ${target.type} DB.`);
900
- }
901
-
902
- if (pipeline) {
903
- if (SlowQueryVerifier.isBsonObjectTooLargeError(err)) {
904
- try {
905
- explainResponse = await explainAggregate('queryPlanner');
906
- usedVerbosity = 'queryPlanner';
907
- }
908
- catch (fallbackErr: any) {
909
- if (SlowQueryVerifier.isBsonObjectTooLargeError(fallbackErr)) {
910
- const durationMs = await SlowQueryVerifier.measureExecution(db, collectionName, pipeline, filter, findOptions, aggregateOptions);
911
- if (!SlowQueryVerifier.isValidDuration(durationMs)) {
912
- throw new SlowQueryVerifierError('bson_too_large', 'Explain/query payload too large to verify.');
913
- }
914
- return {
915
- durationMs,
916
- explainPlan: {},
917
- explainStats: {},
918
- stageSummaries: []
919
- };
920
- }
921
-
922
- throw fallbackErr;
923
- }
924
- }
925
- else {
926
- throw err;
927
- }
928
- }
929
- else {
930
- if (SlowQueryVerifier.isBsonObjectTooLargeError(err)) {
931
- try {
932
- const cursor = SlowQueryVerifier.buildFindCursor(db.collection(collectionName), filter, findOptions);
933
- explainResponse = await cursor.explain('queryPlanner');
934
- usedVerbosity = 'queryPlanner';
935
- }
936
- catch {
937
- const durationMs = await SlowQueryVerifier.measureExecution(db, collectionName, pipeline, filter, findOptions, aggregateOptions);
938
- if (!SlowQueryVerifier.isValidDuration(durationMs)) {
939
- throw new SlowQueryVerifierError('bson_too_large', 'Explain/query payload too large to verify.');
940
- }
941
- return {
942
- durationMs,
943
- explainPlan: {},
944
- explainStats: {},
945
- stageSummaries: []
946
- };
947
- }
948
- }
949
- else {
950
- throw err;
951
- }
952
- }
953
- }
954
-
955
- let durationMs = SlowQueryVerifier.resolveDurationMs(explainResponse);
956
-
957
- if (!SlowQueryVerifier.isValidDuration(durationMs)) {
958
- durationMs = await SlowQueryVerifier.measureExecution(db, collectionName, pipeline, filter, findOptions, aggregateOptions);
959
- }
960
-
961
- const explainPlanRaw = explainResponse?.queryPlanner ?? explainResponse ?? {};
962
- const explainStatsRaw = usedVerbosity === 'queryPlanner' ? {} : (explainResponse?.executionStats ?? {});
963
- const stageSummaries = SlowQueryVerifier.extractStageSummaries(explainResponse, explainStatsRaw);
964
-
965
- const explainPlan = SlowQueryVerifier.sanitizeExplainPayload(SlowQueryVerifier.normalizeExplainPayload(explainPlanRaw));
966
- const explainStats = SlowQueryVerifier.sanitizeExplainPayload(SlowQueryVerifier.normalizeExplainPayload(explainStatsRaw));
967
-
968
- return {
969
- durationMs,
970
- explainPlan,
971
- explainStats,
972
- stageSummaries
973
- };
974
- }
975
- finally {
976
- if (client) {
977
- await client.close();
978
- }
979
- }
980
- }
981
-
982
- public async measureQuery(log: SlowQueryLogModel, overrides?: SlowQueryExecutionOverride): Promise<ExplainResult> {
983
- return this.runExplain(log, overrides);
984
- }
985
-
986
- private async handleExplainResult(log: SlowQueryLogModel, result: ExplainResult) {
987
- if (!log._id) {
988
- return;
989
- }
990
-
991
- const existingRuns = (Array.isArray(log.verification_runs) ? log.verification_runs : [])
992
- .filter(run => SlowQueryVerifier.isValidDuration(run.duration_ms));
993
- const durationValid = SlowQueryVerifier.isValidDuration(result.durationMs);
994
-
995
- if (!durationValid) {
996
- if (this.config.debugLogging) {
997
- console.warn('Slow query verification reported invalid duration; keeping log pending.', log.collection, result.durationMs);
998
- }
999
-
1000
- const previousInvalidAttempts = typeof log.verification_invalid_attempts === 'number'
1001
- ? log.verification_invalid_attempts
1002
- : 0;
1003
- const invalidAttempts = previousInvalidAttempts + 1;
1004
- const shouldFail = invalidAttempts >= VERIFICATION_ATTEMPTS;
1005
-
1006
- const nextRunAt = new Date(Date.now() + VERIFICATION_INTERVAL_MS);
1007
- const updateOps: Record<string, any> = {
1008
- $set: {
1009
- verification_runs: existingRuns,
1010
- verification_status: shouldFail ? 'failed' : 'pending',
1011
- verification_invalid_attempts: invalidAttempts,
1012
- explain_plan: result.explainPlan,
1013
- explain_execution_stats: result.explainStats,
1014
- explain_generated_at: new Date(),
1015
- verification_notes: shouldFail ? 'Verification failed: invalid duration' : 'Explain returned invalid duration'
1016
- }
1017
- };
1018
-
1019
- if (shouldFail) {
1020
- updateOps.$unset = {
1021
- verification_next_run_at: ''
1022
- };
1023
- }
1024
- else {
1025
- updateOps.$set.verification_next_run_at = nextRunAt;
1026
- }
1027
-
1028
- await SlowQueryLogs.updateOne({_id: log._id}, updateOps);
1029
- return;
1030
- }
1031
-
1032
- const runs: SlowQueryVerificationRun[] = existingRuns
1033
- .concat([
1034
- {
1035
- timestamp: new Date(),
1036
- duration_ms: result.durationMs
1037
- }
1038
- ])
1039
- .slice(-VERIFICATION_ATTEMPTS);
1040
-
1041
- const hasFastRun = runs.some(run => run.duration_ms < VERIFICATION_THRESHOLD_MS);
1042
-
1043
- if (hasFastRun) {
1044
- // const durations = runs.map(run => `${run.duration_ms}ms`).join(', ');
1045
- // console.info('Removing slow query log; not consistently slow.', log.collection, durations);
1046
- await SlowQueryLogs.deleteOne({_id: log._id});
1047
- return;
1048
- }
1049
-
1050
- const hasEnoughRuns = runs.length >= VERIFICATION_ATTEMPTS;
1051
- let status: SlowQueryVerificationStatus = hasEnoughRuns ? 'passed' : 'pending';
1052
- let nextRunAt: Date | undefined;
1053
-
1054
- if (!hasEnoughRuns) {
1055
- nextRunAt = new Date(Date.now() + VERIFICATION_INTERVAL_MS);
1056
- }
1057
- const updateOps: Record<string, any> = {
1058
- $set: {
1059
- verification_runs: runs,
1060
- verification_status: status,
1061
- verification_invalid_attempts: 0,
1062
- explain_plan: result.explainPlan,
1063
- explain_execution_stats: result.explainStats,
1064
- explain_generated_at: new Date()
1065
- }
1066
- };
1067
-
1068
- if (nextRunAt) {
1069
- updateOps.$set.verification_next_run_at = nextRunAt;
1070
- }
1071
- else {
1072
- updateOps.$unset = updateOps.$unset || {};
1073
- updateOps.$unset.verification_next_run_at = '';
1074
- }
1075
-
1076
- await SlowQueryLogs.updateOne({_id: log._id}, updateOps);
1077
-
1078
- if (status === 'passed') {
1079
- await this.assignSlowQueryCounter(log._id);
1080
- this.scheduleAutoOptimization(log._id);
1081
- }
1082
- }
1083
-
1084
- private async handleExplainError(log: SlowQueryLogModel, err: any) {
1085
- if (!log._id) {
1086
- return;
1087
- }
1088
-
1089
- const message = typeof err === 'string' ? err : err?.message || 'Unknown error';
1090
- const code = err instanceof SlowQueryVerifierError ? err.code : null;
1091
- const permanentFailure = !!code && ['client_db_not_found', 'client_db_missing_uri', 'client_db_missing_database', 'main_db_unavailable', 'aggregate_write_stage', 'bson_too_large', 'collection_missing'].includes(code);
1092
-
1093
- if (this.config.debugLogging) {
1094
- console.error('Slow query verification error for', log.collection, message);
1095
- }
1096
- else if (!permanentFailure) {
1097
- console.warn('Slow query verification error for', log.collection, message);
1098
- }
1099
-
1100
- if (permanentFailure) {
1101
- await SlowQueryLogs.updateOne({_id: log._id}, {
1102
- $set: {
1103
- verification_status: 'failed',
1104
- verification_notes: `Verification failed: ${message}`
1105
- },
1106
- $unset: {
1107
- verification_next_run_at: ''
1108
- }
1109
- });
1110
- return;
1111
- }
1112
-
1113
- const nextRunAt = new Date(Date.now() + VERIFICATION_INTERVAL_MS);
1114
- await SlowQueryLogs.updateOne({_id: log._id}, {
1115
- $set: {
1116
- verification_next_run_at: nextRunAt,
1117
- verification_notes: `Verification error: ${message}`
1118
- }
1119
- });
1120
- }
1121
-
1122
- private async assignSlowQueryCounter(logId: string) {
1123
- if (!logId) {
1124
- return;
1125
- }
1126
-
1127
- const latest = await SlowQueryLogs.findOne({_id: logId});
1128
-
1129
- if (!latest) {
1130
- return;
1131
- }
1132
-
1133
- const hasNumber = typeof latest.slow_query_count === 'number' && latest.slow_query_count > 0;
1134
-
1135
- if (hasNumber) {
1136
- return;
1137
- }
1138
-
1139
- const counterValue = await ResolveIOServer.getMainServer().getMethodManager().callMethod('incCounter', 'slow_query');
1140
- const counterString = pad(counterValue, 6);
1141
-
1142
- await SlowQueryLogs.updateOne({_id: logId}, {
1143
- $set: {
1144
- slow_query_count: counterValue,
1145
- slow_query_count_string: counterString
1146
- }
1147
- });
1148
- }
1149
-
1150
- private scheduleAutoOptimization(logId: string): void {
1151
- if (!logId) {
1152
- return;
1153
- }
1154
- if (this.autoOptimizeInFlight.has(logId)) {
1155
- return;
1156
- }
1157
-
1158
- this.autoOptimizeInFlight.add(logId);
1159
- this.runAutoOptimizationInBackground(logId);
1160
- }
1161
-
1162
- private runAutoOptimizationInBackground(logId: string): void {
1163
- setImmediate(async () => {
1164
- try {
1165
- await this.runAutoOptimization(logId);
1166
- }
1167
- catch (error) {
1168
- console.error('Slow query auto optimization failed', {logId, error: (error as Error)?.message || error});
1169
- }
1170
- finally {
1171
- this.autoOptimizeInFlight.delete(logId);
1172
- }
1173
- });
1174
- }
1175
-
1176
- private escapeRegex(value: string): string {
1177
- return String(value || '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
1178
- }
1179
-
1180
- private getEscalationRecipients(): string[] {
1181
- const unique = new Set<string>();
1182
- (this.config.escalationEmails || []).forEach((entry) => {
1183
- const normalized = String(entry || '').trim().toLowerCase();
1184
- if (normalized) {
1185
- unique.add(normalized);
1186
- }
1187
- });
1188
- if (!unique.size) {
1189
- unique.add(DEFAULT_ERROR_ALERT_EMAIL);
1190
- }
1191
- return Array.from(unique);
1192
- }
1193
-
1194
- private async sendSlowQueryEscalationNotice(log: SlowQueryLogModel, reason: string): Promise<void> {
1195
- const recipients = this.getEscalationRecipients();
1196
- if (!recipients.length || !log?._id) {
1197
- return;
1198
- }
1199
-
1200
- const subject = `ResolveIO Slow Query Escalation: ${log.slow_query_count_string || log._id}`;
1201
- const body = [
1202
- 'Slow-query auto optimization budget was exhausted and additional autonomous attempts were stopped.',
1203
- '',
1204
- `Reason: ${reason}`,
1205
- `Slow Query Log ID: ${log._id}`,
1206
- `Slow Query Counter: ${log.slow_query_count_string || 'n/a'}`,
1207
- `Collection: ${log.collection || 'unknown'}`,
1208
- `Query Hash: ${log.query_hash || 'n/a'}`,
1209
- `Client: ${log.client_name || log.client_slug || 'unknown'}`,
1210
- `Environment: ${log.environment || 'unknown'}`,
1211
- `Attempts: ${log.auto_fix_attempt_count || 0}/${this.config.autoOptimizeMaxAttemptsPerQuery}`,
1212
- '',
1213
- 'The query has been marked ignored to protect customer AI credits. Manual tuning or explicit allow-list override is required.'
1214
- ].join('\n');
1215
-
1216
- const methodManager = ResolveIOServer.getMainServer().getMethodManager();
1217
- for (const recipient of recipients) {
1218
- try {
1219
- await methodManager.sendEmail(recipient, subject, body);
1220
- }
1221
- catch (error) {
1222
- console.error('Failed sending slow-query escalation email', {recipient, logId: log._id, error});
1223
- }
1224
- }
1225
- }
1226
-
1227
- private async resolveSlowQueryClientTarget(log: SlowQueryLogModel): Promise<{ idClient: string; clientName: string }> {
1228
- const directId = String((log as any)?.id_client || '').trim();
1229
- if (directId) {
1230
- return {
1231
- idClient: directId,
1232
- clientName: String(log?.client_name || '').trim()
1233
- };
1234
- }
1235
-
1236
- const candidates = [
1237
- String(log?.client_slug || '').trim(),
1238
- String(log?.client_name || '').trim(),
1239
- String(log?.source_app || '').trim()
1240
- ].filter(Boolean);
1241
- for (const candidate of candidates) {
1242
- const escaped = this.escapeRegex(candidate);
1243
- const match = await Clients.findOne({
1244
- $or: [
1245
- {name: {$regex: `^${escaped}$`, $options: 'i'}},
1246
- {demo_name: {$regex: `^${escaped}$`, $options: 'i'}},
1247
- {project: {$regex: `^${escaped}$`, $options: 'i'}},
1248
- {repo: {$regex: `^${escaped}$`, $options: 'i'}}
1249
- ]
1250
- }, {
1251
- projection: {
1252
- _id: 1,
1253
- name: 1
1254
- }
1255
- });
1256
- if (match?._id) {
1257
- return {
1258
- idClient: String(match._id || '').trim(),
1259
- clientName: String(match.name || log?.client_name || candidate || '').trim()
1260
- };
1261
- }
1262
- }
1263
-
1264
- return {
1265
- idClient: '',
1266
- clientName: String(log?.client_name || '').trim()
1267
- };
1268
- }
1269
-
1270
- private buildLocalInternalUserCriteria(): Record<string, any> {
1271
- return {
1272
- $and: [
1273
- {
1274
- is_customer: {
1275
- $ne: true
1276
- }
1277
- },
1278
- {
1279
- $nor: [
1280
- {
1281
- 'other.id_customer': {
1282
- $exists: true,
1283
- $nin: ['', null]
1284
- }
1285
- },
1286
- {
1287
- id_customer: {
1288
- $exists: true,
1289
- $nin: ['', null]
1290
- }
1291
- }
1292
- ]
1293
- }
1294
- ]
1295
- };
1296
- }
1297
-
1298
- private buildLocalAdminUserCriteria(): Record<string, any> {
1299
- return {
1300
- $or: [
1301
- {
1302
- 'roles.super_admin': true
1303
- },
1304
- {
1305
- 'roles.groups.name': {
1306
- $regex: 'admin',
1307
- $options: 'i'
1308
- }
1309
- },
1310
- {
1311
- 'roles.groups.views': {
1312
- $regex: '^/manage'
1313
- }
1314
- },
1315
- {
1316
- 'roles.groups.views': '/manage'
1317
- }
1318
- ]
1319
- };
1320
- }
1321
-
1322
- private async resolveLocalNotificationUserIds(isGeneratedApp: boolean): Promise<string[]> {
1323
- const andClauses: Record<string, any>[] = [
1324
- {
1325
- active: true
1326
- },
1327
- {
1328
- username: {
1329
- $exists: true
1330
- }
1331
- }
1332
- ];
1333
-
1334
- if (isGeneratedApp) {
1335
- andClauses.push(this.buildLocalInternalUserCriteria());
1336
- }
1337
- else {
1338
- andClauses.push(this.buildLocalAdminUserCriteria());
1339
- }
1340
-
1341
- const users = await Users.find({
1342
- $and: andClauses
1343
- }, {
1344
- projection: {
1345
- _id: 1
1346
- },
1347
- limit: MAX_LOCAL_NOTIFICATION_USERS
1348
- });
1349
-
1350
- if (!Array.isArray(users) || !users.length) {
1351
- return [];
1352
- }
1353
-
1354
- const unique = new Set<string>();
1355
- for (const user of users) {
1356
- const idUser = String(user?._id || '').trim();
1357
- if (idUser) {
1358
- unique.add(idUser);
1359
- }
1360
- if (unique.size >= MAX_LOCAL_NOTIFICATION_USERS) {
1361
- break;
1362
- }
1363
- }
1364
- return Array.from(unique);
1365
- }
1366
-
1367
- private async isGeneratedAICoderClient(idClient: string): Promise<boolean> {
1368
- const normalizedClientId = String(idClient || '').trim();
1369
- if (!normalizedClientId) {
1370
- return false;
1371
- }
1372
-
1373
- const match = await AICoderApps.findOne({
1374
- client_id: normalizedClientId
1375
- }, {
1376
- projection: {
1377
- _id: 1
1378
- }
1379
- });
1380
-
1381
- return !!match?._id;
1382
- }
1383
-
1384
- private buildAICoderSlowQuerySummary(
1385
- stage: 'detected_auto_optimize_enabled' | 'completed_success' | 'completed_failed',
1386
- log: SlowQueryLogModel,
1387
- extra?: { reason?: string; notes?: string }
1388
- ): string {
1389
- return buildGeneratedSlowQuerySummary(stage, {
1390
- collection: String(log?.collection || '').trim(),
1391
- source_app: String(log?.source_app || '').trim(),
1392
- environment: String(log?.environment || '').trim(),
1393
- baseline_duration_ms: Number((log as any)?.auto_fix_result?.baseline?.durationMs),
1394
- after_duration_ms: Number((log as any)?.auto_fix_result?.after?.durationMs),
1395
- reason: extra?.reason,
1396
- notes: extra?.notes
1397
- }, extra);
1398
- }
1399
-
1400
- private buildResolveIOProjectSlowQueryDetails(log: SlowQueryLogModel, extra?: { reason?: string; notes?: string }): string {
1401
- const runDurations = Array.isArray(log?.verification_runs)
1402
- ? log.verification_runs
1403
- .map(run => Number(run?.duration_ms))
1404
- .filter(duration => Number.isFinite(duration) && duration >= 0)
1405
- : [];
1406
-
1407
- return buildResolveIOProjectSlowQueryDetails({
1408
- collection: log.collection || '',
1409
- client_name: log.client_name || log.client_slug || '',
1410
- source_app: log.source_app || '',
1411
- environment: log.environment || '',
1412
- query_hash: log.query_hash || '',
1413
- baseline_duration_ms: Number((log as any)?.auto_fix_result?.baseline?.durationMs),
1414
- after_duration_ms: Number((log as any)?.auto_fix_result?.after?.durationMs),
1415
- verification_run_durations_ms: runDurations,
1416
- log_id: log._id || '',
1417
- reason: extra?.reason,
1418
- notes: extra?.notes
1419
- });
1420
- }
1421
-
1422
- private async notifyCustomerSlowQueryStatus(
1423
- stage: 'detected_auto_optimize_enabled' | 'completed_success' | 'completed_failed',
1424
- log: SlowQueryLogModel,
1425
- extra?: { reason?: string; notes?: string }
1426
- ): Promise<void> {
1427
- if (!log?._id) {
1428
- return;
1429
- }
1430
-
1431
- const target = await this.resolveSlowQueryClientTarget(log);
1432
- let isGeneratedApp = false;
1433
- if (target.idClient) {
1434
- isGeneratedApp = await this.isGeneratedAICoderClient(target.idClient);
1435
- }
1436
- else {
1437
- const generatedApp = await this.resolveAutoOptimizeApp(log);
1438
- isGeneratedApp = !!generatedApp;
1439
- }
1440
-
1441
- if (!isGeneratedApp && stage !== 'completed_success') {
1442
- return;
1443
- }
1444
-
1445
- const issueKey = String(log.query_hash || log._id || '').trim();
1446
- const dedupeKey = `slow-query:${issueKey}:${stage}`;
1447
- const metadata = {
1448
- log_id: log._id,
1449
- query_hash: log.query_hash || '',
1450
- collection: log.collection || '',
1451
- environment: log.environment || '',
1452
- source_app: log.source_app || '',
1453
- reason: extra?.reason || '',
1454
- notes: extra?.notes || ''
1455
- };
1456
- const targetPayload: Record<string, any> = {};
1457
- if (target.idClient) {
1458
- targetPayload.target_type = isGeneratedApp ? 'client_internal' : 'client_admins';
1459
- targetPayload.id_client = target.idClient;
1460
- targetPayload.client_name = target.clientName || undefined;
1461
- }
1462
- else {
1463
- const idUsers = await this.resolveLocalNotificationUserIds(isGeneratedApp);
1464
- if (!idUsers.length) {
1465
- return;
1466
- }
1467
- targetPayload.target_type = 'users';
1468
- targetPayload.id_users = idUsers;
1469
- targetPayload.client_name = target.clientName || undefined;
1470
- }
1471
-
1472
- let payload: Record<string, any> = {};
1473
- if (isGeneratedApp) {
1474
- if (stage === 'detected_auto_optimize_enabled') {
1475
- payload = {
1476
- title: 'Slow query detected. Auto-optimization is running.',
1477
- message: 'We detected a slow query and started an automatic optimization workflow. We will notify you when it is complete.',
1478
- severity: 'info',
1479
- details: this.buildAICoderSlowQuerySummary(stage, log, extra)
1480
- };
1481
- }
1482
- else if (stage === 'completed_success') {
1483
- payload = {
1484
- title: 'Slow query optimization completed.',
1485
- message: `We optimized slow performance for ${log.collection || 'a collection'} and published the improvement.`,
1486
- severity: 'success',
1487
- details: this.buildAICoderSlowQuerySummary(stage, log, extra)
1488
- };
1489
- }
1490
- else {
1491
- payload = {
1492
- title: 'Slow query needs manual review.',
1493
- message: 'We could not complete automatic optimization for a slow query. Manual tuning is required.',
1494
- severity: 'warning',
1495
- details: this.buildAICoderSlowQuerySummary(stage, log, extra)
1496
- };
1497
- }
1498
- }
1499
- else {
1500
- payload = {
1501
- title: 'ResolveIO project slow query optimized',
1502
- message: `Slow-query optimization completed for ${log.collection || 'a collection'}.`,
1503
- severity: 'success',
1504
- details: this.buildResolveIOProjectSlowQueryDetails(log, extra)
1505
- };
1506
- }
1507
-
1508
- try {
1509
- await ResolveIOServer.getMainServer().getMethodManager().callMethod('createCustomerNotification', {
1510
- ...targetPayload,
1511
- category: 'slow-query',
1512
- source: 'slow-query-auto-optimize',
1513
- source_id: String(log._id || '').trim() || undefined,
1514
- dedupe_key: dedupeKey,
1515
- metadata,
1516
- ...payload
1517
- });
1518
- }
1519
- catch (error) {
1520
- if (this.config.debugLogging) {
1521
- console.warn('Slow query customer notification failed', {
1522
- logId: log._id,
1523
- stage,
1524
- error: error?.message || error
1525
- });
1526
- }
1527
- }
1528
- }
1529
-
1530
- private parseDate(value: any): Date | null {
1531
- if (!value) {
1532
- return null;
1533
- }
1534
- if (value instanceof Date && !Number.isNaN(value.getTime())) {
1535
- return value;
1536
- }
1537
- const parsed = new Date(value);
1538
- return Number.isNaN(parsed.getTime()) ? null : parsed;
1539
- }
1540
-
1541
- private resolveFingerprintScopeKey(log: SlowQueryLogModel): string {
1542
- const explicit = String((log as any)?.fingerprint_scope_key || '').trim();
1543
- if (explicit) {
1544
- return explicit;
1545
- }
1546
-
1547
- const normalize = (value: any): string => {
1548
- const normalized = String(value || '')
1549
- .trim()
1550
- .toLowerCase()
1551
- .replace(/\s+/g, ' ');
1552
- return normalized || '_';
1553
- };
1554
-
1555
- const clientKey = normalize(log?.client_slug || log?.client_name || '');
1556
- const sourceApp = normalize(log?.source_app || '');
1557
- const environment = normalize(log?.environment || '');
1558
- const publication = normalize(log?.publication || '');
1559
- const serverUrl = normalize((log as any)?.server_url || '');
1560
- return [clientKey, sourceApp, environment, publication, serverUrl].join('|');
1561
- }
1562
-
1563
- private buildFingerprintMatchQuery(log: SlowQueryLogModel): Record<string, any> {
1564
- const conditions: Record<string, any>[] = [];
1565
- const collection = String(log?.collection || '').trim();
1566
- if (collection) {
1567
- conditions.push({collection});
1568
- }
1569
-
1570
- const explicitScopeKey = String((log as any)?.fingerprint_scope_key || '').trim();
1571
- const scopeKey = this.resolveFingerprintScopeKey(log);
1572
- if (scopeKey) {
1573
- const legacyScopeQuery: Record<string, any> = {};
1574
- if (String(log?.client_slug || '').trim()) {
1575
- legacyScopeQuery.client_slug = log.client_slug;
1576
- }
1577
- if (String(log?.environment || '').trim()) {
1578
- legacyScopeQuery.environment = log.environment;
1579
- }
1580
- if (String(log?.source_app || '').trim()) {
1581
- legacyScopeQuery.source_app = log.source_app;
1582
- }
1583
- if (String(log?.publication || '').trim()) {
1584
- legacyScopeQuery.publication = log.publication;
1585
- }
1586
- if (String((log as any)?.server_url || '').trim()) {
1587
- legacyScopeQuery.server_url = (log as any).server_url;
1588
- }
1589
- if (explicitScopeKey) {
1590
- conditions.push({fingerprint_scope_key: scopeKey});
1591
- }
1592
- else {
1593
- const scopeCandidates: Record<string, any>[] = [{_id: log?._id || ''}, {fingerprint_scope_key: scopeKey}];
1594
- if (Object.keys(legacyScopeQuery).length) {
1595
- scopeCandidates.push(legacyScopeQuery);
1596
- }
1597
- conditions.push({$or: scopeCandidates});
1598
- }
1599
- }
1600
-
1601
- const canonicalHash = String((log as any)?.canonical_fingerprint_hash || '').trim();
1602
- const queryHash = String(log?.query_hash || '').trim();
1603
- if (canonicalHash && queryHash && canonicalHash !== queryHash) {
1604
- conditions.push({
1605
- $or: [
1606
- {canonical_fingerprint_hash: canonicalHash},
1607
- {
1608
- canonical_fingerprint_hash: {$exists: false},
1609
- query_hash: queryHash
1610
- }
1611
- ]
1612
- });
1613
- }
1614
- else if (canonicalHash) {
1615
- conditions.push({canonical_fingerprint_hash: canonicalHash});
1616
- }
1617
- else if (queryHash) {
1618
- conditions.push({query_hash: queryHash});
1619
- }
1620
- else if (log?._id) {
1621
- conditions.push({_id: log._id});
1622
- }
1623
-
1624
- if (!conditions.length) {
1625
- return {_id: log?._id || ''};
1626
- }
1627
- if (conditions.length === 1) {
1628
- return conditions[0];
1629
- }
1630
- return {$and: conditions};
1631
- }
1632
-
1633
- private countAttemptsWithinWindow(log: SlowQueryLogModel, windowStart: Date): number {
1634
- const history = Array.isArray((log as any)?.auto_fix_attempt_history)
1635
- ? ((log as any).auto_fix_attempt_history as any[])
1636
- : [];
1637
- let attemptsInWindow = 0;
1638
- history.forEach((entry) => {
1639
- const attemptAt = this.parseDate(entry);
1640
- if (attemptAt && attemptAt.getTime() >= windowStart.getTime()) {
1641
- attemptsInWindow += 1;
1642
- }
1643
- });
1644
- if (attemptsInWindow > 0) {
1645
- return attemptsInWindow;
1646
- }
1647
-
1648
- const lastAttemptAt = this.parseDate((log as any)?.auto_fix_last_attempt_at);
1649
- const attemptsUsed = Number.isFinite(Number((log as any)?.auto_fix_attempt_count))
1650
- ? Number((log as any).auto_fix_attempt_count)
1651
- : 0;
1652
- if (lastAttemptAt && lastAttemptAt.getTime() >= windowStart.getTime() && attemptsUsed > 0) {
1653
- return 1;
1654
- }
1655
- return 0;
1656
- }
1657
-
1658
- private async resolveFingerprintAttemptsInWindow(log: SlowQueryLogModel, windowStart: Date): Promise<number> {
1659
- const matchQuery = this.buildFingerprintMatchQuery(log);
1660
- const matches = await SlowQueryLogs.find(matchQuery, {
1661
- limit: 200,
1662
- projection: {
1663
- _id: 1,
1664
- auto_fix_attempt_history: 1,
1665
- auto_fix_attempt_count: 1,
1666
- auto_fix_last_attempt_at: 1
1667
- }
1668
- }) as SlowQueryLogModel[];
1669
-
1670
- let attempts = 0;
1671
- matches.forEach((entry) => {
1672
- attempts += this.countAttemptsWithinWindow(entry, windowStart);
1673
- });
1674
- return attempts;
1675
- }
1676
-
1677
- private resolveCooldownDeadline(log: SlowQueryLogModel): Date | null {
1678
- const cooldownMinutes = Number.isFinite(Number(this.config.autoOptimizeCooldownMinutes))
1679
- ? Number(this.config.autoOptimizeCooldownMinutes)
1680
- : 0;
1681
- if (cooldownMinutes <= 0) {
1682
- return null;
1683
- }
1684
-
1685
- const lastAttemptAt = this.parseDate(log?.auto_fix_last_attempt_at);
1686
- if (!lastAttemptAt) {
1687
- return null;
1688
- }
1689
-
1690
- return new Date(lastAttemptAt.getTime() + (cooldownMinutes * 60 * 1000));
1691
- }
1692
-
1693
- private async markAutoOptimizeCooldownActive(log: SlowQueryLogModel, cooldownUntil: Date): Promise<void> {
1694
- if (!log?._id) {
1695
- return;
1696
- }
1697
- const reason = `Auto optimize cooldown is active until ${cooldownUntil.toISOString()}.`;
1698
- await SlowQueryLogs.updateOne({_id: log._id}, {
1699
- $set: {
1700
- status: 'investigating',
1701
- auto_fix_status: 'failed',
1702
- auto_fix_disabled_reason: reason,
1703
- verification_notes: reason,
1704
- verification_status: 'pending',
1705
- verification_next_run_at: cooldownUntil,
1706
- last_triaged_by: 'auto-slow-query',
1707
- last_triaged_at: new Date()
1708
- }
1709
- });
1710
- }
1711
-
1712
- private async markAutoOptimizeTokenIneligible(log: SlowQueryLogModel, reason: string): Promise<void> {
1713
- if (!log?._id) {
1714
- return;
1715
- }
1716
-
1717
- const message = `Auto optimize blocked by AI credit preflight: ${reason}`;
1718
- await SlowQueryLogs.updateOne({_id: log._id}, {
1719
- $set: {
1720
- status: 'ignored',
1721
- ignored: true,
1722
- auto_fix_status: 'failed',
1723
- auto_fix_disabled_reason: message,
1724
- verification_notes: message,
1725
- last_triaged_by: 'auto-slow-query',
1726
- last_triaged_at: new Date()
1727
- },
1728
- $unset: {
1729
- verification_next_run_at: ''
1730
- }
1731
- });
1732
-
1733
- const refreshed = (await SlowQueryLogs.findOne({_id: log._id})) || log;
1734
- await this.sendSlowQueryEscalationNotice(refreshed, message);
1735
- await this.notifyCustomerSlowQueryStatus('completed_failed', refreshed, {reason: message});
1736
- }
1737
-
1738
- private async markAutoOptimizeBudgetExceeded(log: SlowQueryLogModel, reason: string): Promise<void> {
1739
- if (!log?._id) {
1740
- return;
1741
- }
1742
- const attemptsUsed = Number.isFinite(Number(log.auto_fix_attempt_count))
1743
- ? Number(log.auto_fix_attempt_count)
1744
- : 0;
1745
- const maxAttempts = Number.isFinite(Number(this.config.autoOptimizeMaxAttemptsPerQuery))
1746
- ? Number(this.config.autoOptimizeMaxAttemptsPerQuery)
1747
- : 0;
1748
- const attemptSummary = maxAttempts > 0
1749
- ? `Attempts on this log: ${attemptsUsed}/${maxAttempts}.`
1750
- : `Attempts on this log: ${attemptsUsed}.`;
1751
- const message = `${reason} Query marked ignored pending manual intervention. ${attemptSummary}`;
1752
- const matchQuery = this.buildFingerprintMatchQuery(log);
1753
-
1754
- await SlowQueryLogs.updateMany(matchQuery, {
1755
- $set: {
1756
- status: 'ignored',
1757
- ignored: true,
1758
- auto_fix_status: 'failed',
1759
- auto_fix_disabled_reason: message,
1760
- verification_notes: message,
1761
- verification_status: 'failed',
1762
- last_triaged_by: 'auto-slow-query',
1763
- last_triaged_at: new Date()
1764
- },
1765
- $unset: {
1766
- verification_next_run_at: ''
1767
- }
1768
- });
1769
-
1770
- const refreshed = (await SlowQueryLogs.findOne({_id: log._id})) || log;
1771
- await this.sendSlowQueryEscalationNotice(refreshed, reason);
1772
- await this.notifyCustomerSlowQueryStatus('completed_failed', refreshed, {reason});
1773
- }
1774
-
1775
- private async maybeStopAutoOptimizeAfterFailure(logId: string, reason: string): Promise<void> {
1776
- if (!logId) {
1777
- return;
1778
- }
1779
- const maxAttempts = Number.isFinite(Number(this.config.autoOptimizeMaxAttemptsPerQuery))
1780
- ? Number(this.config.autoOptimizeMaxAttemptsPerQuery)
1781
- : 0;
1782
- if (maxAttempts <= 0) {
1783
- return;
1784
- }
1785
- const latest = await SlowQueryLogs.findOne({_id: logId});
1786
- if (!latest || latest.ignored) {
1787
- return;
1788
- }
1789
- const attemptsUsed = Number.isFinite(Number(latest.auto_fix_attempt_count))
1790
- ? Number(latest.auto_fix_attempt_count)
1791
- : 0;
1792
- if (attemptsUsed < maxAttempts) {
1793
- return;
1794
- }
1795
- await this.markAutoOptimizeBudgetExceeded(latest, reason);
1796
- }
1797
-
1798
- private shouldFallbackDashboardMethod(error: any): boolean {
1799
- const message = `${error?.message || ''}`.toLowerCase();
1800
- return message.includes('not found')
1801
- || message.includes('worker dispatch unavailable')
1802
- || message.includes('no worker')
1803
- || message.includes('unavailable for aidashboard');
1804
- }
1805
-
1806
- private async createDashboardJob(payload: Record<string, any>): Promise<AIDashboardJob> {
1807
- const methodManager = ResolveIOServer.getMainServer().getMethodManager();
1808
- try {
1809
- return await methodManager.callMethod('aiDashboardCreateJob', payload);
1810
- }
1811
- catch (error) {
1812
- if (!this.shouldFallbackDashboardMethod(error)) {
1813
- throw error;
1814
- }
1815
- }
1816
-
1817
- const manager: any = ResolveIOServer['AIDashboardManager'];
1818
- if (manager && manager.isEnabled && manager.isEnabled()) {
1819
- return await manager.createJob(payload);
1820
- }
1821
-
1822
- throw new Error('AI Dashboard manager is not available.');
1823
- }
1824
-
1825
- private async waitForDashboardJobStop(jobId: string, timeoutMs: number): Promise<void> {
1826
- const methodManager = ResolveIOServer.getMainServer().getMethodManager();
1827
- try {
1828
- await methodManager.callMethod('aiDashboardWaitForJobStop', jobId, timeoutMs);
1829
- return;
1830
- }
1831
- catch (error) {
1832
- if (!this.shouldFallbackDashboardMethod(error)) {
1833
- throw error;
1834
- }
1835
- }
1836
-
1837
- const manager: any = ResolveIOServer['AIDashboardManager'];
1838
- if (manager && manager.isEnabled && manager.isEnabled()) {
1839
- await manager.waitForJobStop(jobId, timeoutMs);
1840
- return;
1841
- }
1842
-
1843
- throw new Error('AI Dashboard manager is not available.');
1844
- }
1845
-
1846
- private async isDashboardJobRunning(jobId: string): Promise<boolean> {
1847
- const methodManager = ResolveIOServer.getMainServer().getMethodManager();
1848
- try {
1849
- const running = await methodManager.callMethod('aiDashboardIsJobRunning', jobId);
1850
- return !!running;
1851
- }
1852
- catch (error) {
1853
- if (!this.shouldFallbackDashboardMethod(error)) {
1854
- throw error;
1855
- }
1856
- }
1857
-
1858
- const manager: any = ResolveIOServer['AIDashboardManager'];
1859
- if (manager && manager.isEnabled && manager.isEnabled()) {
1860
- return !!manager.isJobRunning(jobId);
1861
- }
1862
-
1863
- throw new Error('AI Dashboard manager is not available.');
1864
- }
1865
-
1866
- private async publishDashboardJob(jobId: string): Promise<AIDashboardJob> {
1867
- const methodManager = ResolveIOServer.getMainServer().getMethodManager();
1868
- try {
1869
- return await methodManager.callMethod('aiDashboardPublishJob', jobId);
1870
- }
1871
- catch (error) {
1872
- if (!this.shouldFallbackDashboardMethod(error)) {
1873
- throw error;
1874
- }
1875
- }
1876
-
1877
- const manager: any = ResolveIOServer['AIDashboardManager'];
1878
- if (manager && manager.isEnabled && manager.isEnabled() && typeof manager.publishJob === 'function') {
1879
- return await manager.publishJob(jobId);
1880
- }
1881
-
1882
- throw new Error('AI Dashboard manager is not available.');
1883
- }
1884
-
1885
- private evaluateDashboardPublishOutcome(job: AIDashboardJob): {success: boolean; message: string; branchName?: string} {
1886
- const logEntries = Array.isArray(job?.log) ? job.log : [];
1887
- const lastMatch = (predicate): string => {
1888
- for (let i = logEntries.length - 1; i >= 0; i -= 1) {
1889
- if (predicate(logEntries[i] || '')) {
1890
- return logEntries[i];
1891
- }
1892
- }
1893
- return '';
1894
- };
1895
-
1896
- const failureEntry = lastMatch(entry => /publish failed|artifact publish failed|deploy failed/i.test(entry || ''));
1897
- if (failureEntry) {
1898
- return {success: false, message: failureEntry};
1899
- }
1900
-
1901
- const publishEntry = lastMatch(entry => /Published build to /i.test(entry || ''));
1902
- if (publishEntry) {
1903
- const branchMatch = publishEntry.match(/\(([^()]+)\)\.?$/);
1904
- return {
1905
- success: true,
1906
- message: publishEntry,
1907
- branchName: (branchMatch && branchMatch[1]) ? branchMatch[1].trim() : undefined
1908
- };
1909
- }
1910
-
1911
- const skippedEntry = lastMatch(entry => /Publish skipped/i.test(entry || ''));
1912
- if (skippedEntry) {
1913
- return {success: false, message: skippedEntry};
1914
- }
1915
-
1916
- return {
1917
- success: false,
1918
- message: 'Dashboard job completed without a publish confirmation log entry.'
1919
- };
1920
- }
1921
-
1922
- private resolveBaselineDurationMs(log: SlowQueryLogModel): number | undefined {
1923
- const runs = Array.isArray(log.verification_runs) ? log.verification_runs : [];
1924
- for (let i = runs.length - 1; i >= 0; i -= 1) {
1925
- const duration = runs[i]?.duration_ms;
1926
- if (SlowQueryVerifier.isValidDuration(duration)) {
1927
- return duration;
1928
- }
1929
- }
1930
- if (SlowQueryVerifier.isValidDuration(log.duration_ms)) {
1931
- return log.duration_ms;
1932
- }
1933
- if (SlowQueryVerifier.isValidDuration(log.avg_duration_ms)) {
1934
- return log.avg_duration_ms;
1935
- }
1936
- return undefined;
1937
- }
1938
-
1939
- private static collectMetricValues(node: any, keySet: Set<string>, output: number[]): void {
1940
- if (!node || typeof node !== 'object') {
1941
- return;
1942
- }
1943
-
1944
- if (Array.isArray(node)) {
1945
- node.forEach((entry) => SlowQueryVerifier.collectMetricValues(entry, keySet, output));
1946
- return;
1947
- }
1948
-
1949
- Object.keys(node).forEach((key) => {
1950
- const value = (node as any)[key];
1951
- if (keySet.has(key) && typeof value === 'number' && Number.isFinite(value) && value >= 0) {
1952
- output.push(value);
1953
- }
1954
- SlowQueryVerifier.collectMetricValues(value, keySet, output);
1955
- });
1956
- }
1957
-
1958
- private static asNonNegativeNumber(value: any): number | undefined {
1959
- return typeof value === 'number' && Number.isFinite(value) && value >= 0
1960
- ? value
1961
- : undefined;
1962
- }
1963
-
1964
- private static maxNumber(values: Array<number | undefined>): number | undefined {
1965
- const candidates = values.filter((value): value is number => typeof value === 'number');
1966
- return candidates.length ? Math.max(...candidates) : undefined;
1967
- }
1968
-
1969
- private static sortStageSummaries(input: ExplainStageSummary[]): ExplainStageSummary[] {
1970
- return input.slice().sort((left, right) => {
1971
- const timeDiff = (right.executionTimeMs ?? -1) - (left.executionTimeMs ?? -1);
1972
- if (timeDiff !== 0) {
1973
- return timeDiff;
1974
- }
1975
-
1976
- const docsDiff = (right.docsExamined ?? -1) - (left.docsExamined ?? -1);
1977
- if (docsDiff !== 0) {
1978
- return docsDiff;
1979
- }
1980
-
1981
- const keysDiff = (right.keysExamined ?? -1) - (left.keysExamined ?? -1);
1982
- if (keysDiff !== 0) {
1983
- return keysDiff;
1984
- }
1985
-
1986
- return `${left.path}|${left.stage}`.localeCompare(`${right.path}|${right.stage}`);
1987
- });
1988
- }
1989
-
1990
- private static collectExecutionTreeStages(node: any, path: string, output: ExplainStageSummary[]): void {
1991
- if (!node || typeof node !== 'object') {
1992
- return;
1993
- }
1994
-
1995
- const stageName = typeof node.stage === 'string' ? node.stage : 'execution-stage';
1996
- const executionTimeMs = SlowQueryVerifier.maxNumber([
1997
- SlowQueryVerifier.asNonNegativeNumber(node.executionTimeMillis),
1998
- SlowQueryVerifier.asNonNegativeNumber(node.executionTimeMillisEstimate)
1999
- ]);
2000
- const docsExamined = SlowQueryVerifier.maxNumber([
2001
- SlowQueryVerifier.asNonNegativeNumber(node.totalDocsExamined),
2002
- SlowQueryVerifier.asNonNegativeNumber(node.docsExamined)
2003
- ]);
2004
- const keysExamined = SlowQueryVerifier.maxNumber([
2005
- SlowQueryVerifier.asNonNegativeNumber(node.totalKeysExamined),
2006
- SlowQueryVerifier.asNonNegativeNumber(node.keysExamined)
2007
- ]);
2008
- const nReturned = SlowQueryVerifier.maxNumber([
2009
- SlowQueryVerifier.asNonNegativeNumber(node.nReturned)
2010
- ]);
2011
-
2012
- if (
2013
- typeof executionTimeMs === 'number'
2014
- || typeof docsExamined === 'number'
2015
- || typeof keysExamined === 'number'
2016
- || typeof nReturned === 'number'
2017
- ) {
2018
- output.push({
2019
- stage: stageName,
2020
- path: path || 'executionStats.executionStages',
2021
- executionTimeMs,
2022
- docsExamined,
2023
- keysExamined,
2024
- nReturned
2025
- });
2026
- }
2027
-
2028
- const recurse = (child: any, childPath: string) => {
2029
- if (!child) {
2030
- return;
2031
- }
2032
- if (Array.isArray(child)) {
2033
- child.forEach((entry, index) => {
2034
- SlowQueryVerifier.collectExecutionTreeStages(entry, `${childPath}[${index}]`, output);
2035
- });
2036
- return;
2037
- }
2038
- SlowQueryVerifier.collectExecutionTreeStages(child, childPath, output);
2039
- };
2040
-
2041
- recurse(node.inputStage, `${path}.inputStage`);
2042
- recurse(node.inputStages, `${path}.inputStages`);
2043
- recurse(node.executionStages, `${path}.executionStages`);
2044
- recurse(node.outerStage, `${path}.outerStage`);
2045
- recurse(node.innerStage, `${path}.innerStage`);
2046
- recurse(node.leftChild, `${path}.leftChild`);
2047
- recurse(node.rightChild, `${path}.rightChild`);
2048
- recurse(node.thenStage, `${path}.thenStage`);
2049
- recurse(node.elseStage, `${path}.elseStage`);
2050
- recurse(node.shards, `${path}.shards`);
2051
- }
2052
-
2053
- private static extractStageSummaries(explainResponse?: Record<string, any>, explainStats?: Record<string, any>): ExplainStageSummary[] {
2054
- const summaries: ExplainStageSummary[] = [];
2055
- const stages = explainResponse?.stages;
2056
-
2057
- if (Array.isArray(stages)) {
2058
- stages.forEach((stageEntry, index) => {
2059
- if (!stageEntry || typeof stageEntry !== 'object') {
2060
- return;
2061
- }
2062
-
2063
- const stageKey = Object.keys(stageEntry).find(key => key.startsWith('$')) || `stage_${index}`;
2064
- const stagePayload = (stageEntry as any)[stageKey];
2065
- const metricsSource = stageKey === '$cursor' && stagePayload?.executionStats
2066
- ? stagePayload.executionStats
2067
- : stagePayload;
2068
- const executionStages = metricsSource?.executionStages || stagePayload?.executionStats?.executionStages;
2069
- const executionTimeMs = SlowQueryVerifier.maxNumber([
2070
- SlowQueryVerifier.asNonNegativeNumber(metricsSource?.executionTimeMillis),
2071
- SlowQueryVerifier.asNonNegativeNumber(metricsSource?.executionTimeMillisEstimate),
2072
- SlowQueryVerifier.asNonNegativeNumber(executionStages?.executionTimeMillis),
2073
- SlowQueryVerifier.asNonNegativeNumber(executionStages?.executionTimeMillisEstimate)
2074
- ]);
2075
- const docsExamined = SlowQueryVerifier.maxNumber([
2076
- SlowQueryVerifier.asNonNegativeNumber(metricsSource?.totalDocsExamined),
2077
- SlowQueryVerifier.asNonNegativeNumber(metricsSource?.docsExamined),
2078
- SlowQueryVerifier.asNonNegativeNumber(executionStages?.totalDocsExamined),
2079
- SlowQueryVerifier.asNonNegativeNumber(executionStages?.docsExamined)
2080
- ]);
2081
- const keysExamined = SlowQueryVerifier.maxNumber([
2082
- SlowQueryVerifier.asNonNegativeNumber(metricsSource?.totalKeysExamined),
2083
- SlowQueryVerifier.asNonNegativeNumber(metricsSource?.keysExamined),
2084
- SlowQueryVerifier.asNonNegativeNumber(executionStages?.totalKeysExamined),
2085
- SlowQueryVerifier.asNonNegativeNumber(executionStages?.keysExamined)
2086
- ]);
2087
- const nReturned = SlowQueryVerifier.maxNumber([
2088
- SlowQueryVerifier.asNonNegativeNumber(metricsSource?.nReturned),
2089
- SlowQueryVerifier.asNonNegativeNumber(executionStages?.nReturned)
2090
- ]);
2091
-
2092
- if (
2093
- typeof executionTimeMs === 'number'
2094
- || typeof docsExamined === 'number'
2095
- || typeof keysExamined === 'number'
2096
- || typeof nReturned === 'number'
2097
- ) {
2098
- summaries.push({
2099
- stage: stageKey,
2100
- path: `stages[${index}]`,
2101
- executionTimeMs,
2102
- docsExamined,
2103
- keysExamined,
2104
- nReturned
2105
- });
2106
- }
2107
-
2108
- if (executionStages) {
2109
- SlowQueryVerifier.collectExecutionTreeStages(
2110
- executionStages,
2111
- `stages[${index}].${stageKey}.executionStages`,
2112
- summaries
2113
- );
2114
- }
2115
- });
2116
- }
2117
-
2118
- const executionRoot = explainStats?.executionStages
2119
- || explainResponse?.executionStats?.executionStages
2120
- || explainResponse?.executionStats;
2121
- if (executionRoot) {
2122
- SlowQueryVerifier.collectExecutionTreeStages(executionRoot, 'executionStats.executionStages', summaries);
2123
- }
2124
-
2125
- const deduped = new Map<string, ExplainStageSummary>();
2126
- summaries.forEach((summary) => {
2127
- const key = [
2128
- summary.path,
2129
- summary.stage,
2130
- typeof summary.executionTimeMs === 'number' ? summary.executionTimeMs : '',
2131
- typeof summary.docsExamined === 'number' ? summary.docsExamined : '',
2132
- typeof summary.keysExamined === 'number' ? summary.keysExamined : '',
2133
- typeof summary.nReturned === 'number' ? summary.nReturned : ''
2134
- ].join('|');
2135
- if (!deduped.has(key)) {
2136
- deduped.set(key, summary);
2137
- }
2138
- });
2139
-
2140
- return SlowQueryVerifier.sortStageSummaries(Array.from(deduped.values()));
2141
- }
2142
-
2143
- private resolveExecutionMetrics(
2144
- explainStats: Record<string, any>,
2145
- fallbackDuration?: number,
2146
- stageSummaries: ExplainStageSummary[] = []
2147
- ): ExecutionMetrics {
2148
- const docsCandidates: number[] = [];
2149
- const returnedCandidates: number[] = [];
2150
- SlowQueryVerifier.collectMetricValues(explainStats || {}, new Set(['totalDocsExamined', 'docsExamined']), docsCandidates);
2151
- SlowQueryVerifier.collectMetricValues(explainStats || {}, new Set(['nReturned']), returnedCandidates);
2152
-
2153
- const stageDocsCandidates = stageSummaries
2154
- .map(stage => stage.docsExamined)
2155
- .filter((value): value is number => typeof value === 'number');
2156
- const stageReturnedCandidates = stageSummaries
2157
- .map(stage => stage.nReturned)
2158
- .filter((value): value is number => typeof value === 'number');
2159
-
2160
- const docsExamined = docsCandidates.length
2161
- ? Math.max(...docsCandidates)
2162
- : (stageDocsCandidates.length ? Math.max(...stageDocsCandidates) : undefined);
2163
- const nReturned = returnedCandidates.length
2164
- ? Math.max(...returnedCandidates)
2165
- : (stageReturnedCandidates.length ? Math.max(...stageReturnedCandidates) : undefined);
2166
- const durationMs = SlowQueryVerifier.isValidDuration(fallbackDuration)
2167
- ? fallbackDuration
2168
- : undefined;
2169
-
2170
- return {
2171
- durationMs,
2172
- docsExamined,
2173
- nReturned,
2174
- topStages: stageSummaries.slice(0, 5)
2175
- };
2176
- }
2177
-
2178
- private evaluateOptimizationOutcome(
2179
- baseline: ExecutionMetrics,
2180
- after: ExecutionMetrics,
2181
- outputEquivalence?: OutputEquivalenceResult
2182
- ): {
2183
- passed: boolean;
2184
- reason: string;
2185
- durationRatio?: number;
2186
- docsRatio?: number;
2187
- nReturnedDeltaRatio?: number;
2188
- outputEquivalence?: OutputEquivalenceResult;
2189
- } {
2190
- if (this.config.autoOptimizeRequireExactOutput) {
2191
- if (!this.config.autoOptimizeOutputCompareEnabled) {
2192
- return {
2193
- passed: false,
2194
- reason: 'Exact output equivalence is required but output comparison is disabled.',
2195
- outputEquivalence
2196
- };
2197
- }
2198
-
2199
- if (!outputEquivalence) {
2200
- return {
2201
- passed: false,
2202
- reason: 'Output equivalence proof is required but was not generated.',
2203
- outputEquivalence
2204
- };
2205
- }
2206
- }
2207
-
2208
- if (outputEquivalence && !outputEquivalence.passed) {
2209
- return {
2210
- passed: false,
2211
- reason: `Output equivalence failed: ${outputEquivalence.reason}`,
2212
- outputEquivalence
2213
- };
2214
- }
2215
-
2216
- if (!SlowQueryVerifier.isValidDuration(baseline.durationMs) || !SlowQueryVerifier.isValidDuration(after.durationMs)) {
2217
- return {
2218
- passed: false,
2219
- reason: 'Unable to compare baseline and post-fix duration.',
2220
- outputEquivalence
2221
- };
2222
- }
2223
-
2224
- const durationRatio = baseline.durationMs > 0 ? (after.durationMs / baseline.durationMs) : 1;
2225
- if (durationRatio > this.config.autoOptimizeDurationRatioTarget) {
2226
- return {
2227
- passed: false,
2228
- reason: `Duration did not improve enough (${round(durationRatio * 100, 0)}% of baseline).`,
2229
- durationRatio,
2230
- outputEquivalence
2231
- };
2232
- }
2233
-
2234
- if (typeof baseline.docsExamined !== 'number' || typeof after.docsExamined !== 'number' || baseline.docsExamined <= 0) {
2235
- return {
2236
- passed: false,
2237
- reason: 'Docs examined metrics are missing for baseline or post-fix explain.',
2238
- durationRatio,
2239
- outputEquivalence
2240
- };
2241
- }
2242
-
2243
- const docsRatio = after.docsExamined / baseline.docsExamined;
2244
- if (docsRatio > this.config.autoOptimizeDocsRatioTarget) {
2245
- return {
2246
- passed: false,
2247
- reason: `Docs examined did not improve enough (${round(docsRatio * 100, 0)}% of baseline).`,
2248
- durationRatio,
2249
- docsRatio,
2250
- outputEquivalence
2251
- };
2252
- }
2253
-
2254
- if (typeof baseline.nReturned === 'number' && typeof after.nReturned === 'number') {
2255
- const denominator = Math.max(baseline.nReturned, 1);
2256
- const nReturnedDeltaRatio = Math.abs(after.nReturned - baseline.nReturned) / denominator;
2257
- if (nReturnedDeltaRatio > this.config.autoOptimizeReturnedDocsTolerance) {
2258
- return {
2259
- passed: false,
2260
- reason: `Returned document count changed too much (${round(nReturnedDeltaRatio * 100, 0)}% delta).`,
2261
- durationRatio,
2262
- docsRatio,
2263
- nReturnedDeltaRatio,
2264
- outputEquivalence
2265
- };
2266
- }
2267
-
2268
- return {
2269
- passed: true,
2270
- reason: 'Query performance improved while keeping returned document count stable.',
2271
- durationRatio,
2272
- docsRatio,
2273
- nReturnedDeltaRatio,
2274
- outputEquivalence
2275
- };
2276
- }
2277
-
2278
- return {
2279
- passed: false,
2280
- reason: 'Returned document metrics are missing for baseline or post-fix explain.',
2281
- durationRatio,
2282
- docsRatio,
2283
- outputEquivalence
2284
- };
2285
- }
2286
-
2287
- private formatStageSummaryForPrompt(stage: ExplainStageSummary, index: number): string {
2288
- const metrics: string[] = [];
2289
- if (typeof stage.executionTimeMs === 'number') {
2290
- metrics.push(`time=${stage.executionTimeMs}ms`);
2291
- }
2292
- if (typeof stage.docsExamined === 'number') {
2293
- metrics.push(`docs=${stage.docsExamined}`);
2294
- }
2295
- if (typeof stage.keysExamined === 'number') {
2296
- metrics.push(`keys=${stage.keysExamined}`);
2297
- }
2298
- if (typeof stage.nReturned === 'number') {
2299
- metrics.push(`returned=${stage.nReturned}`);
2300
- }
2301
- return `${index + 1}. ${stage.stage} @ ${stage.path}${metrics.length ? ` (${metrics.join(', ')})` : ''}`;
2302
- }
2303
-
2304
- private buildSlowQueryAutoOptimizeDescription(
2305
- log: SlowQueryLogModel,
2306
- app: AICoderAppModel,
2307
- baseline: ExecutionMetrics,
2308
- repoSlug: string
2309
- ): string {
2310
- const topStages = Array.isArray(baseline.topStages) ? baseline.topStages : [];
2311
- const lookupExprInCount = SlowQueryVerifier.countLookupExprInPattern(Array.isArray(log.pipeline) ? log.pipeline : []);
2312
- const lines: string[] = [
2313
- 'Autonomous slow-query optimization request.',
2314
- '',
2315
- 'Hard requirements:',
2316
- '1. Before changing code, query the project MongoDB diagnostics logs for the latest slow-query context.',
2317
- '2. Query `slow-query-logs` using `_id` and `query_hash` from this task, then use the newest matching records.',
2318
- '3. If a previous dashboard job id is provided, query `ai-development-jobs` by that `_id` and review recent failure logs before retrying.',
2319
- '4. Treat `.dashboard-output/build-*.log` as primary build evidence, and `.build-output/build-*.log` as retained history when diagnosing failures.',
2320
- '5. Locate the source query in app code and optimize it safely (query shape contract must remain compatible).',
2321
- '6. Add or adjust indexes/code paths so docs examined and processing time drop significantly.',
2322
- '7. Measure before/after `explain(\"executionStats\")` and identify the slowest stages by execution time/docs examined.',
2323
- '8. Returned data must be exactly equivalent before and after optimization. Any output difference is a failed run.',
2324
- '9. In `$lookup`, avoid `$expr` + `$in` when equivalent `localField` / `foreignField` joins are possible and index-friendly.',
2325
- '10. Run build/lint checks and iterate until green.',
2326
- '11. Use workspace context `/var/ai-workspace/<id_slow_query>` and inspect transpiled runtime references under `/var/app/current`.',
2327
- '12. Publish to default branch and deploy artifacts automatically after build success.',
2328
- '',
2329
- `App: ${app.name || app._id}`,
2330
- `Repo: ${repoSlug || app.repo || 'unknown'}`,
2331
- `Slow Query #: ${log.slow_query_count_string || log._id || ''}`,
2332
- `Workspace Context Id: ${String(log?._id || '').trim() || 'n/a'}`,
2333
- `Workspace Path: /var/ai-workspace/${String(log?._id || '').trim() || '<id_slow_query>'}`,
2334
- `Collection: ${log.collection}`,
2335
- `Query Hash: ${log.query_hash}`,
2336
- `Slow Query Log Id: ${String(log?._id || '').trim() || 'n/a'}`,
2337
- `Previous Dashboard Job Id: ${String((log as any)?.openai_task_id || '').trim() || 'n/a'}`,
2338
- `Source App: ${log.source_app || 'n/a'}`,
2339
- `Environment: ${log.environment || 'n/a'}`,
2340
- `Baseline Duration (ms): ${typeof baseline.durationMs === 'number' ? baseline.durationMs : 'unknown'}`,
2341
- `Baseline Docs Examined: ${typeof baseline.docsExamined === 'number' ? baseline.docsExamined : 'unknown'}`,
2342
- `Baseline Returned Docs: ${typeof baseline.nReturned === 'number' ? baseline.nReturned : 'unknown'}`,
2343
- `Detected $lookup with $expr+$in: ${lookupExprInCount}`,
2344
- '',
2345
- 'Baseline Hot Stages:',
2346
- ...(topStages.length
2347
- ? topStages.map((stage, index) => this.formatStageSummaryForPrompt(stage, index))
2348
- : ['No stage-level metrics were captured.']),
2349
- '',
2350
- 'Filter:',
2351
- '```json',
2352
- JSON.stringify(log.filter || {}, null, 2),
2353
- '```',
2354
- '',
2355
- 'Pipeline:',
2356
- '```json',
2357
- JSON.stringify(Array.isArray(log.pipeline) ? log.pipeline : [], null, 2),
2358
- '```',
2359
- '',
2360
- 'Options:',
2361
- '```json',
2362
- JSON.stringify(log.options || {}, null, 2),
2363
- '```'
2364
- ];
2365
- return lines.join('\n');
2366
- }
2367
-
2368
- private resolveAutoOptimizeRepoSlug(app: AICoderAppModel | null): string {
2369
- const configuredRepo = String(this.config?.autofixGithubRepo || '').trim();
2370
- const configuredOwner = String(this.config?.autofixGithubOwner || 'resolveio').trim() || 'resolveio';
2371
- if (configuredRepo) {
2372
- return `${configuredOwner}/${configuredRepo}`;
2373
- }
2374
- return String(app?.repo || '').trim();
2375
- }
2376
-
2377
- private resolveAutoOptimizeRepoPath(app: AICoderAppModel | null): string {
2378
- const configuredPath = String(this.config?.autofixRepoRoot || '').trim();
2379
- if (configuredPath) {
2380
- return configuredPath;
2381
- }
2382
- return String(app?.git_local_path || '').trim();
2383
- }
2384
-
2385
- private static queryHasExplicitSort(pipeline?: any[], findOptions?: Record<string, any>): boolean {
2386
- const hasFindSort = !!(findOptions?.sort && typeof findOptions.sort === 'object' && Object.keys(findOptions.sort).length);
2387
- if (hasFindSort) {
2388
- return true;
2389
- }
2390
-
2391
- if (!Array.isArray(pipeline)) {
2392
- return false;
2393
- }
2394
-
2395
- return pipeline.some((stage) => {
2396
- if (!stage || typeof stage !== 'object') {
2397
- return false;
2398
- }
2399
- const sort = (stage as any).$sort;
2400
- return !!(sort && typeof sort === 'object' && Object.keys(sort).length);
2401
- });
2402
- }
2403
-
2404
- private static countLookupExprInPattern(pipeline?: any[]): number {
2405
- if (!Array.isArray(pipeline)) {
2406
- return 0;
2407
- }
2408
-
2409
- let count = 0;
2410
- pipeline.forEach((stage) => {
2411
- const lookup = stage && typeof stage === 'object' ? (stage as any).$lookup : undefined;
2412
- if (!lookup || typeof lookup !== 'object' || !Array.isArray(lookup.pipeline)) {
2413
- return;
2414
- }
2415
-
2416
- const lookupPipelineJson = JSON.stringify(lookup.pipeline);
2417
- if (lookupPipelineJson.includes('"$expr"') && lookupPipelineJson.includes('"$in"')) {
2418
- count += 1;
2419
- }
2420
- });
2421
- return count;
2422
- }
2423
-
2424
- private static buildBoundedPipelineForOutputCompare(pipeline: any[], maxDocs: number): any[] {
2425
- const cloned = SlowQueryVerifier.deepClone(pipeline);
2426
- if (!Number.isFinite(maxDocs) || maxDocs <= 0) {
2427
- return cloned;
2428
- }
2429
-
2430
- cloned.push({
2431
- $limit: maxDocs + 1
2432
- });
2433
- return cloned;
2434
- }
2435
-
2436
- private static buildBoundedFindOptionsForOutputCompare(findOptions: Record<string, any> | undefined, maxDocs: number): Record<string, any> | undefined {
2437
- const bounded = findOptions ? SlowQueryVerifier.deepClone(findOptions) : {};
2438
- if (!Number.isFinite(maxDocs) || maxDocs <= 0) {
2439
- return Object.keys(bounded).length ? bounded : undefined;
2440
- }
2441
-
2442
- const compareLimit = maxDocs + 1;
2443
- if (typeof bounded.limit === 'number' && bounded.limit > 0) {
2444
- bounded.limit = Math.min(bounded.limit, compareLimit);
2445
- }
2446
- else {
2447
- bounded.limit = compareLimit;
2448
- }
2449
-
2450
- return Object.keys(bounded).length ? bounded : undefined;
2451
- }
2452
-
2453
- private static normalizeOutputValue(value: any): any {
2454
- if (value === null) {
2455
- return null;
2456
- }
2457
- if (typeof value === 'undefined') {
2458
- return null;
2459
- }
2460
- if (value instanceof Date) {
2461
- return {$date: value.toISOString()};
2462
- }
2463
- if (Buffer.isBuffer(value)) {
2464
- return {$binary: value.toString('base64')};
2465
- }
2466
-
2467
- const valueType = typeof value;
2468
- if (valueType === 'string' || valueType === 'boolean') {
2469
- return value;
2470
- }
2471
- if (valueType === 'number') {
2472
- if (!Number.isFinite(value)) {
2473
- return '__non_finite_number__';
2474
- }
2475
- return value;
2476
- }
2477
- if (valueType === 'bigint') {
2478
- return value.toString();
2479
- }
2480
- if (valueType === 'function') {
2481
- return '__function__';
2482
- }
2483
-
2484
- if (Array.isArray(value)) {
2485
- return value.map(entry => SlowQueryVerifier.normalizeOutputValue(entry));
2486
- }
2487
-
2488
- if (valueType === 'object') {
2489
- if (typeof (value as any).toHexString === 'function') {
2490
- try {
2491
- return {$oid: (value as any).toHexString()};
2492
- }
2493
- catch {
2494
- // continue into generic object handling
2495
- }
2496
- }
2497
-
2498
- if (typeof (value as any).toJSON === 'function') {
2499
- try {
2500
- return SlowQueryVerifier.normalizeOutputValue((value as any).toJSON());
2501
- }
2502
- catch {
2503
- // continue into generic object handling
2504
- }
2505
- }
2506
-
2507
- const normalized: Record<string, any> = {};
2508
- Object.keys(value).sort().forEach((key) => {
2509
- normalized[key] = SlowQueryVerifier.normalizeOutputValue(value[key]);
2510
- });
2511
- return normalized;
2512
- }
2513
-
2514
- return `${value}`;
2515
- }
2516
-
2517
- private static digestOutputDocument(doc: any): string {
2518
- const normalized = SlowQueryVerifier.normalizeOutputValue(doc);
2519
- const serialized = JSON.stringify(normalized);
2520
- return createHash('sha256').update(serialized, 'utf8').digest('hex');
2521
- }
2522
-
2523
- private async captureOutputFingerprint(log: SlowQueryLogModel, overrides?: SlowQueryExecutionOverride): Promise<OutputFingerprint> {
2524
- const collectionName = log.collection;
2525
- if (!collectionName) {
2526
- throw new Error('Slow query missing collection name.');
2527
- }
2528
-
2529
- const target = await this.resolveExplainTarget(log);
2530
- let client: MongoClient | undefined;
2531
- let db: any;
2532
-
2533
- try {
2534
- if (target.type === 'client') {
2535
- if (!target.uri) {
2536
- throw new SlowQueryVerifierError('client_db_missing_uri', 'Client DB missing uri.');
2537
- }
2538
-
2539
- client = await MongoClient.connect(target.uri, {
2540
- connectTimeoutMS: 10000,
2541
- serverSelectionTimeoutMS: 10000,
2542
- readPreference: 'secondaryPreferred'
2543
- });
2544
- db = client.db(target.dbName);
2545
- }
2546
- else {
2547
- db = ResolveIOServer.getMainDB();
2548
- }
2549
-
2550
- if (!db) {
2551
- throw new SlowQueryVerifierError('main_db_unavailable', 'Main server DB is not available.');
2552
- }
2553
-
2554
- const maxDocs = Number.isFinite(Number(this.config.autoOptimizeOutputCompareMaxDocs))
2555
- ? Number(this.config.autoOptimizeOutputCompareMaxDocs)
2556
- : AUTO_OPTIMIZE_DEFAULT_OUTPUT_COMPARE_MAX_DOCS;
2557
- const effectiveMaxDocs = maxDocs > 0 ? Math.floor(maxDocs) : AUTO_OPTIMIZE_DEFAULT_OUTPUT_COMPARE_MAX_DOCS;
2558
- const effectiveLog = SlowQueryVerifier.applyQueryOverrides(log, overrides);
2559
- const pipeline = SlowQueryVerifier.extractPipelineFromLog(effectiveLog);
2560
- const filter = effectiveLog.filter ?? {};
2561
- const findOptions = SlowQueryVerifier.extractFindOptions(effectiveLog.options);
2562
- const aggregateOptions = SlowQueryVerifier.extractAggregateOptions(effectiveLog.options);
2563
- const explicitSort = SlowQueryVerifier.queryHasExplicitSort(pipeline, findOptions);
2564
-
2565
- let cursor: any;
2566
- if (pipeline) {
2567
- if (SlowQueryVerifier.pipelineHasWriteStage(pipeline as any[])) {
2568
- throw new SlowQueryVerifierError('aggregate_write_stage', 'Aggregate pipeline includes a write stage; output comparison skipped.');
2569
- }
2570
-
2571
- const boundedPipeline = SlowQueryVerifier.buildBoundedPipelineForOutputCompare(
2572
- pipeline,
2573
- effectiveMaxDocs
2574
- );
2575
- cursor = db.collection(collectionName)
2576
- .aggregate(boundedPipeline, {
2577
- ...(aggregateOptions || {}),
2578
- readPreference: 'secondaryPreferred'
2579
- });
2580
- }
2581
- else {
2582
- const boundedFindOptions = SlowQueryVerifier.buildBoundedFindOptionsForOutputCompare(
2583
- findOptions,
2584
- effectiveMaxDocs
2585
- );
2586
- cursor = SlowQueryVerifier.buildFindCursor(
2587
- db.collection(collectionName),
2588
- filter,
2589
- boundedFindOptions
2590
- );
2591
- }
2592
-
2593
- const orderedHasher = createHash('sha256');
2594
- let unorderedSum = 0;
2595
- let unorderedXor = 0;
2596
- let docsCompared = 0;
2597
- let truncated = false;
2598
- let firstDocDigest = '';
2599
- let lastDocDigest = '';
2600
- const startedAt = Date.now();
2601
-
2602
- try {
2603
- for await (const doc of cursor) {
2604
- if (docsCompared >= effectiveMaxDocs) {
2605
- truncated = true;
2606
- break;
2607
- }
2608
-
2609
- const docDigest = SlowQueryVerifier.digestOutputDocument(doc);
2610
- if (!firstDocDigest) {
2611
- firstDocDigest = docDigest;
2612
- }
2613
- lastDocDigest = docDigest;
2614
- orderedHasher.update(docDigest, 'utf8');
2615
-
2616
- const head = (parseInt(docDigest.slice(0, 8), 16) ^ parseInt(docDigest.slice(8, 16), 16)) >>> 0;
2617
- const tail = (parseInt(docDigest.slice(16, 24), 16) ^ parseInt(docDigest.slice(24, 32), 16)) >>> 0;
2618
- unorderedSum = (unorderedSum + head + tail) >>> 0;
2619
- unorderedXor = (unorderedXor ^ head ^ tail) >>> 0;
2620
- docsCompared += 1;
2621
- }
2622
- }
2623
- finally {
2624
- if (cursor && typeof cursor.close === 'function') {
2625
- await cursor.close();
2626
- }
2627
- }
2628
-
2629
- const durationMs = Date.now() - startedAt;
2630
- return {
2631
- explicitSort,
2632
- docsCompared,
2633
- truncated,
2634
- orderedDigest: orderedHasher.digest('hex'),
2635
- unorderedDigest: `${unorderedSum.toString(16).padStart(8, '0')}:${unorderedXor.toString(16).padStart(8, '0')}`,
2636
- firstDocDigest,
2637
- lastDocDigest,
2638
- durationMs: SlowQueryVerifier.isValidDuration(durationMs) ? durationMs : -1,
2639
- maxDocs: effectiveMaxDocs
2640
- };
2641
- }
2642
- finally {
2643
- if (client) {
2644
- await client.close();
2645
- }
2646
- }
2647
- }
2648
-
2649
- private compareOutputEquivalence(baseline: OutputFingerprint, after: OutputFingerprint): OutputEquivalenceResult {
2650
- const mode: 'ordered' | 'unordered' = (baseline.explicitSort || after.explicitSort)
2651
- ? 'ordered'
2652
- : 'unordered';
2653
-
2654
- if (baseline.truncated || after.truncated) {
2655
- return {
2656
- passed: false,
2657
- reason: `Result set exceeded output comparison cap (${Math.max(baseline.maxDocs, after.maxDocs)} docs).`,
2658
- mode,
2659
- baseline,
2660
- after
2661
- };
2662
- }
2663
-
2664
- if (baseline.docsCompared !== after.docsCompared) {
2665
- return {
2666
- passed: false,
2667
- reason: `Returned row count changed (${baseline.docsCompared} -> ${after.docsCompared}).`,
2668
- mode,
2669
- baseline,
2670
- after
2671
- };
2672
- }
2673
-
2674
- if (mode === 'ordered') {
2675
- if (baseline.orderedDigest !== after.orderedDigest) {
2676
- return {
2677
- passed: false,
2678
- reason: 'Ordered output digest changed.',
2679
- mode,
2680
- baseline,
2681
- after
2682
- };
2683
- }
2684
- }
2685
- else if (baseline.unorderedDigest !== after.unorderedDigest) {
2686
- return {
2687
- passed: false,
2688
- reason: 'Output set digest changed.',
2689
- mode,
2690
- baseline,
2691
- after
2692
- };
2693
- }
2694
-
2695
- return {
2696
- passed: true,
2697
- reason: 'Output fingerprints are equivalent.',
2698
- mode,
2699
- baseline,
2700
- after
2701
- };
2702
- }
2703
-
2704
- private async resolveAutoOptimizeApp(log: SlowQueryLogModel): Promise<AICoderAppModel | null> {
2705
- const exactRepoCandidate = String(log.environment || '').trim();
2706
- if (exactRepoCandidate && exactRepoCandidate.includes('/')) {
2707
- const escaped = this.escapeRegex(exactRepoCandidate);
2708
- const byRepo = await AICoderApps.findOne({
2709
- repo: {$regex: `^${escaped}$`, $options: 'i'}
2710
- }, {
2711
- sort: {
2712
- updatedAt: -1,
2713
- createdAt: -1
2714
- }
2715
- });
2716
- if (byRepo) {
2717
- return byRepo;
2718
- }
2719
- }
2720
-
2721
- const clientCandidates = [
2722
- String(log.client_slug || '').trim(),
2723
- String(log.client_name || '').trim(),
2724
- String(log.source_app || '').trim()
2725
- ].filter(Boolean);
2726
-
2727
- for (const candidate of clientCandidates) {
2728
- const escaped = this.escapeRegex(candidate);
2729
- const byApp = await AICoderApps.findOne({
2730
- $or: [
2731
- {slug: {$regex: `^${escaped}$`, $options: 'i'}},
2732
- {name: {$regex: `^${escaped}$`, $options: 'i'}}
2733
- ]
2734
- }, {
2735
- sort: {
2736
- updatedAt: -1,
2737
- createdAt: -1
2738
- }
2739
- });
2740
- if (byApp) {
2741
- return byApp;
2742
- }
2743
-
2744
- const clientDoc = await Clients.findOne({
2745
- $or: [
2746
- {name: {$regex: `^${escaped}$`, $options: 'i'}},
2747
- {demo_name: {$regex: `^${escaped}$`, $options: 'i'}},
2748
- {project: {$regex: `^${escaped}$`, $options: 'i'}},
2749
- {repo: {$regex: `^${escaped}$`, $options: 'i'}}
2750
- ]
2751
- });
2752
- if (!clientDoc?._id) {
2753
- continue;
2754
- }
2755
- const byClient = await AICoderApps.findOne({client_id: clientDoc._id}, {
2756
- sort: {
2757
- updatedAt: -1,
2758
- createdAt: -1
2759
- }
2760
- });
2761
- if (byClient) {
2762
- return byClient;
2763
- }
2764
- }
2765
-
2766
- return null;
2767
- }
2768
-
2769
- private async runAutoOptimization(logId: string, force = false): Promise<void> {
2770
- const autoOptimizeEnabled = await this.resolveAutoOptimizeEnabled();
2771
- if (!logId || (!autoOptimizeEnabled && !force)) {
2772
- return;
2773
- }
2774
-
2775
- const log = await SlowQueryLogs.findOne({_id: logId});
2776
- if (!log || !log._id || log.ignored) {
2777
- return;
2778
- }
2779
- if (log.status === 'optimized' && !force) {
2780
- return;
2781
- }
2782
- if (log.auto_fix_status === 'running') {
2783
- return;
2784
- }
2785
- if (log.auto_fix_status === 'queued' && String(log.openai_task_id || '').trim()) {
2786
- return;
2787
- }
2788
-
2789
- if (!force) {
2790
- const attemptsUsed = Number.isFinite(Number(log.auto_fix_attempt_count))
2791
- ? Number(log.auto_fix_attempt_count)
2792
- : 0;
2793
- const maxAttempts = Number.isFinite(Number(this.config.autoOptimizeMaxAttemptsPerQuery))
2794
- ? Number(this.config.autoOptimizeMaxAttemptsPerQuery)
2795
- : 0;
2796
- if (maxAttempts > 0 && attemptsUsed >= maxAttempts) {
2797
- await this.markAutoOptimizeBudgetExceeded(log, 'Auto optimize skipped');
2798
- return;
2799
- }
2800
-
2801
- const cooldownDeadline = this.resolveCooldownDeadline(log);
2802
- if (cooldownDeadline && cooldownDeadline.getTime() > Date.now()) {
2803
- await this.markAutoOptimizeCooldownActive(log, cooldownDeadline);
2804
- return;
2805
- }
2806
-
2807
- const fingerprintMaxAttempts = Number.isFinite(Number(this.config.autoOptimizeMaxAttemptsPerFingerprint))
2808
- ? Number(this.config.autoOptimizeMaxAttemptsPerFingerprint)
2809
- : 0;
2810
- if (fingerprintMaxAttempts > 0) {
2811
- const windowHours = Number.isFinite(Number(this.config.autoOptimizeFingerprintWindowHours))
2812
- ? Number(this.config.autoOptimizeFingerprintWindowHours)
2813
- : AUTO_OPTIMIZE_DEFAULT_FINGERPRINT_WINDOW_HOURS;
2814
- const windowStart = new Date(Date.now() - (windowHours * 60 * 60 * 1000));
2815
- const fingerprintAttempts = await this.resolveFingerprintAttemptsInWindow(log, windowStart);
2816
- if (fingerprintAttempts >= fingerprintMaxAttempts) {
2817
- await this.markAutoOptimizeBudgetExceeded(
2818
- log,
2819
- `Auto optimize skipped: fingerprint budget reached (${fingerprintAttempts}/${fingerprintMaxAttempts}) in the last ${windowHours}h.`
2820
- );
2821
- return;
2822
- }
2823
- }
2824
- }
2825
-
2826
- const app = await this.resolveAutoOptimizeApp(log);
2827
- const resolvedRepoSlug = this.resolveAutoOptimizeRepoSlug(app);
2828
- const resolvedRepoPath = this.resolveAutoOptimizeRepoPath(app);
2829
- if (!app?._id || !resolvedRepoSlug) {
2830
- await SlowQueryLogs.updateOne({_id: logId}, {
2831
- $set: {
2832
- status: 'investigating',
2833
- auto_fix_status: 'failed',
2834
- verification_notes: 'Auto optimize skipped: unable to map slow query to AI Coder app/repo configuration.',
2835
- last_triaged_by: 'auto-slow-query',
2836
- last_triaged_at: new Date()
2837
- }
2838
- });
2839
- return;
2840
- }
2841
- const tokenEligibility = await checkAICoderTokenEligibility(
2842
- app._id,
2843
- this.config.autoOptimizeRequiredTokens > 0 ? this.config.autoOptimizeRequiredTokens : undefined
2844
- );
2845
- if (!tokenEligibility.allowed) {
2846
- const reason = `${tokenEligibility.message} Available: ${tokenEligibility.summary.available_tokens.toLocaleString()} tokens; required: ${tokenEligibility.required_tokens.toLocaleString()}.`;
2847
- await this.markAutoOptimizeTokenIneligible(log, reason);
2848
- return;
2849
- }
2850
-
2851
- let baselineExplain: ExplainResult;
2852
- try {
2853
- baselineExplain = await this.runExplain(log);
2854
- }
2855
- catch (error) {
2856
- await SlowQueryLogs.updateOne({_id: logId}, {
2857
- $set: {
2858
- status: 'investigating',
2859
- auto_fix_status: 'failed',
2860
- auto_fix_result: {
2861
- baseline_error: error?.message || 'unknown'
2862
- },
2863
- verification_notes: `Auto optimize baseline measurement failed: ${error?.message || 'unknown error'}`,
2864
- last_triaged_by: 'auto-slow-query',
2865
- last_triaged_at: new Date()
2866
- }
2867
- });
2868
- await this.maybeStopAutoOptimizeAfterFailure(logId, 'Auto optimize baseline measurement failed');
2869
- return;
2870
- }
2871
-
2872
- const baselineFallbackDuration = this.resolveBaselineDurationMs(log);
2873
- const baselineDurationMs = SlowQueryVerifier.isValidDuration(baselineExplain.durationMs)
2874
- ? baselineExplain.durationMs
2875
- : baselineFallbackDuration;
2876
- const baselineMetrics = this.resolveExecutionMetrics(
2877
- baselineExplain.explainStats || {},
2878
- baselineDurationMs,
2879
- baselineExplain.stageSummaries || []
2880
- );
2881
-
2882
- let baselineOutputFingerprint: OutputFingerprint | undefined;
2883
- if (this.config.autoOptimizeOutputCompareEnabled) {
2884
- try {
2885
- baselineOutputFingerprint = await this.captureOutputFingerprint(log);
2886
- }
2887
- catch (error) {
2888
- await SlowQueryLogs.updateOne({_id: logId}, {
2889
- $set: {
2890
- status: 'investigating',
2891
- auto_fix_status: 'failed',
2892
- auto_fix_result: {
2893
- baseline_error: error?.message || 'unknown'
2894
- },
2895
- verification_notes: `Auto optimize baseline output comparison failed: ${error?.message || 'unknown error'}`,
2896
- last_triaged_by: 'auto-slow-query',
2897
- last_triaged_at: new Date()
2898
- }
2899
- });
2900
- await this.maybeStopAutoOptimizeAfterFailure(logId, 'Auto optimize baseline output comparison failed');
2901
- return;
2902
- }
2903
- }
2904
-
2905
- const title = `Optimize slow query ${log.slow_query_count_string || log.collection}`;
2906
- const description = this.buildSlowQueryAutoOptimizeDescription(log, app, baselineMetrics, resolvedRepoSlug);
2907
-
2908
- let job: AIDashboardJob;
2909
- try {
2910
- job = await this.createDashboardJob({
2911
- project: app._id,
2912
- title,
2913
- description,
2914
- repo: resolvedRepoSlug,
2915
- path: resolvedRepoPath || undefined,
2916
- projectRoot: app.project_root || undefined
2917
- });
2918
- }
2919
- catch (error) {
2920
- await SlowQueryLogs.updateOne({_id: logId}, {
2921
- $set: {
2922
- status: 'investigating',
2923
- auto_fix_status: 'failed',
2924
- verification_notes: `Auto optimize enqueue failed: ${error?.message || 'unknown error'}`,
2925
- last_triaged_by: 'auto-slow-query',
2926
- last_triaged_at: new Date()
2927
- }
2928
- });
2929
- await this.maybeStopAutoOptimizeAfterFailure(logId, 'Auto optimize wait failed');
2930
- return;
2931
- }
2932
-
2933
- const jobId = String(job?._id || '').trim();
2934
- if (!jobId) {
2935
- await SlowQueryLogs.updateOne({_id: logId}, {
2936
- $set: {
2937
- status: 'investigating',
2938
- auto_fix_status: 'failed',
2939
- verification_notes: 'Auto optimize enqueue failed: dashboard job id missing.',
2940
- last_triaged_by: 'auto-slow-query',
2941
- last_triaged_at: new Date()
2942
- }
2943
- });
2944
- return;
2945
- }
2946
-
2947
- const attemptStartedAt = new Date();
2948
- await SlowQueryLogs.updateOne({_id: logId}, {
2949
- $inc: {
2950
- auto_fix_attempt_count: 1
2951
- },
2952
- $push: {
2953
- auto_fix_attempt_history: {
2954
- $each: [attemptStartedAt],
2955
- $slice: -100
2956
- }
2957
- },
2958
- $set: {
2959
- status: 'queued',
2960
- auto_fix_status: 'running',
2961
- openai_task_id: jobId,
2962
- auto_fix_last_attempt_at: attemptStartedAt,
2963
- auto_fix_disabled_reason: '',
2964
- verification_notes: `Auto optimize job queued (${jobId}).`,
2965
- last_triaged_by: 'auto-slow-query',
2966
- last_triaged_at: new Date()
2967
- }
2968
- });
2969
- const queuedLog = (await SlowQueryLogs.findOne({_id: logId})) || log;
2970
- await this.notifyCustomerSlowQueryStatus('detected_auto_optimize_enabled', queuedLog);
2971
-
2972
- try {
2973
- await this.waitForDashboardJobStop(jobId, this.config.autoOptimizeWaitTimeoutMs);
2974
- }
2975
- catch (error) {
2976
- await SlowQueryLogs.updateOne({_id: logId}, {
2977
- $set: {
2978
- status: 'investigating',
2979
- auto_fix_status: 'failed',
2980
- auto_fix_result: {
2981
- job_id: jobId,
2982
- error: error?.message || 'timeout'
2983
- },
2984
- verification_notes: `Auto optimize wait failed: ${error?.message || 'timeout'}`,
2985
- last_triaged_by: 'auto-slow-query',
2986
- last_triaged_at: new Date()
2987
- }
2988
- });
2989
- await this.maybeStopAutoOptimizeAfterFailure(logId, 'Auto optimize job state check failed');
2990
- return;
2991
- }
2992
-
2993
- let isRunning = false;
2994
- try {
2995
- isRunning = await this.isDashboardJobRunning(jobId);
2996
- }
2997
- catch (error) {
2998
- await SlowQueryLogs.updateOne({_id: logId}, {
2999
- $set: {
3000
- status: 'investigating',
3001
- auto_fix_status: 'failed',
3002
- auto_fix_result: {
3003
- job_id: jobId,
3004
- error: error?.message || 'unknown'
3005
- },
3006
- verification_notes: `Unable to confirm dashboard job state: ${error?.message || 'unknown error'}`,
3007
- last_triaged_by: 'auto-slow-query',
3008
- last_triaged_at: new Date()
3009
- }
3010
- });
3011
- return;
3012
- }
3013
-
3014
- if (isRunning) {
3015
- await SlowQueryLogs.updateOne({_id: logId}, {
3016
- $set: {
3017
- status: 'investigating',
3018
- auto_fix_status: 'failed',
3019
- auto_fix_result: {
3020
- job_id: jobId,
3021
- error: 'still_running_after_timeout'
3022
- },
3023
- verification_notes: `Auto optimize timed out while waiting for dashboard job ${jobId}.`,
3024
- last_triaged_by: 'auto-slow-query',
3025
- last_triaged_at: new Date()
3026
- }
3027
- });
3028
- await this.maybeStopAutoOptimizeAfterFailure(logId, 'Auto optimize timed out');
3029
- return;
3030
- }
3031
-
3032
- const finalJob = await AIDashboardJobs.findOne({_id: jobId}) as AIDashboardJob | null;
3033
- if (!finalJob || finalJob.phase !== 'COMPLETE' || finalJob.paused) {
3034
- await SlowQueryLogs.updateOne({_id: logId}, {
3035
- $set: {
3036
- status: 'investigating',
3037
- auto_fix_status: 'failed',
3038
- auto_fix_result: {
3039
- job_id: jobId,
3040
- job_phase: finalJob?.phase || 'missing',
3041
- job_paused: !!finalJob?.paused
3042
- },
3043
- verification_notes: `Auto optimize job ${jobId} did not complete successfully.`,
3044
- last_triaged_by: 'auto-slow-query',
3045
- last_triaged_at: new Date()
3046
- }
3047
- });
3048
- await this.maybeStopAutoOptimizeAfterFailure(logId, 'Auto optimize job did not complete');
3049
- return;
3050
- }
3051
-
3052
- const publishOutcome = this.evaluateDashboardPublishOutcome(finalJob);
3053
- if (!publishOutcome.success) {
3054
- await SlowQueryLogs.updateOne({_id: logId}, {
3055
- $set: {
3056
- status: 'investigating',
3057
- auto_fix_status: 'failed',
3058
- auto_fix_result: {
3059
- job_id: jobId,
3060
- publish_message: publishOutcome.message,
3061
- publish_branch: publishOutcome.branchName || ''
3062
- },
3063
- verification_notes: `Auto optimize publish/deploy failed: ${publishOutcome.message}`,
3064
- last_triaged_by: 'auto-slow-query',
3065
- last_triaged_at: new Date()
3066
- }
3067
- });
3068
- await this.maybeStopAutoOptimizeAfterFailure(logId, 'Auto optimize publish/deploy failed');
3069
- return;
3070
- }
3071
-
3072
- const refreshedLog = (await SlowQueryLogs.findOne({_id: logId})) || log;
3073
- let afterExplain: ExplainResult;
3074
- try {
3075
- afterExplain = await this.runExplain(refreshedLog);
3076
- }
3077
- catch (error) {
3078
- await SlowQueryLogs.updateOne({_id: logId}, {
3079
- $set: {
3080
- status: 'investigating',
3081
- auto_fix_status: 'failed',
3082
- auto_fix_result: {
3083
- job_id: jobId,
3084
- publish_message: publishOutcome.message,
3085
- publish_branch: publishOutcome.branchName || '',
3086
- validation_error: error?.message || 'unknown'
3087
- },
3088
- verification_notes: `Post-deploy validation failed: ${error?.message || 'unknown error'}`,
3089
- last_triaged_by: 'auto-slow-query',
3090
- last_triaged_at: new Date()
3091
- }
3092
- });
3093
- await this.maybeStopAutoOptimizeAfterFailure(logId, 'Auto optimize post-deploy validation failed');
3094
- return;
3095
- }
3096
-
3097
- const afterMetrics = this.resolveExecutionMetrics(
3098
- afterExplain.explainStats || {},
3099
- afterExplain.durationMs,
3100
- afterExplain.stageSummaries || []
3101
- );
3102
-
3103
- let outputEquivalence: OutputEquivalenceResult | undefined;
3104
- if (this.config.autoOptimizeOutputCompareEnabled) {
3105
- if (!baselineOutputFingerprint) {
3106
- outputEquivalence = {
3107
- passed: false,
3108
- reason: 'Baseline output fingerprint missing.',
3109
- mode: 'unknown'
3110
- };
3111
- }
3112
- else {
3113
- try {
3114
- const afterOutputFingerprint = await this.captureOutputFingerprint(refreshedLog);
3115
- outputEquivalence = this.compareOutputEquivalence(baselineOutputFingerprint, afterOutputFingerprint);
3116
- }
3117
- catch (error) {
3118
- await SlowQueryLogs.updateOne({_id: logId}, {
3119
- $set: {
3120
- status: 'investigating',
3121
- auto_fix_status: 'failed',
3122
- auto_fix_result: {
3123
- job_id: jobId,
3124
- publish_message: publishOutcome.message,
3125
- publish_branch: publishOutcome.branchName || '',
3126
- baseline: baselineMetrics,
3127
- after: afterMetrics,
3128
- output_equivalence_error: error?.message || 'unknown'
3129
- },
3130
- verification_notes: `Post-deploy output comparison failed: ${error?.message || 'unknown error'}`,
3131
- explain_plan: afterExplain.explainPlan,
3132
- explain_execution_stats: afterExplain.explainStats,
3133
- explain_generated_at: new Date(),
3134
- last_triaged_by: 'auto-slow-query',
3135
- last_triaged_at: new Date()
3136
- },
3137
- $push: {
3138
- verification_runs: {
3139
- timestamp: new Date(),
3140
- duration_ms: afterExplain.durationMs
3141
- }
3142
- }
3143
- });
3144
- await this.maybeStopAutoOptimizeAfterFailure(logId, 'Auto optimize output comparison failed');
3145
- return;
3146
- }
3147
- }
3148
- }
3149
-
3150
- const validation = this.evaluateOptimizationOutcome(baselineMetrics, afterMetrics, outputEquivalence);
3151
- const autoFixResult = {
3152
- job_id: jobId,
3153
- publish_message: publishOutcome.message,
3154
- publish_branch: publishOutcome.branchName || '',
3155
- baseline: baselineMetrics,
3156
- after: afterMetrics,
3157
- output_equivalence: outputEquivalence,
3158
- validation
3159
- };
3160
-
3161
- if (!validation.passed) {
3162
- await SlowQueryLogs.updateOne({_id: logId}, {
3163
- $set: {
3164
- status: 'investigating',
3165
- auto_fix_status: 'failed',
3166
- auto_fix_result: autoFixResult,
3167
- verification_notes: `Auto optimize validation failed: ${validation.reason}`,
3168
- explain_plan: afterExplain.explainPlan,
3169
- explain_execution_stats: afterExplain.explainStats,
3170
- explain_generated_at: new Date(),
3171
- last_triaged_by: 'auto-slow-query',
3172
- last_triaged_at: new Date()
3173
- },
3174
- $push: {
3175
- verification_runs: {
3176
- timestamp: new Date(),
3177
- duration_ms: afterExplain.durationMs
3178
- }
3179
- }
3180
- });
3181
- await this.maybeStopAutoOptimizeAfterFailure(logId, 'Auto optimize validation failed');
3182
- return;
3183
- }
3184
-
3185
- await SlowQueryLogs.updateOne({_id: logId}, {
3186
- $set: {
3187
- status: 'optimized',
3188
- auto_fix_status: 'completed',
3189
- auto_fix_result: autoFixResult,
3190
- verification_notes: `Auto optimize completed: ${validation.reason}`,
3191
- explain_plan: afterExplain.explainPlan,
3192
- explain_execution_stats: afterExplain.explainStats,
3193
- explain_generated_at: new Date(),
3194
- last_triaged_by: 'auto-slow-query',
3195
- last_triaged_at: new Date()
3196
- },
3197
- $push: {
3198
- verification_runs: {
3199
- timestamp: new Date(),
3200
- duration_ms: afterExplain.durationMs
3201
- }
3202
- }
3203
- });
3204
- const optimizedLog = (await SlowQueryLogs.findOne({_id: logId})) || log;
3205
- await this.notifyCustomerSlowQueryStatus('completed_success', optimizedLog, {notes: validation.reason});
3206
- }
3207
-
3208
- private async resolveClientDB(log: SlowQueryLogModel): Promise<ClientDBModel | undefined> {
3209
- const candidates = [
3210
- log.client_slug,
3211
- log.client_name,
3212
- log.source_app
3213
- ].filter(Boolean);
3214
-
3215
- if (!candidates.length) {
3216
- return undefined;
3217
- }
3218
-
3219
- const matches = await ClientDBs.find({
3220
- $or: [
3221
- { client: { $in: candidates } },
3222
- { name: { $in: candidates } }
3223
- ]
3224
- }, {
3225
- limit: 10
3226
- });
3227
-
3228
- if (!Array.isArray(matches) || !matches.length) {
3229
- return undefined;
3230
- }
3231
-
3232
- const prodMatch = matches.find(match => match && match.dev_server === false);
3233
- if (prodMatch) {
3234
- return prodMatch;
3235
- }
3236
-
3237
- const devMatch = matches.find(match => match && match.dev_server === true);
3238
- if (devMatch) {
3239
- return devMatch;
3240
- }
3241
-
3242
- return matches[0];
3243
- }
3244
-
3245
- private async resolveExplainTarget(log: SlowQueryLogModel): Promise<ExplainTarget> {
3246
- const clientDB = await this.resolveClientDB(log);
3247
-
3248
- if (clientDB) {
3249
- const dbName = clientDB.database || clientDB.name;
3250
- if (!dbName) {
3251
- throw new SlowQueryVerifierError('client_db_missing_database', 'Client DB missing database name.');
3252
- }
3253
-
3254
- return {
3255
- type: 'client',
3256
- dbName,
3257
- uri: clientDB.uri
3258
- };
3259
- }
3260
-
3261
- if (!this.config.fallbackToMainDB) {
3262
- throw new SlowQueryVerifierError('client_db_not_found', 'Could not resolve client DB for slow query verification.');
3263
- }
3264
-
3265
- const mainDb = ResolveIOServer.getMainDB();
3266
- const mainDbName = mainDb?.databaseName;
3267
-
3268
- if (!mainDb || !mainDbName) {
3269
- throw new SlowQueryVerifierError('main_db_unavailable', 'Main server DB is not available.');
3270
- }
3271
-
3272
- return {
3273
- type: 'main',
3274
- dbName: mainDbName
3275
- };
3276
- }
3277
-
3278
- private static extractPipelineFromLog(log: SlowQueryLogModel): any[] | undefined {
3279
- if (!log) {
3280
- return undefined;
3281
- }
3282
-
3283
- if (Array.isArray(log.pipeline)) {
3284
- return log.pipeline;
3285
- }
3286
-
3287
- if (log.filter && Array.isArray((log.filter as any).pipeline)) {
3288
- return (log.filter as any).pipeline;
3289
- }
3290
-
3291
- return SlowQueryVerifier.extractPipelineOptions(log.options);
3292
- }
3293
-
3294
- private static applyQueryOverrides(log: SlowQueryLogModel, overrides?: SlowQueryExecutionOverride): SlowQueryLogModel {
3295
- if (!overrides) {
3296
- return log;
3297
- }
3298
-
3299
- const merged: SlowQueryLogModel = {...log};
3300
-
3301
- if (overrides.filter !== undefined) {
3302
- merged.filter = overrides.filter;
3303
- }
3304
-
3305
- if (overrides.options !== undefined) {
3306
- merged.options = overrides.options;
3307
- }
3308
-
3309
- if (overrides.pipeline !== undefined) {
3310
- merged.pipeline = overrides.pipeline;
3311
- }
3312
-
3313
- return merged;
3314
- }
3315
-
3316
- private static extractPipelineOptions(options?: Record<string, any>): any[] | undefined {
3317
- if (!options) {
3318
- return undefined;
3319
- }
3320
-
3321
- if (Array.isArray(options)) {
3322
- return options;
3323
- }
3324
-
3325
- if (Array.isArray(options.pipeline)) {
3326
- return options.pipeline;
3327
- }
3328
-
3329
- return undefined;
3330
- }
3331
-
3332
- private static extractFindOptions(options?: Record<string, any>): Record<string, any> | undefined {
3333
- if (!options || Array.isArray(options)) {
3334
- return undefined;
3335
- }
3336
-
3337
- const normalized: Record<string, any> = {...options};
3338
- if (Array.isArray(normalized.pipeline)) {
3339
- delete normalized.pipeline;
3340
- }
3341
-
3342
- if (!Object.keys(normalized).length) {
3343
- return undefined;
3344
- }
3345
-
3346
- return normalized;
3347
- }
3348
-
3349
- private static resolveDurationMs(explainResponse: Record<string, any>): number {
3350
- const stats = explainResponse?.executionStats;
3351
- if (!stats) {
3352
- const stages = explainResponse?.stages;
3353
- if (Array.isArray(stages)) {
3354
- for (const stage of stages) {
3355
- const stageStats = stage?.$cursor?.executionStats;
3356
- const candidates = [
3357
- stageStats?.executionTimeMillis,
3358
- stageStats?.executionTimeMillisEstimate,
3359
- stageStats?.executionStages?.executionTimeMillis,
3360
- stageStats?.executionStages?.executionTimeMillisEstimate
3361
- ];
3362
-
3363
- for (const candidate of candidates) {
3364
- if (SlowQueryVerifier.isValidDuration(candidate)) {
3365
- return candidate;
3366
- }
3367
- }
3368
- }
3369
- }
3370
-
3371
- return -1;
3372
- }
3373
-
3374
- const candidates = [
3375
- stats.executionTimeMillis,
3376
- stats.executionTimeMillisEstimate,
3377
- stats.executionStages?.executionTimeMillis,
3378
- stats.executionStages?.executionTimeMillisEstimate
3379
- ];
3380
-
3381
- for (const candidate of candidates) {
3382
- if (SlowQueryVerifier.isValidDuration(candidate)) {
3383
- return candidate;
3384
- }
3385
- }
3386
-
3387
- return -1;
3388
- }
3389
-
3390
- private static pipelineHasWriteStage(pipeline: any[]): boolean {
3391
- if (!Array.isArray(pipeline)) {
3392
- return false;
3393
- }
3394
-
3395
- return pipeline.some(stage => {
3396
- if (!stage || typeof stage !== 'object') {
3397
- return false;
3398
- }
3399
-
3400
- return typeof (stage as any).$out !== 'undefined' || typeof (stage as any).$merge !== 'undefined';
3401
- });
3402
- }
3403
-
3404
- private static extractAggregateOptions(options?: Record<string, any>): Record<string, any> | undefined {
3405
- if (!options || Array.isArray(options) || typeof options !== 'object') {
3406
- return undefined;
3407
- }
3408
-
3409
- const allowedKeys = [
3410
- 'allowDiskUse',
3411
- 'bypassDocumentValidation',
3412
- 'collation',
3413
- 'comment',
3414
- 'hint',
3415
- 'let',
3416
- 'maxTimeMS',
3417
- 'maxAwaitTimeMS'
3418
- ];
3419
-
3420
- const result: Record<string, any> = {};
3421
- allowedKeys.forEach(key => {
3422
- if (typeof (options as any)[key] !== 'undefined') {
3423
- result[key] = (options as any)[key];
3424
- }
3425
- });
3426
-
3427
- return Object.keys(result).length ? result : undefined;
3428
- }
3429
-
3430
- private static buildAggregateExplainCommand(
3431
- collectionName: string,
3432
- pipeline: any[],
3433
- options?: Record<string, any>,
3434
- verbosity: ExplainVerbosity = 'executionStats'
3435
- ): Record<string, any> {
3436
- const aggregateCommand: Record<string, any> = {
3437
- aggregate: collectionName,
3438
- pipeline,
3439
- cursor: {}
3440
- };
3441
-
3442
- if (options && typeof options === 'object') {
3443
- Object.keys(options).forEach(key => {
3444
- if (typeof (options as any)[key] !== 'undefined') {
3445
- aggregateCommand[key] = (options as any)[key];
3446
- }
3447
- });
3448
- }
3449
-
3450
- return {
3451
- explain: aggregateCommand,
3452
- verbosity
3453
- };
3454
- }
3455
-
3456
- private static isAggregateExplainWriteConcernError(err: any): boolean {
3457
- const message = `${err?.message || ''}`;
3458
- return message.includes('Option "explain" cannot be used on an aggregate call with writeConcern');
3459
- }
3460
-
3461
- private static isBsonObjectTooLargeError(err: any): boolean {
3462
- const message = `${err?.message || ''}`;
3463
- return message.includes('BSONObj size:') && message.includes('is invalid');
3464
- }
3465
-
3466
- private static isValidDuration(value: any): value is number {
3467
- return typeof value === 'number' && !Number.isNaN(value) && value >= 0;
3468
- }
3469
-
3470
- private static buildFindCursor(collection: any, filter: Record<string, any>, findOptions?: Record<string, any>) {
3471
- const cursorOptions: Record<string, any> = {};
3472
-
3473
- if (findOptions) {
3474
- Object.keys(findOptions).forEach(key => {
3475
- if (['sort', 'skip', 'limit', 'projection', 'fields'].includes(key)) {
3476
- return;
3477
- }
3478
-
3479
- cursorOptions[key] = findOptions[key];
3480
- });
3481
- }
3482
-
3483
- cursorOptions.readPreference = 'secondaryPreferred';
3484
-
3485
- let cursor = collection.find(filter || {}, cursorOptions);
3486
-
3487
- const projection = findOptions?.projection ?? findOptions?.fields;
3488
- if (projection && typeof projection === 'object') {
3489
- cursor = cursor.project(projection);
3490
- }
3491
-
3492
- const sort = findOptions?.sort;
3493
- if (sort && typeof sort === 'object') {
3494
- cursor = cursor.sort(sort);
3495
- }
3496
-
3497
- const skip = findOptions?.skip;
3498
- if (typeof skip === 'number') {
3499
- cursor = cursor.skip(skip);
3500
- }
3501
-
3502
- const limit = findOptions?.limit;
3503
- if (typeof limit === 'number') {
3504
- cursor = cursor.limit(limit);
3505
- }
3506
-
3507
- return cursor;
3508
- }
3509
-
3510
- private static async measureExecution(
3511
- db: any,
3512
- collectionName: string,
3513
- pipeline?: any[],
3514
- filter?: Record<string, any>,
3515
- findOptions?: Record<string, any>,
3516
- aggregateOptions?: Record<string, any>
3517
- ): Promise<number> {
3518
- const start = Date.now();
3519
-
3520
- try {
3521
- if (Array.isArray(pipeline)) {
3522
- await db.collection(collectionName)
3523
- .aggregate(pipeline, {
3524
- ...(aggregateOptions || {}),
3525
- readPreference: 'secondaryPreferred'
3526
- })
3527
- .toArray();
3528
- }
3529
- else {
3530
- const cursor = SlowQueryVerifier.buildFindCursor(
3531
- db.collection(collectionName),
3532
- filter ?? {},
3533
- findOptions
3534
- );
3535
- await cursor.toArray();
3536
- }
3537
- }
3538
- catch (err) {
3539
- if (!SlowQueryVerifier.isBsonObjectTooLargeError(err)) {
3540
- console.error('Slow query measurement execution failed for', collectionName, err);
3541
- }
3542
- return -1;
3543
- }
3544
-
3545
- const duration = Date.now() - start;
3546
- return SlowQueryVerifier.isValidDuration(duration) ? duration : -1;
3547
- }
3548
-
3549
- private static normalizeExplainPayload(input?: any): Record<string, any> {
3550
- const cloned = SlowQueryVerifier.deepClone(input ?? {});
3551
- return typeof cloned === 'object' && cloned !== null ? cloned : {};
3552
- }
3553
-
3554
- private static sanitizeExplainPayload(payload: Record<string, any>, maxBytes = 2 * 1024 * 1024): Record<string, any> {
3555
- try {
3556
- const json = JSON.stringify(payload);
3557
- const bytes = Buffer.byteLength(json, 'utf8');
3558
- return bytes <= maxBytes ? payload : {};
3559
- }
3560
- catch {
3561
- return {};
3562
- }
3563
- }
3564
-
3565
- private static deepClone(value: any): any {
3566
- if (value === null || typeof value !== 'object') {
3567
- return value;
3568
- }
3569
-
3570
- if (Array.isArray(value)) {
3571
- return value.map(item => SlowQueryVerifier.deepClone(item));
3572
- }
3573
-
3574
- if (typeof value.toJSON === 'function') {
3575
- try {
3576
- return SlowQueryVerifier.deepClone(value.toJSON());
3577
- }
3578
- catch {
3579
- // fall through to manual clone
3580
- }
3581
- }
3582
-
3583
- const result: Record<string, any> = {};
3584
- Object.keys(value).forEach(key => {
3585
- result[key] = SlowQueryVerifier.deepClone(value[key]);
3586
- });
3587
-
3588
- return result;
3589
- }
3590
- }