@adonis-agora/durable 1.0.0-rc.2 → 1.0.0-rc.20

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 (398) hide show
  1. package/CHANGELOG.md +198 -0
  2. package/dist/assets/spa/assets/index-BgoNV3TR.css +1 -0
  3. package/dist/assets/spa/assets/index-DzXygyDm.js +57 -0
  4. package/dist/assets/spa/assets/index-DzXygyDm.js.map +1 -0
  5. package/dist/assets/spa/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
  6. package/dist/assets/spa/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
  7. package/dist/assets/spa/assets/jetbrains-mono-latin-500-normal-BWZEU5yA.woff2 +0 -0
  8. package/dist/assets/spa/assets/jetbrains-mono-latin-500-normal-CJOVTJB7.woff +0 -0
  9. package/dist/assets/spa/assets/jetbrains-mono-latin-600-normal-BfsvjouI.woff +0 -0
  10. package/dist/assets/spa/assets/jetbrains-mono-latin-600-normal-C8RAYTDA.woff2 +0 -0
  11. package/dist/assets/spa/assets/sora-latin-400-normal-CRt88UEn.woff2 +0 -0
  12. package/dist/assets/spa/assets/sora-latin-400-normal-OW7qkl5a.woff +0 -0
  13. package/dist/assets/spa/assets/sora-latin-500-normal-01eiPEn0.woff2 +0 -0
  14. package/dist/assets/spa/assets/sora-latin-500-normal-w58xtEt9.woff +0 -0
  15. package/dist/assets/spa/assets/sora-latin-600-normal-1_7fyUAY.woff +0 -0
  16. package/dist/assets/spa/assets/sora-latin-600-normal-Cdg4DaK0.woff2 +0 -0
  17. package/dist/assets/spa/assets/sora-latin-700-normal-9waGdLWo.woff2 +0 -0
  18. package/dist/assets/spa/assets/sora-latin-700-normal-BKPfQAnC.woff +0 -0
  19. package/dist/assets/spa/index.html +2 -8
  20. package/dist/commands/agora_doctor.d.ts +23 -0
  21. package/dist/commands/agora_doctor.d.ts.map +1 -0
  22. package/dist/commands/agora_doctor.js +110 -0
  23. package/dist/commands/agora_doctor.js.map +1 -0
  24. package/dist/commands/durable_doctor.d.ts +8 -0
  25. package/dist/commands/durable_doctor.d.ts.map +1 -0
  26. package/dist/commands/durable_doctor.js +8 -0
  27. package/dist/commands/durable_doctor.js.map +1 -0
  28. package/dist/commands/durable_worker.d.ts +3 -3
  29. package/dist/commands/durable_worker.js +7 -7
  30. package/dist/commands/durable_worker.js.map +1 -1
  31. package/dist/commands/health.d.ts +12 -0
  32. package/dist/commands/health.d.ts.map +1 -0
  33. package/dist/commands/health.js +34 -0
  34. package/dist/commands/health.js.map +1 -0
  35. package/dist/commands/lifecycle_republish.d.ts +8 -9
  36. package/dist/commands/lifecycle_republish.d.ts.map +1 -1
  37. package/dist/commands/lifecycle_republish.js +29 -16
  38. package/dist/commands/lifecycle_republish.js.map +1 -1
  39. package/dist/commands/main.d.ts +25 -5
  40. package/dist/commands/main.d.ts.map +1 -1
  41. package/dist/commands/main.js +54 -7
  42. package/dist/commands/main.js.map +1 -1
  43. package/dist/commands/runs.d.ts +9 -16
  44. package/dist/commands/runs.d.ts.map +1 -1
  45. package/dist/commands/runs.js +60 -47
  46. package/dist/commands/runs.js.map +1 -1
  47. package/dist/commands/runs_bulk.d.ts +17 -0
  48. package/dist/commands/runs_bulk.d.ts.map +1 -0
  49. package/dist/commands/runs_bulk.js +74 -0
  50. package/dist/commands/runs_bulk.js.map +1 -0
  51. package/dist/commands/runs_cancel.d.ts +15 -0
  52. package/dist/commands/runs_cancel.d.ts.map +1 -0
  53. package/dist/commands/runs_cancel.js +39 -0
  54. package/dist/commands/runs_cancel.js.map +1 -0
  55. package/dist/commands/runs_continue.d.ts +12 -0
  56. package/dist/commands/runs_continue.d.ts.map +1 -0
  57. package/dist/commands/runs_continue.js +28 -0
  58. package/dist/commands/runs_continue.js.map +1 -0
  59. package/dist/commands/runs_event.d.ts +13 -0
  60. package/dist/commands/runs_event.d.ts.map +1 -0
  61. package/dist/commands/runs_event.js +28 -0
  62. package/dist/commands/runs_event.js.map +1 -0
  63. package/dist/commands/runs_fan.d.ts +16 -0
  64. package/dist/commands/runs_fan.d.ts.map +1 -0
  65. package/dist/commands/runs_fan.js +58 -0
  66. package/dist/commands/runs_fan.js.map +1 -0
  67. package/dist/commands/runs_redispatch.d.ts +14 -0
  68. package/dist/commands/runs_redispatch.d.ts.map +1 -0
  69. package/dist/commands/runs_redispatch.js +35 -0
  70. package/dist/commands/runs_redispatch.js.map +1 -0
  71. package/dist/commands/runs_replay.d.ts +14 -0
  72. package/dist/commands/runs_replay.d.ts.map +1 -0
  73. package/dist/commands/runs_replay.js +38 -0
  74. package/dist/commands/runs_replay.js.map +1 -0
  75. package/dist/commands/runs_retry.d.ts +13 -0
  76. package/dist/commands/runs_retry.d.ts.map +1 -0
  77. package/dist/commands/runs_retry.js +29 -0
  78. package/dist/commands/runs_retry.js.map +1 -0
  79. package/dist/commands/runs_show.d.ts +14 -0
  80. package/dist/commands/runs_show.d.ts.map +1 -0
  81. package/dist/commands/runs_show.js +96 -0
  82. package/dist/commands/runs_show.js.map +1 -0
  83. package/dist/commands/runs_signal.d.ts +17 -0
  84. package/dist/commands/runs_signal.d.ts.map +1 -0
  85. package/dist/commands/runs_signal.js +49 -0
  86. package/dist/commands/runs_signal.js.map +1 -0
  87. package/dist/commands/runs_update.d.ts +15 -0
  88. package/dist/commands/runs_update.d.ts.map +1 -0
  89. package/dist/commands/runs_update.js +39 -0
  90. package/dist/commands/runs_update.js.map +1 -0
  91. package/dist/commands/runs_values.d.ts +15 -0
  92. package/dist/commands/runs_values.d.ts.map +1 -0
  93. package/dist/commands/runs_values.js +45 -0
  94. package/dist/commands/runs_values.js.map +1 -0
  95. package/dist/commands/schedules.d.ts +18 -0
  96. package/dist/commands/schedules.d.ts.map +1 -0
  97. package/dist/commands/schedules.js +70 -0
  98. package/dist/commands/schedules.js.map +1 -0
  99. package/dist/commands/schedules_pause.d.ts +16 -0
  100. package/dist/commands/schedules_pause.d.ts.map +1 -0
  101. package/dist/commands/schedules_pause.js +40 -0
  102. package/dist/commands/schedules_pause.js.map +1 -0
  103. package/dist/commands/schedules_resume.d.ts +8 -0
  104. package/dist/commands/schedules_resume.d.ts.map +1 -0
  105. package/dist/commands/schedules_resume.js +8 -0
  106. package/dist/commands/schedules_resume.js.map +1 -0
  107. package/dist/commands/schedules_trigger.d.ts +13 -0
  108. package/dist/commands/schedules_trigger.d.ts.map +1 -0
  109. package/dist/commands/schedules_trigger.js +32 -0
  110. package/dist/commands/schedules_trigger.js.map +1 -0
  111. package/dist/commands/tasks_complete.d.ts +15 -0
  112. package/dist/commands/tasks_complete.d.ts.map +1 -0
  113. package/dist/commands/tasks_complete.js +41 -0
  114. package/dist/commands/tasks_complete.js.map +1 -0
  115. package/dist/commands/tasks_fail.d.ts +16 -0
  116. package/dist/commands/tasks_fail.d.ts.map +1 -0
  117. package/dist/commands/tasks_fail.js +47 -0
  118. package/dist/commands/tasks_fail.js.map +1 -0
  119. package/dist/commands/workers.d.ts +11 -0
  120. package/dist/commands/workers.d.ts.map +1 -0
  121. package/dist/commands/workers.js +35 -0
  122. package/dist/commands/workers.js.map +1 -0
  123. package/dist/commands/workflows_lifecycle.d.ts +12 -0
  124. package/dist/commands/workflows_lifecycle.d.ts.map +1 -0
  125. package/dist/commands/workflows_lifecycle.js +37 -0
  126. package/dist/commands/workflows_lifecycle.js.map +1 -0
  127. package/dist/configure.d.ts +1 -1
  128. package/dist/configure.js +1 -1
  129. package/dist/providers/dashboard_provider.d.ts +57 -54
  130. package/dist/providers/dashboard_provider.d.ts.map +1 -1
  131. package/dist/providers/dashboard_provider.js +399 -288
  132. package/dist/providers/dashboard_provider.js.map +1 -1
  133. package/dist/providers/durable_provider.d.ts.map +1 -1
  134. package/dist/providers/durable_provider.js +67 -37
  135. package/dist/providers/durable_provider.js.map +1 -1
  136. package/dist/src/commands/console_command.d.ts +82 -0
  137. package/dist/src/commands/console_command.d.ts.map +1 -0
  138. package/dist/src/commands/console_command.js +238 -0
  139. package/dist/src/commands/console_command.js.map +1 -0
  140. package/dist/src/commands/runs.js +2 -2
  141. package/dist/src/commands/runs.js.map +1 -1
  142. package/dist/src/concurrency.d.ts.map +1 -1
  143. package/dist/src/concurrency.js +1 -0
  144. package/dist/src/concurrency.js.map +1 -1
  145. package/dist/src/config_types.d.ts +79 -26
  146. package/dist/src/config_types.d.ts.map +1 -1
  147. package/dist/src/dashboard/access_denied_page.d.ts +5 -7
  148. package/dist/src/dashboard/access_denied_page.d.ts.map +1 -1
  149. package/dist/src/dashboard/access_denied_page.js +2 -6
  150. package/dist/src/dashboard/access_denied_page.js.map +1 -1
  151. package/dist/src/dashboard/audit.d.ts +14 -0
  152. package/dist/src/dashboard/audit.d.ts.map +1 -0
  153. package/dist/src/dashboard/audit.js +36 -0
  154. package/dist/src/dashboard/audit.js.map +1 -0
  155. package/dist/src/dashboard/console_authorize.d.ts +98 -0
  156. package/dist/src/dashboard/console_authorize.d.ts.map +1 -0
  157. package/dist/src/dashboard/console_authorize.js +163 -0
  158. package/dist/src/dashboard/console_authorize.js.map +1 -0
  159. package/dist/src/dashboard/console_shell.d.ts +45 -0
  160. package/dist/src/dashboard/console_shell.d.ts.map +1 -0
  161. package/dist/src/dashboard/console_shell.js +125 -0
  162. package/dist/src/dashboard/console_shell.js.map +1 -0
  163. package/dist/src/dashboard/console_switcher.d.ts +132 -0
  164. package/dist/src/dashboard/console_switcher.d.ts.map +1 -0
  165. package/dist/src/dashboard/console_switcher.js +201 -0
  166. package/dist/src/dashboard/console_switcher.js.map +1 -0
  167. package/dist/src/dashboard/define_config.d.ts +68 -81
  168. package/dist/src/dashboard/define_config.d.ts.map +1 -1
  169. package/dist/src/dashboard/define_config.js +18 -112
  170. package/dist/src/dashboard/define_config.js.map +1 -1
  171. package/dist/src/dashboard/dev_tools.d.ts +102 -0
  172. package/dist/src/dashboard/dev_tools.d.ts.map +1 -0
  173. package/dist/src/dashboard/dev_tools.js +502 -0
  174. package/dist/src/dashboard/dev_tools.js.map +1 -0
  175. package/dist/src/dashboard/diagnostics-recorder.d.ts +1 -1
  176. package/dist/src/dashboard/diagnostics-recorder.js +1 -1
  177. package/dist/src/dashboard/gateway-adapter.d.ts +6 -6
  178. package/dist/src/dashboard/gateway-adapter.d.ts.map +1 -1
  179. package/dist/src/dashboard/gateway-adapter.js +5 -5
  180. package/dist/src/dashboard/gateway-adapter.js.map +1 -1
  181. package/dist/src/dashboard/handlers.d.ts +48 -39
  182. package/dist/src/dashboard/handlers.d.ts.map +1 -1
  183. package/dist/src/dashboard/handlers.js +181 -83
  184. package/dist/src/dashboard/handlers.js.map +1 -1
  185. package/dist/src/dashboard/index.d.ts +12 -9
  186. package/dist/src/dashboard/index.d.ts.map +1 -1
  187. package/dist/src/dashboard/index.js +10 -7
  188. package/dist/src/dashboard/index.js.map +1 -1
  189. package/dist/src/dashboard/list-filters.d.ts +27 -0
  190. package/dist/src/dashboard/list-filters.d.ts.map +1 -0
  191. package/dist/src/dashboard/list-filters.js +69 -0
  192. package/dist/src/dashboard/list-filters.js.map +1 -0
  193. package/dist/src/dashboard/openapi.d.ts.map +1 -1
  194. package/dist/src/dashboard/openapi.js +127 -52
  195. package/dist/src/dashboard/openapi.js.map +1 -1
  196. package/dist/src/dashboard/run-filter.d.ts +18 -15
  197. package/dist/src/dashboard/run-filter.d.ts.map +1 -1
  198. package/dist/src/dashboard/run-filter.js +30 -22
  199. package/dist/src/dashboard/run-filter.js.map +1 -1
  200. package/dist/src/dashboard/spa.d.ts +26 -1
  201. package/dist/src/dashboard/spa.d.ts.map +1 -1
  202. package/dist/src/dashboard/spa.js +9 -2
  203. package/dist/src/dashboard/spa.js.map +1 -1
  204. package/dist/src/dashboard/tenant_scope.d.ts +38 -0
  205. package/dist/src/dashboard/tenant_scope.d.ts.map +1 -0
  206. package/dist/src/dashboard/tenant_scope.js +166 -0
  207. package/dist/src/dashboard/tenant_scope.js.map +1 -0
  208. package/dist/src/define_config.d.ts +27 -12
  209. package/dist/src/define_config.d.ts.map +1 -1
  210. package/dist/src/define_config.js +9 -4
  211. package/dist/src/define_config.js.map +1 -1
  212. package/dist/src/diagnostics/index.d.ts +11 -0
  213. package/dist/src/diagnostics/index.d.ts.map +1 -0
  214. package/dist/src/diagnostics/index.js +6 -0
  215. package/dist/src/diagnostics/index.js.map +1 -0
  216. package/dist/src/diagnostics/transport.d.ts +71 -0
  217. package/dist/src/diagnostics/transport.d.ts.map +1 -0
  218. package/dist/src/diagnostics/transport.js +188 -0
  219. package/dist/src/diagnostics/transport.js.map +1 -0
  220. package/dist/src/diagnostics-bridge.d.ts +2 -1
  221. package/dist/src/diagnostics-bridge.d.ts.map +1 -1
  222. package/dist/src/diagnostics-bridge.js +7 -1
  223. package/dist/src/diagnostics-bridge.js.map +1 -1
  224. package/dist/src/doctor/agora_doctor.d.ts +116 -0
  225. package/dist/src/doctor/agora_doctor.d.ts.map +1 -0
  226. package/dist/src/doctor/agora_doctor.js +685 -0
  227. package/dist/src/doctor/agora_doctor.js.map +1 -0
  228. package/dist/src/doctor/checks.d.ts +28 -0
  229. package/dist/src/doctor/checks.d.ts.map +1 -0
  230. package/dist/src/doctor/checks.js +288 -0
  231. package/dist/src/doctor/checks.js.map +1 -0
  232. package/dist/src/doctor/index.d.ts +9 -0
  233. package/dist/src/doctor/index.d.ts.map +1 -0
  234. package/dist/src/doctor/index.js +16 -0
  235. package/dist/src/doctor/index.js.map +1 -0
  236. package/dist/src/engine.d.ts +55 -9
  237. package/dist/src/engine.d.ts.map +1 -1
  238. package/dist/src/engine.js +333 -84
  239. package/dist/src/engine.js.map +1 -1
  240. package/dist/src/errors.d.ts +9 -0
  241. package/dist/src/errors.d.ts.map +1 -1
  242. package/dist/src/errors.js +13 -0
  243. package/dist/src/errors.js.map +1 -1
  244. package/dist/src/event-trigger-bridge.d.ts +5 -14
  245. package/dist/src/event-trigger-bridge.d.ts.map +1 -1
  246. package/dist/src/event-trigger-bridge.js +11 -99
  247. package/dist/src/event-trigger-bridge.js.map +1 -1
  248. package/dist/src/index.d.ts +8 -5
  249. package/dist/src/index.d.ts.map +1 -1
  250. package/dist/src/index.js +6 -4
  251. package/dist/src/index.js.map +1 -1
  252. package/dist/src/interfaces.d.ts +53 -9
  253. package/dist/src/interfaces.d.ts.map +1 -1
  254. package/dist/src/interfaces.js +8 -0
  255. package/dist/src/interfaces.js.map +1 -1
  256. package/dist/src/origin.js +3 -3
  257. package/dist/src/role_bindings.d.ts +8 -8
  258. package/dist/src/role_bindings.js +8 -8
  259. package/dist/src/run-gateway/interface.d.ts +4 -3
  260. package/dist/src/run-gateway/interface.d.ts.map +1 -1
  261. package/dist/src/run-gateway/proxy-run-gateway.d.ts +4 -2
  262. package/dist/src/run-gateway/proxy-run-gateway.d.ts.map +1 -1
  263. package/dist/src/run-gateway/proxy-run-gateway.js +21 -6
  264. package/dist/src/run-gateway/proxy-run-gateway.js.map +1 -1
  265. package/dist/src/run-gateway/run-request-responder.d.ts +6 -1
  266. package/dist/src/run-gateway/run-request-responder.d.ts.map +1 -1
  267. package/dist/src/run-gateway/run-request-responder.js +30 -3
  268. package/dist/src/run-gateway/run-request-responder.js.map +1 -1
  269. package/dist/src/run-gateway/store-run-gateway.d.ts +4 -4
  270. package/dist/src/run-gateway/store-run-gateway.d.ts.map +1 -1
  271. package/dist/src/run-gateway/store-run-gateway.js +4 -4
  272. package/dist/src/run-gateway/store-run-gateway.js.map +1 -1
  273. package/dist/src/run-gateway/tenant-auth.d.ts +19 -1
  274. package/dist/src/run-gateway/tenant-auth.d.ts.map +1 -1
  275. package/dist/src/run-gateway/tenant-auth.js +133 -0
  276. package/dist/src/run-gateway/tenant-auth.js.map +1 -1
  277. package/dist/src/run-gateway/tenant-scoped-gateway.d.ts +10 -0
  278. package/dist/src/run-gateway/tenant-scoped-gateway.d.ts.map +1 -0
  279. package/dist/src/run-gateway/tenant-scoped-gateway.js +107 -0
  280. package/dist/src/run-gateway/tenant-scoped-gateway.js.map +1 -0
  281. package/dist/src/run-value-facets.d.ts +1 -1
  282. package/dist/src/run-value-facets.d.ts.map +1 -1
  283. package/dist/src/run-value-facets.js +5 -1
  284. package/dist/src/run-value-facets.js.map +1 -1
  285. package/dist/src/scheduler.d.ts.map +1 -1
  286. package/dist/src/scheduler.js +2 -1
  287. package/dist/src/scheduler.js.map +1 -1
  288. package/dist/src/schedules.d.ts +16 -1
  289. package/dist/src/schedules.d.ts.map +1 -1
  290. package/dist/src/schedules.js +29 -4
  291. package/dist/src/schedules.js.map +1 -1
  292. package/dist/src/services/main.d.ts +3 -3
  293. package/dist/src/services/main.js +6 -6
  294. package/dist/src/services/main.js.map +1 -1
  295. package/dist/src/singleton-admission.d.ts.map +1 -1
  296. package/dist/src/singleton-admission.js +2 -0
  297. package/dist/src/singleton-admission.js.map +1 -1
  298. package/dist/src/singleton-gate.d.ts +1 -1
  299. package/dist/src/singleton-gate.d.ts.map +1 -1
  300. package/dist/src/singleton-gate.js +4 -3
  301. package/dist/src/singleton-gate.js.map +1 -1
  302. package/dist/src/sql-schema.d.ts +54 -0
  303. package/dist/src/sql-schema.d.ts.map +1 -0
  304. package/dist/src/sql-schema.js +145 -0
  305. package/dist/src/sql-schema.js.map +1 -0
  306. package/dist/src/stores/factory.d.ts +6 -0
  307. package/dist/src/stores/factory.d.ts.map +1 -1
  308. package/dist/src/stores/factory.js +3 -0
  309. package/dist/src/stores/factory.js.map +1 -1
  310. package/dist/src/stores/lucid-mappers.d.ts +3 -0
  311. package/dist/src/stores/lucid-mappers.d.ts.map +1 -1
  312. package/dist/src/stores/lucid-mappers.js +12 -0
  313. package/dist/src/stores/lucid-mappers.js.map +1 -1
  314. package/dist/src/stores/lucid-pg-hot-path.d.ts +6 -2
  315. package/dist/src/stores/lucid-pg-hot-path.d.ts.map +1 -1
  316. package/dist/src/stores/lucid-pg-hot-path.js +7 -3
  317. package/dist/src/stores/lucid-pg-hot-path.js.map +1 -1
  318. package/dist/src/stores/lucid-schema.d.ts +18 -4
  319. package/dist/src/stores/lucid-schema.d.ts.map +1 -1
  320. package/dist/src/stores/lucid-schema.js +106 -61
  321. package/dist/src/stores/lucid-schema.js.map +1 -1
  322. package/dist/src/stores/lucid.d.ts +42 -2
  323. package/dist/src/stores/lucid.d.ts.map +1 -1
  324. package/dist/src/stores/lucid.js +450 -231
  325. package/dist/src/stores/lucid.js.map +1 -1
  326. package/dist/src/tenancy.d.ts +104 -0
  327. package/dist/src/tenancy.d.ts.map +1 -0
  328. package/dist/src/tenancy.js +91 -0
  329. package/dist/src/tenancy.js.map +1 -0
  330. package/dist/src/testing/in-memory-state-store.d.ts.map +1 -1
  331. package/dist/src/testing/in-memory-state-store.js +9 -3
  332. package/dist/src/testing/in-memory-state-store.js.map +1 -1
  333. package/dist/src/testing-kit/state-store-conformance.d.ts.map +1 -1
  334. package/dist/src/testing-kit/state-store-conformance.js +59 -0
  335. package/dist/src/testing-kit/state-store-conformance.js.map +1 -1
  336. package/dist/src/testing-kit/transport-conformance.d.ts.map +1 -1
  337. package/dist/src/testing-kit/transport-conformance.js +5 -1
  338. package/dist/src/testing-kit/transport-conformance.js.map +1 -1
  339. package/dist/src/transports/bullmq/bullmq-transport.d.ts +1 -1
  340. package/dist/src/transports/bullmq/bullmq-transport.d.ts.map +1 -1
  341. package/dist/src/transports/bullmq/bullmq-transport.js +14 -3
  342. package/dist/src/transports/bullmq/bullmq-transport.js.map +1 -1
  343. package/dist/src/transports/db-schema.d.ts +13 -5
  344. package/dist/src/transports/db-schema.d.ts.map +1 -1
  345. package/dist/src/transports/db-schema.js +52 -27
  346. package/dist/src/transports/db-schema.js.map +1 -1
  347. package/dist/src/transports/db.d.ts +6 -0
  348. package/dist/src/transports/db.d.ts.map +1 -1
  349. package/dist/src/transports/db.js +91 -42
  350. package/dist/src/transports/db.js.map +1 -1
  351. package/dist/src/transports/factory.d.ts +16 -0
  352. package/dist/src/transports/factory.d.ts.map +1 -1
  353. package/dist/src/transports/factory.js +6 -1
  354. package/dist/src/transports/factory.js.map +1 -1
  355. package/dist/src/transports/queue.d.ts +2 -2
  356. package/dist/src/transports/queue.d.ts.map +1 -1
  357. package/dist/src/transports/queue.js +49 -8
  358. package/dist/src/transports/queue.js.map +1 -1
  359. package/dist/src/transports/worker-keys-db.d.ts +5 -0
  360. package/dist/src/transports/worker-keys-db.d.ts.map +1 -1
  361. package/dist/src/transports/worker-keys-db.js +10 -8
  362. package/dist/src/transports/worker-keys-db.js.map +1 -1
  363. package/dist/src/workflow-discovery.d.ts +1 -1
  364. package/dist/src/workflow-discovery.d.ts.map +1 -1
  365. package/dist/src/workflow-discovery.js +4 -7
  366. package/dist/src/workflow-discovery.js.map +1 -1
  367. package/dist/src/workflow-events.d.ts +20 -51
  368. package/dist/src/workflow-events.d.ts.map +1 -1
  369. package/dist/src/workflow-events.js +12 -30
  370. package/dist/src/workflow-events.js.map +1 -1
  371. package/dist/src/workflow-ref.d.ts +1 -1
  372. package/dist/stubs/config/durable_dashboard.stub +9 -29
  373. package/dist/stubs/database/migrations/create_durable_tables.stub +12 -2
  374. package/dist/stubs/database/migrations/create_durable_transport_messages.stub +34 -5
  375. package/dist/stubs/database/migrations/create_durable_transport_tables.stub +30 -8
  376. package/dist/stubs/database/migrations/create_durable_transport_worker_keys.stub +24 -2
  377. package/package.json +25 -7
  378. package/skills/durable-cluster/SKILL.md +43 -23
  379. package/skills/durable-reliability/SKILL.md +13 -7
  380. package/dist/assets/spa/assets/index-70hGzOnT.css +0 -1
  381. package/dist/assets/spa/assets/index-CJjMAUau.js +0 -16
  382. package/dist/assets/spa/assets/index-CJjMAUau.js.map +0 -1
  383. package/dist/commands/retry.d.ts +0 -16
  384. package/dist/commands/retry.d.ts.map +0 -1
  385. package/dist/commands/retry.js +0 -34
  386. package/dist/commands/retry.js.map +0 -1
  387. package/dist/src/dashboard/auth.d.ts +0 -134
  388. package/dist/src/dashboard/auth.d.ts.map +0 -1
  389. package/dist/src/dashboard/auth.js +0 -156
  390. package/dist/src/dashboard/auth.js.map +0 -1
  391. package/dist/src/dashboard/login_page.d.ts +0 -36
  392. package/dist/src/dashboard/login_page.d.ts.map +0 -1
  393. package/dist/src/dashboard/login_page.js +0 -158
  394. package/dist/src/dashboard/login_page.js.map +0 -1
  395. package/dist/src/dashboard/session_cookie.d.ts +0 -45
  396. package/dist/src/dashboard/session_cookie.d.ts.map +0 -1
  397. package/dist/src/dashboard/session_cookie.js +0 -97
  398. package/dist/src/dashboard/session_cookie.js.map +0 -1
@@ -36,6 +36,7 @@ import { SINGLETON_RESERVED_TAGS, withoutAdmission, withSingletonTracked, } from
36
36
  import { SingletonGate } from './singleton-gate.js';
37
37
  import { validateStandard, validateStandardSync, } from './standard-schema.js';
38
38
  import { StickyCache, StickyContinuation } from './sticky-continuation.js';
39
+ import { intersectTenantIds, runTenantOf, runVisible, tenantScopeIds, } from './tenancy.js';
39
40
  import { sanitizeQueueToken, tenantGroup } from './tenant-group.js';
40
41
  import { TransportPool } from './transport-pool.js';
41
42
  import { workflowAls } from './workflow-als.js';
@@ -72,6 +73,13 @@ const isBreakpoint = (cp) => cp.status === 'pending' && cp.name.startsWith(BREAK
72
73
  * through some other path (an operator's cancel, a timeout sweep, a dead-letter) while this turn's
73
74
  * outcome was being computed from a possibly-stale snapshot.
74
75
  */
76
+ /** Statuses a wake refused by a held lease may stamp a request for another turn on. */
77
+ const WAKEABLE_RUN_STATUSES = ['running', 'suspended', 'blocked', 'cancelling'];
78
+ /**
79
+ * Statuses a wake that finds the lease free re-drives: parked runs, and a `cancelling` run whose
80
+ * undo is not executing (its turn unwinds the saga).
81
+ */
82
+ const PARKED_RUN_STATUSES = ['suspended', 'blocked', 'cancelling'];
75
83
  const SETTLE_ELIGIBLE_STATUSES = [
76
84
  'pending',
77
85
  'running',
@@ -247,7 +255,13 @@ function isNewerVersion(a, b) {
247
255
  return a.localeCompare(b, undefined, { numeric: true }) > 0;
248
256
  }
249
257
  /** The statuses a run can be skipped in for an unregistered workflow — every non-terminal one. */
250
- const UNREGISTERED_DEAD_LETTER_FROM = ['pending', 'running', 'suspended', 'blocked'];
258
+ const UNREGISTERED_DEAD_LETTER_FROM = [
259
+ 'pending',
260
+ 'running',
261
+ 'suspended',
262
+ 'blocked',
263
+ 'cancelling',
264
+ ];
251
265
  /** Thrown by {@link WorkflowEngine.resume} when the run belongs to a different namespace. */
252
266
  export class NamespaceMismatch extends Error {
253
267
  constructor() {
@@ -325,6 +339,7 @@ export class WorkflowEngine {
325
339
  context;
326
340
  /** Establish the originating context ambiently around a local step body (see {@link WorkflowEngineDeps.rehydrate}). Default passthrough. */
327
341
  rehydrate;
342
+ tenancy;
328
343
  compensationRetries;
329
344
  compensationTimeoutMs;
330
345
  retention;
@@ -402,6 +417,8 @@ export class WorkflowEngine {
402
417
  #allSchedules = [];
403
418
  /** Runtime pause overrides by schedule key (console-issued; win over the config's `paused`). */
404
419
  #schedulePauseOverrides = new Map();
420
+ /** Schedule-pause broadcasts still being handed to the control plane (see {@link flushControl}). */
421
+ #controlInFlight = new Set();
405
422
  /** Flow-control admission backend for remote steps (see {@link registerQueue}). */
406
423
  admission;
407
424
  /** Runs on THIS instance blocked on admission, by queue — woken early on a freed-slot signal. */
@@ -602,6 +619,7 @@ export class WorkflowEngine {
602
619
  this.context = deps.context;
603
620
  // Default passthrough: with no bridge supplied, a local step body runs exactly as before.
604
621
  this.rehydrate = deps.rehydrate ?? ((_carrier, fn) => fn());
622
+ this.tenancy = deps.tenancy;
605
623
  this.compensationRetries = Math.max(1, deps.compensationRetries ?? 1);
606
624
  this.compensationTimeoutMs = Math.max(1_000, deps.compensationTimeoutMs ?? 300_000);
607
625
  this.retention = deps.retention;
@@ -613,6 +631,8 @@ export class WorkflowEngine {
613
631
  start: (workflow, input, runId, opts) => this.start(workflow, input, runId, opts),
614
632
  getRun: (runId) => this.store.getRun(runId),
615
633
  isDraining: () => this.draining,
634
+ tenant: () => this.startTenant(undefined),
635
+ visible: (record) => this.runVisibleNow({ tenantId: record.tenantId }),
616
636
  }, { namespace: this.namespace, clock: this.clock });
617
637
  this.stalledAfterMs = Math.max(60_000, deps.stalledAfterMs ?? 900_000);
618
638
  this.trackStepStart = deps.trackStepStart ?? true;
@@ -1007,12 +1027,23 @@ export class WorkflowEngine {
1007
1027
  return false;
1008
1028
  this.#schedulePauseOverrides.set(key, paused);
1009
1029
  if (this.controlPlane) {
1010
- void this.controlPlane
1030
+ const sent = this.controlPlane
1011
1031
  .publishControl({ kind: 'schedulePause', key, paused, from: this.instanceId })
1012
- .catch(() => undefined);
1032
+ .catch(() => undefined)
1033
+ .finally(() => this.#controlInFlight.delete(sent));
1034
+ this.#controlInFlight.add(sent);
1013
1035
  }
1014
1036
  return true;
1015
1037
  }
1038
+ /**
1039
+ * Wait until the schedule pauses this instance broadcast have been handed to the control plane. A
1040
+ * short-lived process (an ace command) calls it before it exits. Resolves `false` when the engine
1041
+ * has no control plane, so a pause reached this process only.
1042
+ */
1043
+ async flushControl() {
1044
+ await Promise.allSettled([...this.#controlInFlight]);
1045
+ return this.controlPlane !== undefined;
1046
+ }
1016
1047
  /**
1017
1048
  * Fire a schedule's CURRENT window now (console "run now"). Idempotent by the window's
1018
1049
  * deterministic run id — triggering a window that already fired returns the existing run instead
@@ -1452,6 +1483,8 @@ export class WorkflowEngine {
1452
1483
  };
1453
1484
  this.trackEffect(this.start(target, input, `dlq:${run.id}`, {
1454
1485
  ...(run.namespace !== undefined ? { namespace: run.namespace } : {}),
1486
+ tenantId: run.tenantId ?? null,
1487
+ ...(run.tenantKind ? { tenantKind: run.tenantKind } : {}),
1455
1488
  }).catch((err) => {
1456
1489
  console.warn(`[adonis-durable] could not start dead-letter workflow ${target} for run ${run.id}: ${err instanceof Error ? err.message : String(err)}`);
1457
1490
  }));
@@ -2535,6 +2568,11 @@ export class WorkflowEngine {
2535
2568
  */
2536
2569
  async republishLifecycle(target) {
2537
2570
  const result = { runs: 0, started: 0, failed: [] };
2571
+ if ('runId' in target &&
2572
+ typeof target.runId === 'string' &&
2573
+ (await this.runHidden(target.runId))) {
2574
+ return result;
2575
+ }
2538
2576
  // A backfill usually follows a deploy: read the fleet's subscriptions now, not from the cache.
2539
2577
  await this.refreshAnnouncedSubscriptions();
2540
2578
  const publish = async (run) => {
@@ -2779,6 +2817,9 @@ export class WorkflowEngine {
2779
2817
  // Idempotent by run id: a redelivered trigger (at-least-once queues) or a scheduler re-tick for
2780
2818
  // the same id is a no-op, returning the existing run's state instead of starting a duplicate.
2781
2819
  const existing = async (prior) => {
2820
+ // Another tenant's run id: answer like a clash, never with that run's state.
2821
+ if (!this.runVisibleNow(prior))
2822
+ throw new Error(`run ${runId} already exists`);
2782
2823
  // A PENDING prior still needs an executor: re-nudge the dispatcher so a redelivered trigger —
2783
2824
  // or a continuation persisted pre-settle (see the ContinueAsNew handoff) picked up by a custom
2784
2825
  // dispatcher — doesn't sit `pending` until the next poll. Idempotent: execution is leased, so
@@ -2827,6 +2868,8 @@ export class WorkflowEngine {
2827
2868
  // Compared against the ENGINE clock (injectable), not wall time — the same clock the timer
2828
2869
  // poller will use to decide the run is due.
2829
2870
  const delayed = startAtMs !== undefined && startAtMs > this.clock();
2871
+ const tenant = this.startTenant(opts);
2872
+ const tenantNamespace = tenant && opts?.namespace === undefined ? this.tenancy?.namespace?.(tenant) : undefined;
2830
2873
  const run = {
2831
2874
  id: runId,
2832
2875
  workflow: name,
@@ -2834,7 +2877,10 @@ export class WorkflowEngine {
2834
2877
  status: delayed ? 'suspended' : 'pending',
2835
2878
  ...(delayed ? { wakeAt: startAtMs } : {}),
2836
2879
  origin: opts?.origin ?? registered.origin,
2837
- namespace: opts?.namespace ?? this.namespace,
2880
+ namespace: opts?.namespace ?? tenantNamespace ?? this.namespace,
2881
+ ...(tenant
2882
+ ? { tenantId: tenant.id, ...(tenant.kind ? { tenantKind: tenant.kind } : {}) }
2883
+ : {}),
2838
2884
  input,
2839
2885
  tags,
2840
2886
  searchAttributes: opts?.searchAttributes,
@@ -2897,6 +2943,16 @@ export class WorkflowEngine {
2897
2943
  * `limiter` bounds how many of these run at once (a fan's children wait in it as ids).
2898
2944
  */
2899
2945
  handoffRun(workflow, input, runId, opts, onStartError, onAlreadySettled, limiter) {
2946
+ // The tenant is read NOW, inside the parent's turn: a bounded handoff may run later, elsewhere.
2947
+ if (opts?.tenantId === undefined) {
2948
+ const tenant = this.startTenant(opts);
2949
+ if (tenant)
2950
+ opts = {
2951
+ ...opts,
2952
+ tenantId: tenant.id,
2953
+ ...(tenant.kind ? { tenantKind: tenant.kind } : {}),
2954
+ };
2955
+ }
2900
2956
  const handoff = async () => {
2901
2957
  // Defer one microtask: never reentrantly resume the parent that is still suspending/settling.
2902
2958
  await Promise.resolve();
@@ -2936,8 +2992,54 @@ export class WorkflowEngine {
2936
2992
  this.trackEffect(limiter ? limiter.submit(runId, handoff) : handoff());
2937
2993
  }
2938
2994
  /** Read a run's current persisted state (or null if unknown). A thin pass-through to the store. */
2939
- getRun(runId) {
2940
- return this.store.getRun(runId);
2995
+ async getRun(runId) {
2996
+ const run = await this.store.getRun(runId);
2997
+ return run && this.runVisibleNow(run) ? run : null;
2998
+ }
2999
+ /** The tenant a new run gets: the explicit option, else the active tenant (tenancy on), else none. */
3000
+ startTenant(opts) {
3001
+ if (opts?.tenantId === null)
3002
+ return undefined;
3003
+ if (typeof opts?.tenantId === 'string' && opts.tenantId !== '') {
3004
+ return opts.tenantKind ? { id: opts.tenantId, kind: opts.tenantKind } : { id: opts.tenantId };
3005
+ }
3006
+ const filter = this.tenantFilterNow();
3007
+ return filter.mode === 'tenant' ? filter.tenant : undefined;
3008
+ }
3009
+ /** How the current app-facing call is scoped (`system` inside the engine's own work). */
3010
+ tenantFilterNow() {
3011
+ if (!this.tenancy || engineInternal.getStore() === true)
3012
+ return { mode: 'system' };
3013
+ return this.tenancy.filter();
3014
+ }
3015
+ /** Whether the current call may see `run` (always, with tenancy off). */
3016
+ runVisibleNow(run) {
3017
+ if (!this.tenancy)
3018
+ return true;
3019
+ return runVisible(this.tenantFilterNow(), run, this.tenancy.nullRows);
3020
+ }
3021
+ /** Whether `runId` exists but the current call may not see it (an app-facing guard: answer not-found). */
3022
+ async runHidden(runId) {
3023
+ if (!this.tenancy)
3024
+ return false;
3025
+ const filter = this.tenantFilterNow();
3026
+ if (filter.mode === 'system' || filter.mode === 'all')
3027
+ return false;
3028
+ const run = await this.store.getRun(runId);
3029
+ return run !== null && !runVisible(filter, run, this.tenancy.nullRows);
3030
+ }
3031
+ /** `query` confined to the runs the current call may see. */
3032
+ tenantScoped(query) {
3033
+ if (!this.tenancy)
3034
+ return query;
3035
+ const ids = tenantScopeIds(this.tenantFilterNow(), this.tenancy.nullRows);
3036
+ if (ids === undefined)
3037
+ return query;
3038
+ return { ...query, tenantIds: intersectTenantIds(query.tenantIds, ids) };
3039
+ }
3040
+ /** Run `fn` as the engine's own work: app-facing tenant guards do not apply inside it. */
3041
+ asEngine(fn) {
3042
+ return this.tenancy ? engineInternal.run(true, fn) : fn();
2941
3043
  }
2942
3044
  /**
2943
3045
  * List persisted runs matching `query` (newest activity first, per the store's own ordering). The
@@ -2945,7 +3047,7 @@ export class WorkflowEngine {
2945
3047
  * through the engine rather than reaching for its private store.
2946
3048
  */
2947
3049
  async listRuns(query) {
2948
- const scoped = withoutBookkeepingRuns(query);
3050
+ const scoped = withoutBookkeepingRuns(this.tenantScoped(query));
2949
3051
  const runs = await this.store.listRuns(scoped);
2950
3052
  const hidden = scoped.excludeWorkflows;
2951
3053
  return hidden?.length ? runs.filter((run) => !hidden.includes(run.workflow)) : runs;
@@ -2957,14 +3059,16 @@ export class WorkflowEngine {
2957
3059
  * {@link listRuns}.
2958
3060
  */
2959
3061
  runValueFacets(axis, query, opts) {
2960
- const scoped = withoutBookkeepingRuns(query);
3062
+ const scoped = withoutBookkeepingRuns(this.tenantScoped(query));
2961
3063
  if (this.store.runValueFacets) {
2962
3064
  return this.store.runValueFacets(axis, scoped, opts);
2963
3065
  }
2964
3066
  return scanRunValueFacets(this.store, axis, scoped, opts);
2965
3067
  }
2966
3068
  /** List a run's step checkpoints (its timeline). Part of the engine's read API; see {@link listRuns}. */
2967
- listCheckpoints(runId) {
3069
+ async listCheckpoints(runId) {
3070
+ if (await this.runHidden(runId))
3071
+ return [];
2968
3072
  return this.store.listCheckpoints(runId);
2969
3073
  }
2970
3074
  /**
@@ -2972,7 +3076,7 @@ export class WorkflowEngine {
2972
3076
  * has {@link StateStore.runFacets} (else counted over a listing). Part of the engine's read API.
2973
3077
  */
2974
3078
  runFacets(query) {
2975
- return runFacetsOf(this.store, withoutBookkeepingRuns(query));
3079
+ return runFacetsOf(this.store, withoutBookkeepingRuns(this.tenantScoped(query)));
2976
3080
  }
2977
3081
  /**
2978
3082
  * A run's detail (see `loadRunDetail`), built from the store's checkpoint aggregates when it has
@@ -2980,6 +3084,8 @@ export class WorkflowEngine {
2980
3084
  * instead of the whole history, and carries an `etag` that `options.ifNoneMatch` can match.
2981
3085
  */
2982
3086
  async getRunDetail(runId, options) {
3087
+ if (await this.runHidden(runId))
3088
+ return null;
2983
3089
  const source = this.detailSource();
2984
3090
  const withExtras = async (detail) => {
2985
3091
  const extras = await this.lifecycleDetail(detail.run);
@@ -3117,7 +3223,9 @@ export class WorkflowEngine {
3117
3223
  return out;
3118
3224
  }
3119
3225
  /** One page of a summarized fan's members (see `loadFanMembers`), versioned like {@link getRunDetail}. */
3120
- getFanMembers(runId, group, query) {
3226
+ async getFanMembers(runId, group, query) {
3227
+ if (await this.runHidden(runId))
3228
+ return null;
3121
3229
  return loadFanMembers(this.detailSource(), runId, group, query);
3122
3230
  }
3123
3231
  detailSource() {
@@ -3225,7 +3333,7 @@ export class WorkflowEngine {
3225
3333
  return [];
3226
3334
  this.#lastStalledSweepAt = now;
3227
3335
  const candidates = await this.store.listRuns({
3228
- statuses: ['suspended', 'running'],
3336
+ statuses: ['suspended', 'running', 'cancelling'],
3229
3337
  namespace: this.namespace,
3230
3338
  updatedBefore: now - this.stalledAfterMs,
3231
3339
  size: 100,
@@ -3455,7 +3563,7 @@ export class WorkflowEngine {
3455
3563
  }
3456
3564
  async deleteRun(runId) {
3457
3565
  const run = await this.store.getRun(runId);
3458
- if (!run)
3566
+ if (!run || !this.runVisibleNow(run))
3459
3567
  return 0;
3460
3568
  // Collect children BEFORE deleting this run's checkpoints (getRunChildren reads them).
3461
3569
  let deleted = 0;
@@ -3469,11 +3577,10 @@ export class WorkflowEngine {
3469
3577
  /**
3470
3578
  * Sweep a deleted run's token-keyed buffered signals — they are not run-scoped rows, so the store's
3471
3579
  * deleteRun can't cascade them. Without this every fire-and-forget child that was never joined leaks
3472
- * its buffered `child:<id>` completion FOREVER (one row per spawn), and a raced compensate-cancel
3473
- * can leave a `cancel:<id>` marker behind.
3580
+ * its buffered `child:<id>` completion FOREVER (one row per spawn).
3474
3581
  */
3475
3582
  async sweepRunBufferedSignals(runId) {
3476
- for (const token of [`child:${runId}`, `cancel:${runId}`]) {
3583
+ for (const token of [`child:${runId}`]) {
3477
3584
  for (;;) {
3478
3585
  const buffered = await this.store.takeBufferedSignal(token).catch(() => null);
3479
3586
  if (!buffered)
@@ -3541,7 +3648,7 @@ export class WorkflowEngine {
3541
3648
  if (!TERMINAL_RUN_STATUSES.includes(status)) {
3542
3649
  if (!cancelLive)
3543
3650
  continue;
3544
- await this.cancel(id).catch(() => undefined);
3651
+ await this.asEngine(() => this.cancel(id)).catch(() => undefined);
3545
3652
  }
3546
3653
  doomed.push(id);
3547
3654
  }
@@ -3706,7 +3813,7 @@ export class WorkflowEngine {
3706
3813
  // Piggybacked on the existing store.getRun above — no extra async step on the happy path. An
3707
3814
  // undefined namespace (a store row created before the field existed) is treated as "belongs to
3708
3815
  // everyone" for back-compat: don't skip it. An operator (this.namespace undefined) belongs to
3709
- // every namespace too, so the guard never applies to it — that is the whole point of the role.
3816
+ // every namespace too, so the guard never applies to it — that is the whole point of the topology.
3710
3817
  if (this.namespace !== undefined &&
3711
3818
  run.namespace !== undefined &&
3712
3819
  run.namespace !== this.namespace) {
@@ -3725,7 +3832,7 @@ export class WorkflowEngine {
3725
3832
  }
3726
3833
  if (!registered)
3727
3834
  throw new WorkflowNotRegistered(run.workflow, run.workflowVersion);
3728
- return this.track(runId, this.execute(run, registered.fn));
3835
+ return this.track(runId, this.inRunTenant(run, () => this.execute(run, registered.fn)));
3729
3836
  }
3730
3837
  /**
3731
3838
  * Deferred re-drive after a suspended settle whose waited checkpoint(s) already settled — the
@@ -4069,6 +4176,27 @@ export class WorkflowEngine {
4069
4176
  // finished run) and re-enqueues it for a second execution. Only a run that still reads
4070
4177
  // `running` under the lease is an orphan; release the lease taken just to look at anything else.
4071
4178
  const run = await this.store.getRun(listed.id);
4179
+ // A compensating cancel whose undo died with its pod (or never started: the pod that took the
4180
+ // cancel crashed before its resume ran). Re-drive it rather than re-pend it, which would drop
4181
+ // the cancel: any turn of a `cancelling` run unwinds the saga and settles it `cancelled`.
4182
+ if (run?.status === 'cancelling') {
4183
+ // Only a pod that has the workflow can run its compensations; past the grace, one that is
4184
+ // registered nowhere is dead-lettered like any orphan.
4185
+ if (!this.isRegisteredHere(run) && !(await this.canResolveWorkflow(run))) {
4186
+ const settled = await this.settleUnregistered(run, nowMs);
4187
+ if (settled)
4188
+ results.push(settled);
4189
+ else if (settled === undefined)
4190
+ this.logUnregisteredSkip(run);
4191
+ continue;
4192
+ }
4193
+ await this.store.releaseRunLock(listed.id, this.instanceId);
4194
+ this.trackEffect(Promise.resolve(this.wake(run.id)).catch((error) => {
4195
+ console.error(`[adonis-durable] could not re-drive cancelling run ${run.id}; retrying on a later tick:`, error);
4196
+ }));
4197
+ results.push({ runId: run.id, status: 'cancelling' });
4198
+ continue;
4199
+ }
4072
4200
  if (run?.status !== 'running') {
4073
4201
  await this.store.releaseRunLock(listed.id, this.instanceId);
4074
4202
  continue;
@@ -4547,10 +4675,16 @@ export class WorkflowEngine {
4547
4675
  const seen = _seen ?? new Set();
4548
4676
  if (seen.has(runId))
4549
4677
  return null;
4678
+ if (_seen === undefined && (await this.runHidden(runId)))
4679
+ return null;
4550
4680
  seen.add(runId);
4551
4681
  const run = await this.store.getRun(runId);
4552
4682
  if (!run)
4553
4683
  return null;
4684
+ // A run undoing its saga for a compensating cancel is not retried over: re-pending it would drop
4685
+ // the cancel. It ends `cancelled`, and can be requeued from there.
4686
+ if (run.status === 'cancelling')
4687
+ return { runId, status: 'cancelling' };
4554
4688
  // A FAILED/DEAD run's replay deterministically re-throws its recorded failures — an exhausted
4555
4689
  // failed checkpoint rethrows, and an awaited child's failure completion rethrows through
4556
4690
  // unwrapCompletion — so "retry" without resetting that state is a no-op that re-fails in
@@ -4691,7 +4825,7 @@ export class WorkflowEngine {
4691
4825
  */
4692
4826
  async redispatchPending(runId) {
4693
4827
  const run = await this.store.getRun(runId);
4694
- if (!run)
4828
+ if (!run || !this.runVisibleNow(run))
4695
4829
  return null;
4696
4830
  const pending = (await this.store.listCheckpoints(runId)).filter((cp) => cp.kind === 'remote' && cp.status === 'pending');
4697
4831
  let redispatched = 0;
@@ -4727,12 +4861,18 @@ export class WorkflowEngine {
4727
4861
  */
4728
4862
  async retryWithInput(runId, input, newRunId) {
4729
4863
  const run = await this.store.getRun(runId);
4730
- if (!run)
4864
+ if (!run || !this.runVisibleNow(run))
4731
4865
  return null;
4732
4866
  const id = newRunId ?? `${runId}~retry~${globalThis.crypto.randomUUID().slice(0, 8)}`;
4733
4867
  // Inherit the original run's namespace so a retry lands in the SAME worker-pool partition (an
4734
- // operator-routed run keeps its tenant on retry, rather than falling back to the engine's own).
4735
- await this.start(run.workflow, input, id, { tags: run.tags, namespace: run.namespace });
4868
+ // operator-routed run keeps its pool on retry, rather than falling back to the engine's own),
4869
+ // and its tenant (or none).
4870
+ await this.start(run.workflow, input, id, {
4871
+ tags: run.tags,
4872
+ namespace: run.namespace,
4873
+ tenantId: run.tenantId ?? null,
4874
+ ...(run.tenantKind ? { tenantKind: run.tenantKind } : {}),
4875
+ });
4736
4876
  return { runId: id };
4737
4877
  }
4738
4878
  /**
@@ -4821,8 +4961,10 @@ export class WorkflowEngine {
4821
4961
  // (`awaitingDecisionTaskId` set) is mid-turn, not settled: that suspend is the engine's re-drive
4822
4962
  // marker, not a sleep/signal the body asked for. Keep waiting until the decision lands. A `blocked`
4823
4963
  // run is parked on the fleet, not on anything the body asked for, and is re-driven on its own.
4824
- const isSettled = (run) => isTerminal(run.status) ||
4825
- (run.status === 'suspended' && run.awaitingDecisionTaskId === undefined);
4964
+ // A `cancelling` run is still undoing its saga: it settles once it is `cancelled`.
4965
+ const isSettled = (run) => run.status !== 'cancelling' &&
4966
+ (isTerminal(run.status) ||
4967
+ (run.status === 'suspended' && run.awaitingDecisionTaskId === undefined));
4826
4968
  const shouldResolve = (run) => opts?.terminal ? isTerminal(run.status) : isSettled(run);
4827
4969
  const toResult = (run) => ({
4828
4970
  runId,
@@ -4997,7 +5139,10 @@ export class WorkflowEngine {
4997
5139
  const waiters = await this.store.listSignalWaiters(eventPrefix(name));
4998
5140
  for (const w of waiters) {
4999
5141
  if (eventMatches(payload, eventMatchOf(w.token))) {
5000
- await this.signal(w.token, payload);
5142
+ // With tenancy on, a tenant's event reaches its own runs only.
5143
+ if (await this.runHidden(w.runId))
5144
+ continue;
5145
+ await this.asEngine(() => this.signal(w.token, payload));
5001
5146
  touched += 1;
5002
5147
  }
5003
5148
  }
@@ -5015,6 +5160,13 @@ export class WorkflowEngine {
5015
5160
  // unless the caller opted out. `touched > 0` covers BOTH the waiter fan-out and the subscriber loop.
5016
5161
  if (touched > 0 || opts?.buffer === false)
5017
5162
  return touched;
5163
+ // A buffered copy carries no tenant and would reach whichever run waits next, so a publisher
5164
+ // scoped to one tenant (or to none) does not buffer: its event reaches live waiters only.
5165
+ if (this.tenancy) {
5166
+ const mode = this.tenantFilterNow().mode;
5167
+ if (mode === 'tenant' || mode === 'tenantless')
5168
+ return touched;
5169
+ }
5018
5170
  const bufferedId = globalThis.crypto.randomUUID();
5019
5171
  await this.store.bufferEvent({ name, payload, id: bufferedId, publishedAt: this.clock() });
5020
5172
  // Re-check: a waiter may have registered in the window between the miss above and this buffer
@@ -5125,6 +5277,8 @@ export class WorkflowEngine {
5125
5277
  * side's write, and the destructive `take*` ops arbitrate so a signal is never delivered twice.
5126
5278
  */
5127
5279
  async signal(token, payload) {
5280
+ if (await this.signalHidden(token))
5281
+ return null;
5128
5282
  const waiter = await this.store.takeSignalWaiter(token);
5129
5283
  if (waiter)
5130
5284
  return this.deliverSignal(waiter, payload);
@@ -5146,6 +5300,23 @@ export class WorkflowEngine {
5146
5300
  }
5147
5301
  return this.deliverSignal(lateWaiter, reclaimed.payload);
5148
5302
  }
5303
+ /** Whether a run another tenant owns is the one waiting on `token` (an app-facing signal is dropped). */
5304
+ async signalHidden(token) {
5305
+ if (!this.tenancy)
5306
+ return false;
5307
+ const filter = this.tenantFilterNow();
5308
+ if (filter.mode === 'system' || filter.mode === 'all')
5309
+ return false;
5310
+ const waiters = await this.store.listSignalWaiters(token);
5311
+ for (const waiter of waiters) {
5312
+ if (waiter.token !== token)
5313
+ continue;
5314
+ const run = await this.store.getRun(waiter.runId);
5315
+ if (run && !runVisible(filter, run, this.tenancy.nullRows))
5316
+ return true;
5317
+ }
5318
+ return false;
5319
+ }
5149
5320
  /**
5150
5321
  * Wake the run parked on `token` with `payload`, if there is one; otherwise do nothing — unlike
5151
5322
  * {@link signal}, it never buffers. For a wake-up whose real payload is ALREADY buffered on the
@@ -5177,14 +5348,18 @@ export class WorkflowEngine {
5177
5348
  * null if no run is waiting on the task (e.g. a duplicate/late delivery) — a safe no-op.
5178
5349
  */
5179
5350
  async completeTask(runId, name, result) {
5180
- return this.signal(`task:${runId}:${name}`, {
5351
+ if (await this.runHidden(runId))
5352
+ return null;
5353
+ return this.asEngine(() => this.signal(`task:${runId}:${name}`, {
5181
5354
  ok: true,
5182
5355
  value: result,
5183
- });
5356
+ }));
5184
5357
  }
5185
5358
  /** Report that a `ctx.task` failed — the run resumes and throws a FatalError at the task. */
5186
5359
  async failTask(runId, name, error) {
5187
- return this.signal(`task:${runId}:${name}`, { ok: false, error });
5360
+ if (await this.runHidden(runId))
5361
+ return null;
5362
+ return this.asEngine(() => this.signal(`task:${runId}:${name}`, { ok: false, error }));
5188
5363
  }
5189
5364
  /**
5190
5365
  * Notify a parent that's waiting on `runId` as a child of its terminal outcome (the `ctx.child`
@@ -5237,7 +5412,7 @@ export class WorkflowEngine {
5237
5412
  if (!(await childCompletionNeedsBuffer(this.store, token.slice(CHILD_TOKEN_PREFIX.length), completion))) {
5238
5413
  return;
5239
5414
  }
5240
- const resumed = await this.signal(token, completion);
5415
+ const resumed = await this.asEngine(() => this.signal(token, completion));
5241
5416
  // The parent may settle the child from its run row between the look above and the buffer
5242
5417
  // write (a join that registers its waiter only now): it then stamped the child first, and the
5243
5418
  // copy just buffered would never be read.
@@ -5302,7 +5477,7 @@ export class WorkflowEngine {
5302
5477
  : settled.status === 'completed'
5303
5478
  ? { ok: true, value: settled.output }
5304
5479
  : failedCompletion(settled.error ?? { message: settled.status });
5305
- await this.signal(token, completion);
5480
+ await this.asEngine(() => this.signal(token, completion));
5306
5481
  }
5307
5482
  /**
5308
5483
  * Cancel a run (e.g. from the dashboard). Returns null if the run does not exist. Pass
@@ -5311,6 +5486,11 @@ export class WorkflowEngine {
5311
5486
  * cancelled. Without it, cancellation is immediate (no undo).
5312
5487
  */
5313
5488
  async cancel(runId, opts) {
5489
+ if (await this.runHidden(runId))
5490
+ return null;
5491
+ return this.asEngine(() => this.cancelOnce(runId, opts, true));
5492
+ }
5493
+ async cancelOnce(runId, opts, mayRetry) {
5314
5494
  const run = await this.store.getRun(runId);
5315
5495
  if (!run)
5316
5496
  return null;
@@ -5323,18 +5503,35 @@ export class WorkflowEngine {
5323
5503
  if (TERMINAL_RUN_STATUSES.includes(run.status)) {
5324
5504
  return { runId, status: run.status, output: run.output, error: run.error };
5325
5505
  }
5506
+ // A compensating cancel is already undoing the saga: a repeat cancel, with or without
5507
+ // `compensate`, echoes it. Taking the instant path below would mark it `cancelled` and skip the
5508
+ // undo still in flight. Recovery re-drives a `cancelling` run whose undo died.
5509
+ if (run.status === 'cancelling')
5510
+ return { runId, status: 'cancelling' };
5326
5511
  // Compensating cancel: resume the run with a cancellation pending — replay re-registers the saga,
5327
5512
  // and at the suspension point execute() runs the undo and marks it cancelled. Run that resume in
5328
5513
  // the BACKGROUND so the caller (e.g. an HTTP request) never blocks on replaying the workflow +
5329
5514
  // compensations. `execute` holds the run's lease, so this can't double-run one a live worker owns
5330
5515
  // (its lease acquire fails and it no-ops); the broadcast tells that worker to abort cooperatively.
5331
5516
  if (opts?.compensate && (run.status === 'suspended' || run.status === 'running')) {
5517
+ // DURABLE cancel intent: the persisted `cancelling` status. It survives a crash or a pod
5518
+ // handoff (any turn of a `cancelling` run unwinds it, a turn already executing on another pod
5519
+ // finds it when its settle loses the status race, and crash recovery re-drives one whose undo
5520
+ // died), and shows the dashboard/API that an undo is in progress. Conditional: a run that
5521
+ // moved meanwhile (settled, parked) is re-read and cancelled for what it is now.
5522
+ const marked = await this.store.updateRunIf(runId, ['suspended', 'running'], {
5523
+ status: 'cancelling',
5524
+ updatedAt: new Date(),
5525
+ });
5526
+ if (!marked) {
5527
+ if (mayRetry)
5528
+ return this.cancelOnce(runId, opts, false);
5529
+ const current = await this.store.getRun(runId);
5530
+ return current
5531
+ ? { runId, status: current.status, output: current.output, error: current.error }
5532
+ : null;
5533
+ }
5332
5534
  this.cancelRequested.add(runId);
5333
- // DURABLE cancel intent: the in-memory flag above only survives on this pod, this boot. The
5334
- // buffered `cancel:<id>` marker is what lets the compensating unwind survive a crash or be
5335
- // picked up by ANOTHER pod (its resume replays into the WorkflowSuspended catch, which checks
5336
- // the marker when the flag is absent). Consumed there once `cancelled` is durably recorded.
5337
- await this.store.bufferSignal(`cancel:${runId}`, true).catch(() => undefined);
5338
5535
  if (this.controlPlane) {
5339
5536
  void this.controlPlane
5340
5537
  .publishControl({ kind: 'cancel', runId, from: this.instanceId }, runTarget(run.namespace))
@@ -5352,7 +5549,7 @@ export class WorkflowEngine {
5352
5549
  // needed covering. The one-microtask defer is preserved; the promise is registered synchronously.
5353
5550
  this.trackEffect(Promise.resolve().then(() => this.resumeImplicit(runId).then(() => this.notifyCancelled(runId))));
5354
5551
  await this.cancelChildren(runId, opts);
5355
- return { runId, status: run.status };
5552
+ return { runId, status: 'cancelling' };
5356
5553
  }
5357
5554
  const error = { message: 'cancelled' };
5358
5555
  // Clears a remote turn's decision marker too: a cancelled run awaits no decision.
@@ -5412,7 +5609,7 @@ export class WorkflowEngine {
5412
5609
  */
5413
5610
  async cancelChildren(parentRunId, opts) {
5414
5611
  for (const id of await this.getRunChildren(parentRunId)) {
5415
- await this.cancel(id, opts).catch(() => undefined);
5612
+ await this.asEngine(() => this.cancel(id, opts)).catch(() => undefined);
5416
5613
  }
5417
5614
  }
5418
5615
  /**
@@ -5468,7 +5665,7 @@ export class WorkflowEngine {
5468
5665
  * the full listing for a store without one.
5469
5666
  */
5470
5667
  countRuns(query) {
5471
- return countRuns(this.store, withoutBookkeepingRuns(query));
5668
+ return countRuns(this.store, withoutBookkeepingRuns(this.tenantScoped(query)));
5472
5669
  }
5473
5670
  /**
5474
5671
  * Every workflow the live fleet can execute in this engine's partition: what workers DECLARE in
@@ -5689,7 +5886,7 @@ export class WorkflowEngine {
5689
5886
  publishedAt: this.clock(),
5690
5887
  })
5691
5888
  .catch(() => undefined);
5692
- const marked = await this.store.updateRunIf(runId, ['running', 'suspended', 'blocked'], {
5889
+ const marked = await this.store.updateRunIf(runId, WAKEABLE_RUN_STATUSES, {
5693
5890
  wakeRequestedAt: this.clock(),
5694
5891
  });
5695
5892
  return marked ? 'stamped' : 'gone';
@@ -5728,7 +5925,7 @@ export class WorkflowEngine {
5728
5925
  if (!(await this.store.tryLockRun(runId, this.instanceId, nowMs + this.leaseMs, nowMs)))
5729
5926
  return;
5730
5927
  const fresh = await this.store.getRun(runId);
5731
- if (fresh?.status !== 'suspended' && fresh?.status !== 'blocked') {
5928
+ if (!fresh || !PARKED_RUN_STATUSES.includes(fresh.status)) {
5732
5929
  await this.store.releaseRunLock(runId, this.instanceId);
5733
5930
  return;
5734
5931
  }
@@ -5828,7 +6025,7 @@ export class WorkflowEngine {
5828
6025
  }
5829
6026
  // Free, and `pending` (its dispatch or the pending-run poll runs it) or `running` with no lease
5830
6027
  // (a holder that died mid-turn: crash recovery takes it): nothing for a wake to do.
5831
- if (run.status !== 'suspended' && run.status !== 'blocked')
6028
+ if (!PARKED_RUN_STATUSES.includes(run.status))
5832
6029
  return;
5833
6030
  if (this.runQueue) {
5834
6031
  this.enqueueRun({
@@ -5841,7 +6038,7 @@ export class WorkflowEngine {
5841
6038
  if (!(await this.store.tryLockRun(runId, this.instanceId, nowMs + this.leaseMs, nowMs)))
5842
6039
  return;
5843
6040
  const fresh = await this.store.getRun(runId);
5844
- if (fresh?.status !== 'suspended' && fresh?.status !== 'blocked') {
6041
+ if (!fresh || !PARKED_RUN_STATUSES.includes(fresh.status)) {
5845
6042
  await this.store.releaseRunLock(runId, this.instanceId);
5846
6043
  return;
5847
6044
  }
@@ -5850,6 +6047,18 @@ export class WorkflowEngine {
5850
6047
  throw err;
5851
6048
  });
5852
6049
  }
6050
+ /**
6051
+ * Run a turn with the run's own tenant active, so a resume driven by a timer, a signal or recovery
6052
+ * (no request around it) sees the tenant the run was started under, and what the turn starts or
6053
+ * dispatches carries it. A run with no tenant runs as before.
6054
+ */
6055
+ inRunTenant(run, fn) {
6056
+ const tenant = runTenantOf(run);
6057
+ if (!this.tenancy || !tenant)
6058
+ return fn();
6059
+ const tenancy = this.tenancy;
6060
+ return engineInternal.run(false, () => tenancy.run(tenant, fn));
6061
+ }
5853
6062
  async execute(run, fn) {
5854
6063
  const registered = this.workflows.get(versionKey(run.workflow, run.workflowVersion));
5855
6064
  // Hold the lease for the WHOLE execution — whatever path got us here (leased sweep, a signal, a
@@ -5916,6 +6125,11 @@ export class WorkflowEngine {
5916
6125
  // A remote (e.g. Python) workflow is advanced by dispatching workflow tasks, not by running an
5917
6126
  // in-process body — but everything around it (lease, recovery, timers, the resume that lands us
5918
6127
  // here on a step result) is identical, so it branches here under the same lease.
6128
+ // A `cancelling` run is a compensating cancel in flight, maybe re-driven by crash recovery or
6129
+ // by another pod that never saw the in-memory flag: re-derive the intent from the status, so
6130
+ // this turn unwinds the saga and settles `cancelled` at whichever exit it reaches.
6131
+ if (run.status === 'cancelling')
6132
+ this.cancelRequested.add(run.id);
5919
6133
  const result = registered?.remote
5920
6134
  ? await this.runRemoteExecution(run, registered)
5921
6135
  : await this.runExecution(run, fn, registered);
@@ -6306,7 +6520,7 @@ export class WorkflowEngine {
6306
6520
  // settle — and it must happen even while a dispatched turn is still out (the hold below would
6307
6521
  // otherwise swallow the cancel's resume, and the in-flight turn's decision would carry the run on
6308
6522
  // as if it was never cancelled). The late decision then finds the run cancelled and is dropped.
6309
- if (await this.takeCancelIntent(run.id))
6523
+ if (this.takeCancelIntent(run.id))
6310
6524
  return this.compensateAndCancel(run, []);
6311
6525
  // A dispatched turn is still out and within its liveness window: whatever woke the run (a step
6312
6526
  // result, a signal, a wait recheck) must not dispatch a second turn over it — that would supersede
@@ -6399,13 +6613,17 @@ export class WorkflowEngine {
6399
6613
  // with its lease released, so crash recovery re-drives it (replaying completed steps; bounded by
6400
6614
  // `maxRecoveryAttempts`).
6401
6615
  if (err instanceof RemoteWorkflowTimeout) {
6402
- await this.store
6616
+ const reset = await this.store
6403
6617
  .updateRunIf(run.id, ['running', 'suspended'], {
6404
6618
  status: 'running',
6405
6619
  updatedAt: new Date(),
6406
6620
  })
6407
6621
  .catch(() => false);
6408
6622
  await this.releaseTurnLease(run.id);
6623
+ // Not reset: the run moved on meanwhile (a compensating cancel made it `cancelling`, or it
6624
+ // settled). Report what it is now, not a `running` it no longer reads.
6625
+ if (!reset)
6626
+ return this.echoCurrentStatus(run.id);
6409
6627
  return { runId: run.id, status: 'running' };
6410
6628
  }
6411
6629
  const error = {
@@ -7317,21 +7535,25 @@ export class WorkflowEngine {
7317
7535
  return wakeAt;
7318
7536
  }
7319
7537
  /**
7320
- * Whether a compensating cancel is pending for this run, consuming the in-memory flag. The intent
7321
- * lives in TWO places: the in-memory `cancelRequested` fast path (same pod, same boot), and the
7322
- * DURABLE buffered `cancel:<id>` marker `cancel()` persisted — so a crash or a cross-pod handoff
7323
- * mid-cancellation can't silently forget the cancel. The marker is peeked (take + re-buffer), not
7324
- * consumed: it must survive a crash mid-unwind; {@link compensateAndCancel} drains it once
7325
- * `cancelled` is durably recorded.
7538
+ * Whether a compensating cancel is pending for this run, consuming the in-memory flag. The flag is
7539
+ * set by `cancel()` on this pod, and at turn start for a run that reads `cancelling` (the durable
7540
+ * intent: a cancel from another pod, or one re-driven by recovery). A cancel another pod persists
7541
+ * while this turn runs is found at the settle instead: the run no longer reads a settle-eligible
7542
+ * status, and {@link settleOrUnwind} unwinds it.
7326
7543
  */
7327
- async takeCancelIntent(runId) {
7328
- if (this.cancelRequested.delete(runId))
7329
- return true;
7330
- const marker = await this.store.takeBufferedSignal(`cancel:${runId}`);
7331
- if (!marker)
7332
- return false;
7333
- await this.store.bufferSignal(`cancel:${runId}`, marker.payload).catch(() => undefined);
7334
- return true;
7544
+ takeCancelIntent(runId) {
7545
+ return this.cancelRequested.delete(runId);
7546
+ }
7547
+ /**
7548
+ * Settle this turn's `outcome` — unless a compensating cancel was persisted on another pod while the
7549
+ * turn ran: the settle's status check then reads `cancelling`, and this turn (which still holds the
7550
+ * lease and the saga it registered) undoes it and settles `cancelled` itself.
7551
+ */
7552
+ async settleOrUnwind(run, outcome, compensations) {
7553
+ const settled = await this.settleRun(run, outcome);
7554
+ if (settled.status !== 'cancelling' || this.leaseLostRuns.has(run.id))
7555
+ return settled;
7556
+ return this.compensateAndCancel(run, compensations);
7335
7557
  }
7336
7558
  /**
7337
7559
  * Undo the registered saga compensations in reverse and settle the run `cancelled`. Shared by EVERY
@@ -7341,8 +7563,8 @@ export class WorkflowEngine {
7341
7563
  */
7342
7564
  async compensateAndCancel(run, compensations) {
7343
7565
  this.cancelRequested.delete(run.id);
7344
- // Zombie fence (as in settleRun): another instance owns the run now; the durable marker is still
7345
- // there for it, so this stale turn must neither undo nor write.
7566
+ // Zombie fence (as in settleRun): another instance owns the run now; the persisted `cancelling`
7567
+ // status is still there for it, so this stale turn must neither undo nor write.
7346
7568
  if (this.leaseLostRuns.has(run.id))
7347
7569
  return this.echoCurrentStatus(run.id);
7348
7570
  for (let i = compensations.length - 1; i >= 0; i -= 1) {
@@ -7357,13 +7579,6 @@ export class WorkflowEngine {
7357
7579
  awaitingDecisionTaskId: undefined,
7358
7580
  updatedAt: new Date(),
7359
7581
  }));
7360
- // The cancellation is durably recorded — consume the marker(s) so a later retry of this run id
7361
- // doesn't get spuriously re-cancelled.
7362
- for (;;) {
7363
- const m = await this.store.takeBufferedSignal(`cancel:${run.id}`).catch(() => null);
7364
- if (!m)
7365
- break;
7366
- }
7367
7582
  this.emit({
7368
7583
  type: 'run.failed',
7369
7584
  runId: run.id,
@@ -7408,7 +7623,11 @@ export class WorkflowEngine {
7408
7623
  }
7409
7624
  // Singleton admission gate: if this run shares its key with `limit` older in-flight runs, wait
7410
7625
  // (suspend on a short timer) until a slot frees instead of running now. Re-checked on each resume.
7411
- if (registered?.singleton && !(await this.singletons.admit(run, registered.singleton))) {
7626
+ // A `cancelling` run skips it: it is tearing down, not competing for a slot, and parking it here
7627
+ // would overwrite `cancelling` with `suspended`.
7628
+ if (run.status !== 'cancelling' &&
7629
+ registered?.singleton &&
7630
+ !(await this.singletons.admit(run, registered.singleton))) {
7412
7631
  return this.singletonWaitResult(run);
7413
7632
  }
7414
7633
  // A body this instance parked on dispatched steps in an earlier turn continues from there (see
@@ -7487,10 +7706,10 @@ export class WorkflowEngine {
7487
7706
  }
7488
7707
  // A compensating cancel requested while this turn ran (or one whose resume replays straight to
7489
7708
  // `return`, with no suspension point left) still wins: undo + settle cancelled, never completed.
7490
- if (await this.takeCancelIntent(run.id)) {
7709
+ if (this.takeCancelIntent(run.id)) {
7491
7710
  return await this.compensateAndCancel(run, compensations);
7492
7711
  }
7493
- return await this.settleRun(run, { kind: 'completed', output });
7712
+ return await this.settleOrUnwind(run, { kind: 'completed', output }, compensations);
7494
7713
  }
7495
7714
  catch (err) {
7496
7715
  if (err instanceof ContinueAsNew) {
@@ -7499,7 +7718,11 @@ export class WorkflowEngine {
7499
7718
  if (this.leaseLostRuns.has(run.id))
7500
7719
  return await this.echoCurrentStatus(run.id);
7501
7720
  // A cancel in flight wins over continue-as-new: undo + cancel instead of forking the next run.
7502
- if (await this.takeCancelIntent(run.id)) {
7721
+ // The run is re-read here (continue-as-new is rare) so a cancel another pod persisted during
7722
+ // this turn usually stops the fork; one that lands after the read loses the conditional
7723
+ // completion below instead.
7724
+ if (this.takeCancelIntent(run.id) ||
7725
+ (await this.store.getRun(run.id))?.status === 'cancelling') {
7503
7726
  return await this.compensateAndCancel(run, compensations);
7504
7727
  }
7505
7728
  const nextId = nextContinuationId(run.id);
@@ -7513,13 +7736,28 @@ export class WorkflowEngine {
7513
7736
  // converges on the already-persisted continuation (idempotent by the continuation id).
7514
7737
  // `dispatch: false`: the tracked handoff below owns the pickup. The continuation inherits
7515
7738
  // the parent RUN's namespace so a tenant's chain stays in its worker-pool partition.
7516
- await this.startCore(run.workflow, err.input, nextId, { namespace: run.namespace }, false);
7517
- await this.store.updateRun(run.id, {
7739
+ await this.startCore(run.workflow, err.input, nextId, {
7740
+ namespace: run.namespace,
7741
+ tenantId: run.tenantId ?? null,
7742
+ ...(run.tenantKind ? { tenantKind: run.tenantKind } : {}),
7743
+ }, false);
7744
+ // Conditional, like every settle: a cancel persisted after the re-read above (`cancelling`, or a
7745
+ // plain `cancelled`) must win over this completion. The continuation already exists then, and
7746
+ // nothing has handed it off: cancel it, and unwind this run if the cancel compensates.
7747
+ const completed = await this.store.updateRunIf(run.id, SETTLE_ELIGIBLE_STATUSES, {
7518
7748
  status: 'completed',
7519
7749
  output: undefined,
7520
7750
  error: undefined,
7521
7751
  updatedAt: new Date(),
7522
7752
  });
7753
+ if (!completed) {
7754
+ await this.asEngine(() => this.cancel(nextId));
7755
+ const current = await this.store.getRun(run.id);
7756
+ if (current?.status === 'cancelling' && !this.leaseLostRuns.has(run.id)) {
7757
+ return await this.compensateAndCancel(run, compensations);
7758
+ }
7759
+ return await this.echoCurrentStatus(run.id);
7760
+ }
7523
7761
  this.emit({
7524
7762
  type: 'run.completed',
7525
7763
  runId: run.id,
@@ -7531,7 +7769,11 @@ export class WorkflowEngine {
7531
7769
  // continuation has entered `inflight` and settled, so `drain()` can't return before the next
7532
7770
  // run's store writes land. Still deferred; its startCore call converges on the run persisted
7533
7771
  // above (prior-run branch) and drives the pickup.
7534
- this.handoffRun(run.workflow, err.input, nextId, { namespace: run.namespace });
7772
+ this.handoffRun(run.workflow, err.input, nextId, {
7773
+ namespace: run.namespace,
7774
+ tenantId: run.tenantId ?? null,
7775
+ ...(run.tenantKind ? { tenantKind: run.tenantKind } : {}),
7776
+ });
7535
7777
  return { runId: run.id, status: 'completed' };
7536
7778
  }
7537
7779
  if (err instanceof WorkflowBlocked) {
@@ -7543,10 +7785,10 @@ export class WorkflowEngine {
7543
7785
  if (err instanceof WorkflowSuspended) {
7544
7786
  // A compensating cancel resumed this run to reach here: the replay re-registered the saga,
7545
7787
  // so undo the completed steps in reverse and mark it cancelled instead of re-suspending.
7546
- if (await this.takeCancelIntent(run.id)) {
7788
+ if (this.takeCancelIntent(run.id)) {
7547
7789
  return await this.compensateAndCancel(run, compensations);
7548
7790
  }
7549
- const settled = await this.settleRun(run, { kind: 'suspended', wakeAt: err.wakeAt });
7791
+ const settled = await this.settleOrUnwind(run, { kind: 'suspended', wakeAt: err.wakeAt }, compensations);
7550
7792
  const stamp = this.suspendStamps.get(run.id);
7551
7793
  this.suspendStamps.delete(run.id);
7552
7794
  if (parkedTurn && cont && settled.status === 'suspended' && stamp !== undefined) {
@@ -7571,7 +7813,7 @@ export class WorkflowEngine {
7571
7813
  }
7572
7814
  // A cancel in flight that surfaced as a thrown error settles `cancelled`, not `failed`: the saga
7573
7815
  // undo runs either way; this keeps the terminal status faithful to the cancel request.
7574
- if (await this.takeCancelIntent(run.id)) {
7816
+ if (this.takeCancelIntent(run.id)) {
7575
7817
  return await this.compensateAndCancel(run, compensations);
7576
7818
  }
7577
7819
  const error = {
@@ -7589,7 +7831,8 @@ export class WorkflowEngine {
7589
7831
  continue;
7590
7832
  await this.runCompensation(run, comp, i);
7591
7833
  }
7592
- return await this.settleRun(run, { kind: 'failed', error });
7834
+ // The saga is undone already: a cancel persisted meanwhile only needs the `cancelled` settle.
7835
+ return await this.settleOrUnwind(run, { kind: 'failed', error }, []);
7593
7836
  }
7594
7837
  finally {
7595
7838
  // Release the recovery lease once the run reaches a terminal/suspended state, so the
@@ -7903,7 +8146,7 @@ export class WorkflowEngine {
7903
8146
  // escape `drain()` onto a torn-down connection ("Transaction query already complete"). The
7904
8147
  // one-microtask defer is preserved; the promise is registered synchronously.
7905
8148
  cancelChild: (childId) => {
7906
- this.trackEffect(Promise.resolve().then(() => this.cancel(childId)));
8149
+ this.trackEffect(Promise.resolve().then(() => this.asEngine(() => this.cancel(childId))));
7907
8150
  },
7908
8151
  // Shallow-merge into the run's searchAttributes (the ctx primitive makes this exactly-once).
7909
8152
  upsertSearchAttributes: async (runId, attrs) => {
@@ -7951,11 +8194,13 @@ export class WorkflowEngine {
7951
8194
  * paused at a breakpoint.
7952
8195
  */
7953
8196
  async continue(runId) {
8197
+ if (await this.runHidden(runId))
8198
+ return null;
7954
8199
  const checkpoints = await this.store.listCheckpoints(runId);
7955
8200
  const bp = checkpoints.find(isBreakpoint);
7956
8201
  if (!bp)
7957
8202
  return null;
7958
- return this.signal(breakpointToken(runId, bp.seq), undefined);
8203
+ return this.asEngine(() => this.signal(breakpointToken(runId, bp.seq), undefined));
7959
8204
  }
7960
8205
  /**
7961
8206
  * Read the latest value a run published for `key` via {@link WorkflowCtx.setEvent} — a
@@ -7963,6 +8208,8 @@ export class WorkflowEngine {
7963
8208
  * never published that key. The suspend-model counterpart of a Temporal query.
7964
8209
  */
7965
8210
  async getEvent(runId, key) {
8211
+ if (await this.runHidden(runId))
8212
+ return undefined;
7966
8213
  const name = `event:${key}`;
7967
8214
  // Targeted read: the highest-seq checkpoint for this name is the most recent value (a re-published
7968
8215
  // key overwrites at a higher seq), matching the old "last in seq order wins" scan. Falls back to a
@@ -7994,7 +8241,7 @@ export class WorkflowEngine {
7994
8241
  */
7995
8242
  async update(runId, name, arg) {
7996
8243
  const run = await this.store.getRun(runId);
7997
- if (!run)
8244
+ if (!run || !this.runVisibleNow(run))
7998
8245
  return { accepted: false, reason: `run ${runId} not found` };
7999
8246
  const validate = this.updateValidators.get(`${run.workflow}:${name}`);
8000
8247
  if (validate) {
@@ -8007,7 +8254,7 @@ export class WorkflowEngine {
8007
8254
  return { accepted: false, reason: err instanceof Error ? err.message : String(err) };
8008
8255
  }
8009
8256
  }
8010
- const result = await this.signal(`update:${runId}:${name}`, arg);
8257
+ const result = await this.asEngine(() => this.signal(`update:${runId}:${name}`, arg));
8011
8258
  return { accepted: true, run: result };
8012
8259
  }
8013
8260
  // ---------------------------------------------------------------------------
@@ -9365,7 +9612,7 @@ export class WorkflowEngine {
9365
9612
  // batch result resurrected a `failed` run into `suspended`, which then continued executing as if
9366
9613
  // the failure never happened, racing whatever the operator did about it, and after any saga
9367
9614
  // compensations already ran). Terminal is terminal — recovery of a failed run belongs to an
9368
- // explicit `requeue`/`durable:retry`, never to a stray result.
9615
+ // explicit `requeue`/`durable:runs:retry`, never to a stray result.
9369
9616
  if (cp.status !== 'pending') {
9370
9617
  if (!terminal)
9371
9618
  return { cp, run };
@@ -9731,6 +9978,8 @@ function turnBeatKey(runId) {
9731
9978
  /** The run whose remote turn is being enqueued in the current async context — lets the decision
9732
9979
  * handler spot a transport delivering the decision from inside its own `dispatchWorkflowTask`. */
9733
9980
  const turnDispatchAls = new AsyncLocalStorage();
9981
+ /** `true` while the engine does its own work: app-facing tenant guards do not apply inside it. */
9982
+ const engineInternal = new AsyncLocalStorage();
9734
9983
  /** Tolerance (ms) between the worker's clock, which stamps a persisted heartbeat, and this engine's,
9735
9984
  * when deciding whether a beat belongs to the current attempt (see `awaitWithHeartbeat`). */
9736
9985
  const HEARTBEAT_CLOCK_SKEW_MS = 1_000;