@pikku/core 0.12.79 → 0.12.80

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 (330) hide show
  1. package/CHANGELOG.md +161 -0
  2. package/dist/dev/hot-reload.js +1 -6
  3. package/dist/ecosystem.d.ts +27 -0
  4. package/dist/ecosystem.js +26 -0
  5. package/dist/errors/error-handler.js +4 -2
  6. package/dist/function/function-runner.js +20 -42
  7. package/dist/function/functions.types.d.ts +9 -5
  8. package/dist/index.d.ts +4 -9
  9. package/dist/index.js +3 -8
  10. package/dist/middleware/auth-cookie.d.ts +0 -4
  11. package/dist/middleware/auth-cookie.js +38 -2
  12. package/dist/middleware/cors.js +1 -0
  13. package/dist/middleware/index.d.ts +1 -1
  14. package/dist/middleware/index.js +1 -1
  15. package/dist/middleware/remote-auth.js +14 -3
  16. package/dist/permissions.d.ts +8 -10
  17. package/dist/permissions.js +0 -11
  18. package/dist/pikku-state.js +12 -5
  19. package/dist/schema.js +35 -1
  20. package/dist/services/ai-run-state-service.d.ts +7 -1
  21. package/dist/services/in-memory-ai-run-state-service.d.ts +1 -1
  22. package/dist/services/in-memory-ai-run-state-service.js +2 -1
  23. package/dist/services/in-memory-workflow-service.js +2 -0
  24. package/dist/services/local-content-request-handler.d.ts +21 -0
  25. package/dist/services/local-content-request-handler.js +72 -53
  26. package/dist/services/local-content.d.ts +6 -0
  27. package/dist/services/local-content.js +14 -1
  28. package/dist/services/workflow-service.d.ts +4 -2
  29. package/dist/testing/service-tests/agent-run-service-tests.d.ts +10 -0
  30. package/dist/testing/service-tests/agent-run-service-tests.js +72 -0
  31. package/dist/testing/service-tests/ai-storage-service-tests.d.ts +3 -0
  32. package/dist/testing/service-tests/ai-storage-service-tests.js +226 -0
  33. package/dist/testing/service-tests/channel-store-tests.d.ts +3 -0
  34. package/dist/testing/service-tests/channel-store-tests.js +72 -0
  35. package/dist/testing/service-tests/credential-service-tests.d.ts +3 -0
  36. package/dist/testing/service-tests/credential-service-tests.js +109 -0
  37. package/dist/testing/service-tests/deployment-service-tests.d.ts +3 -0
  38. package/dist/testing/service-tests/deployment-service-tests.js +21 -0
  39. package/dist/testing/service-tests/event-hub-store-tests.d.ts +3 -0
  40. package/dist/testing/service-tests/event-hub-store-tests.js +34 -0
  41. package/dist/testing/service-tests/secret-service-tests.d.ts +3 -0
  42. package/dist/testing/service-tests/secret-service-tests.js +80 -0
  43. package/dist/testing/service-tests/session-store-tests.d.ts +3 -0
  44. package/dist/testing/service-tests/session-store-tests.js +43 -0
  45. package/dist/testing/service-tests/workflow-run-service-tests.d.ts +3 -0
  46. package/dist/testing/service-tests/workflow-run-service-tests.js +42 -0
  47. package/dist/testing/service-tests/workflow-service-tests.d.ts +3 -0
  48. package/dist/testing/service-tests/workflow-service-tests.js +150 -0
  49. package/dist/testing/service-tests.d.ts +6 -0
  50. package/dist/testing/service-tests.js +26 -791
  51. package/dist/types/core.types.d.ts +11 -1
  52. package/dist/types/state.types.d.ts +1 -1
  53. package/dist/wirings/actor-flow/run-conversation.js +1 -6
  54. package/dist/wirings/ai-agent/agent-rpc.d.ts +15 -0
  55. package/dist/wirings/ai-agent/agent-rpc.js +53 -0
  56. package/dist/wirings/ai-agent/ai-agent-agui.js +1 -5
  57. package/dist/wirings/ai-agent/ai-agent-memory.js +3 -1
  58. package/dist/wirings/ai-agent/ai-agent-prepare.js +3 -9
  59. package/dist/wirings/ai-agent/ai-agent-runner.js +28 -107
  60. package/dist/wirings/ai-agent/ai-agent-stream.js +20 -61
  61. package/dist/wirings/ai-agent/ai-agent-turn.d.ts +56 -0
  62. package/dist/wirings/ai-agent/ai-agent-turn.js +81 -0
  63. package/dist/wirings/ai-agent/ai-agent.types.d.ts +1 -1
  64. package/dist/wirings/ai-agent/voice-input.js +1 -6
  65. package/dist/wirings/ai-agent/voice-output.js +2 -12
  66. package/dist/wirings/channel/channel-common.js +1 -0
  67. package/dist/wirings/channel/channel-handler.js +3 -5
  68. package/dist/wirings/channel/channel-rpc-service.d.ts +0 -6
  69. package/dist/wirings/channel/channel-rpc-service.js +0 -8
  70. package/dist/wirings/channel/channel-rpc.types.d.ts +6 -0
  71. package/dist/wirings/channel/channel-rpc.types.js +8 -0
  72. package/dist/wirings/channel/channel-runner.d.ts +1 -3
  73. package/dist/wirings/channel/channel-runner.js +16 -8
  74. package/dist/wirings/channel/channel.types.d.ts +2 -0
  75. package/dist/wirings/channel/pikku-abstract-channel-handler.js +1 -0
  76. package/dist/wirings/channel/serverless/serverless-channel-runner.js +3 -0
  77. package/dist/wirings/cli/channel/cli-channel-runner.js +2 -0
  78. package/dist/wirings/cli/cli-runner.js +4 -2
  79. package/dist/wirings/cli/cli.types.d.ts +0 -8
  80. package/dist/wirings/cli/command-parser.js +13 -0
  81. package/dist/wirings/gateway/gateway-runner.js +9 -2
  82. package/dist/wirings/http/http-routes.js +2 -0
  83. package/dist/wirings/http/http-runner.d.ts +0 -10
  84. package/dist/wirings/http/http-runner.js +6 -13
  85. package/dist/wirings/http/http.types.d.ts +0 -10
  86. package/dist/wirings/http/index.d.ts +1 -1
  87. package/dist/wirings/http/index.js +1 -1
  88. package/dist/wirings/mcp/mcp-runner.d.ts +0 -7
  89. package/dist/wirings/mcp/mcp-runner.js +0 -6
  90. package/dist/wirings/rpc/rpc-runner.d.ts +8 -51
  91. package/dist/wirings/rpc/rpc-runner.js +53 -86
  92. package/dist/wirings/rpc/rpc-types.d.ts +3 -0
  93. package/dist/wirings/secret/validate-secret-definitions.js +2 -2
  94. package/dist/wirings/trigger/pikku-trigger-service.d.ts +0 -4
  95. package/dist/wirings/trigger/trigger-runner.js +1 -0
  96. package/dist/wirings/virtual-user/run-virtual-user.js +11 -11
  97. package/dist/wirings/virtual-user/virtual-user-derive.js +4 -25
  98. package/dist/wirings/virtual-user/virtual-user-dispositions.js +1 -4
  99. package/dist/wirings/workflow/dsl/workflow-dsl.types.d.ts +29 -2
  100. package/dist/wirings/workflow/feature.js +7 -4
  101. package/dist/wirings/workflow/graph/graph-runner.d.ts +1 -2
  102. package/dist/wirings/workflow/graph/graph-runner.js +8 -7
  103. package/dist/wirings/workflow/graph/workflow-graph.types.d.ts +0 -4
  104. package/dist/wirings/workflow/index.d.ts +7 -2
  105. package/dist/wirings/workflow/index.js +5 -1
  106. package/dist/wirings/workflow/pikku-scenario-service.d.ts +17 -3
  107. package/dist/wirings/workflow/pikku-scenario-service.js +38 -10
  108. package/dist/wirings/workflow/pikku-workflow-service.d.ts +28 -147
  109. package/dist/wirings/workflow/pikku-workflow-service.js +71 -493
  110. package/dist/wirings/workflow/workflow-approval.d.ts +39 -0
  111. package/dist/wirings/workflow/workflow-approval.js +114 -0
  112. package/dist/wirings/workflow/workflow-constants.d.ts +26 -0
  113. package/dist/wirings/workflow/workflow-constants.js +35 -0
  114. package/dist/wirings/workflow/workflow-errors.d.ts +58 -0
  115. package/dist/wirings/workflow/workflow-errors.js +112 -0
  116. package/dist/wirings/workflow/workflow-meta-resolver.d.ts +11 -0
  117. package/dist/wirings/workflow/workflow-meta-resolver.js +31 -0
  118. package/dist/wirings/workflow/workflow-queue-routing.d.ts +8 -0
  119. package/dist/wirings/workflow/workflow-queue-routing.js +38 -0
  120. package/dist/wirings/workflow/workflow-queue-wiring.d.ts +20 -0
  121. package/dist/wirings/workflow/workflow-queue-wiring.js +79 -0
  122. package/dist/wirings/workflow/workflow-recovery.d.ts +68 -0
  123. package/dist/wirings/workflow/workflow-recovery.js +101 -0
  124. package/dist/wirings/workflow/workflow-run-engine.types.d.ts +54 -0
  125. package/dist/wirings/workflow/workflow-run-engine.types.js +1 -0
  126. package/dist/wirings/workflow/workflow-run-ownership.d.ts +16 -0
  127. package/dist/wirings/workflow/workflow-run-ownership.js +29 -0
  128. package/dist/wirings/workflow/workflow-suspend.d.ts +12 -0
  129. package/dist/wirings/workflow/workflow-suspend.js +33 -0
  130. package/dist/wirings/workflow/workflow.types.d.ts +7 -0
  131. package/knowledge/decisions/internals/a-non-streaming-agent-run-registers-with-airunstate-too.md +22 -0
  132. package/knowledge/decisions/internals/a-resumed-agent-turn-is-as-interruptible-as-the-first.md +20 -0
  133. package/knowledge/decisions/internals/a-scenario-step-template-is-offered-unfilled.md +21 -0
  134. package/knowledge/decisions/internals/a-virtual-user-decides-whether-to-trust-memory-once-per-turn.md +21 -0
  135. package/knowledge/decisions/internals/a-wall-clock-threshold-is-a-load-test-in-disguise.md +46 -0
  136. package/knowledge/decisions/internals/a-workflow-wire-is-built-from-the-run-not-from-the-rpc-service.md +45 -0
  137. package/knowledge/decisions/internals/agent-context-waits-for-a-tool-result-still-being-written.md +21 -0
  138. package/knowledge/decisions/internals/agent-speech-travels-as-a-custom-agui-event.md +22 -0
  139. package/knowledge/decisions/internals/an-agent-interrupt-is-not-a-failure.md +23 -0
  140. package/knowledge/decisions/internals/an-agent-run-owned-by-another-instance-says-so.md +25 -0
  141. package/knowledge/decisions/internals/an-agent-stream-send-must-return-the-inner-sends-promise.md +22 -0
  142. package/knowledge/decisions/internals/an-empty-text-part-is-omitted-from-an-agent-message.md +20 -0
  143. package/knowledge/decisions/internals/an-empty-transcript-is-not-recorded.md +22 -0
  144. package/knowledge/decisions/internals/an-unref-d-timer-cannot-be-awaited-under-node-test.md +66 -0
  145. package/knowledge/decisions/internals/channel-state-accessors-are-unsound-generics-that-every-implementation-asserts.md +33 -0
  146. package/knowledge/decisions/internals/gateway-listener-middleware-runs-without-an-rpc-on-the-wire.md +43 -0
  147. package/knowledge/decisions/internals/hot-reload-writes-into-the-function-map-captured-at-startup.md +26 -0
  148. package/knowledge/decisions/internals/index.md +38 -26
  149. package/knowledge/decisions/internals/only-exposed-functions-enter-a-virtual-user-catalogue.md +21 -0
  150. package/knowledge/decisions/internals/scenario-given-and-when-are-sugar-but-then-is-not.md +24 -0
  151. package/knowledge/decisions/internals/side-effects-are-an-allowlist-not-a-boolean.md +34 -0
  152. package/knowledge/decisions/internals/speech-synthesis-picks-a-voice-per-sentence-and-warns-once.md +24 -0
  153. package/knowledge/decisions/internals/the-actor-prompt-says-json-because-of-json-object-mode.md +22 -0
  154. package/knowledge/decisions/internals/the-agent-done-event-goes-through-the-middleware-and-is-awaited.md +26 -0
  155. package/knowledge/decisions/internals/the-api-report-pins-members-not-just-names.md +43 -0
  156. package/knowledge/decisions/internals/the-ecosystem-entry-point-carries-the-adapter-surface.md +58 -0
  157. package/knowledge/decisions/internals/the-middleware-resolution-cache-is-deliberately-unbounded.md +40 -0
  158. package/knowledge/decisions/internals/the-per-invocation-rpc-view-is-a-class.md +40 -0
  159. package/knowledge/decisions/internals/the-persona-runtime-is-exported-from-the-persona-entry-point.md +28 -0
  160. package/knowledge/decisions/internals/the-transcript-event-is-sent-ahead-of-the-run.md +25 -0
  161. package/knowledge/decisions/internals/the-virtual-user-catalogue-is-the-only-gate-on-what-may-be-called.md +21 -0
  162. package/knowledge/decisions/internals/the-worker-disposition-is-the-one-that-is-not-testing.md +22 -0
  163. package/knowledge/decisions/internals/thread-history-records-the-transcript-not-the-audio.md +25 -0
  164. package/knowledge/decisions/internals/virtual-user-step-order-comes-from-insertion-order.md +21 -0
  165. package/knowledge/decisions/internals/voice-output-speaks-unless-voice-input-explicitly-says-otherwise.md +24 -0
  166. package/knowledge/decisions/internals/wiring-registries-erase-the-generics-their-wire-functions-capture.md +35 -0
  167. package/knowledge/decisions/security/a-dropped-audit-write-is-always-logged.md +4 -2
  168. package/knowledge/decisions/security/a-graph-run-starts-at-an-entry-node-the-graph-declared.md +33 -0
  169. package/knowledge/decisions/security/a-permission-gets-a-wire-it-cannot-reply-on.md +33 -0
  170. package/knowledge/decisions/security/a-step-runs-the-function-the-workflow-dispatched-it-with.md +37 -0
  171. package/knowledge/decisions/security/a-virtual-user-is-never-offered-a-step-that-would-forge-its-own-oracle.md +28 -0
  172. package/knowledge/decisions/security/a-workflow-run-is-read-and-approved-by-its-owner.md +34 -0
  173. package/knowledge/decisions/security/an-agent-approval-is-claimed-before-the-tool-runs.md +33 -0
  174. package/knowledge/decisions/security/an-upload-is-counted-as-it-arrives-not-buffered-then-measured.md +24 -0
  175. package/knowledge/decisions/security/index.md +7 -0
  176. package/knowledge/questions/channel-middleware-accepts-bare-factories-that-nothing-resolves.md +35 -0
  177. package/knowledge/questions/index.md +2 -1
  178. package/knowledge/questions/unauthorized-channel-replies-escape-the-declared-out-type.md +44 -0
  179. package/package.json +13 -3
  180. package/scripts/generate-api-report.d.mts +1 -0
  181. package/scripts/generate-api-report.mjs +89 -0
  182. package/scripts/generate-api-report.mts +218 -0
  183. package/src/api-report.test.ts +69 -0
  184. package/src/column-form.ts +1 -4
  185. package/src/crypto-utils.test.ts +15 -2
  186. package/src/dev/hot-reload.ts +2 -7
  187. package/src/ecosystem.ts +40 -0
  188. package/src/errors/error-handler.ts +5 -3
  189. package/src/errors/error.test.ts +4 -1
  190. package/src/function/function-runner.test.ts +1 -1
  191. package/src/function/function-runner.ts +28 -39
  192. package/src/function/functions.types.ts +15 -9
  193. package/src/handle-error.ts +1 -1
  194. package/src/index.ts +3 -22
  195. package/src/middleware/auth-cookie.test.ts +50 -0
  196. package/src/middleware/auth-cookie.ts +38 -2
  197. package/src/middleware/cors.ts +2 -1
  198. package/src/middleware/index.ts +1 -1
  199. package/src/middleware/remote-auth.test.ts +43 -0
  200. package/src/middleware/remote-auth.ts +17 -3
  201. package/src/middleware-runner.ts +4 -2
  202. package/src/no-any-casts.test.ts +57 -0
  203. package/src/permissions.test.ts +3 -1
  204. package/src/permissions.ts +22 -24
  205. package/src/pikku-state.ts +17 -6
  206. package/src/public-surface.json +578 -0
  207. package/src/public-surface.json.README +25 -0
  208. package/src/public-surface.test.ts +105 -0
  209. package/src/removed-legacy-exports.test.ts +62 -0
  210. package/src/schema.test.ts +78 -0
  211. package/src/schema.ts +36 -1
  212. package/src/services/ai-run-state-service.ts +7 -1
  213. package/src/services/audit-service.ts +2 -2
  214. package/src/services/in-memory-ai-run-state-service.ts +3 -2
  215. package/src/services/in-memory-workflow-service.ts +2 -0
  216. package/src/services/index.ts +1 -4
  217. package/src/services/local-content-request-handler.test.ts +43 -5
  218. package/src/services/local-content-request-handler.ts +103 -74
  219. package/src/services/local-content.ts +15 -1
  220. package/src/services/local-email-service.ts +5 -1
  221. package/src/services/system-role-guard.test.ts +4 -1
  222. package/src/services/workflow-service.ts +9 -2
  223. package/src/side-effects-are-declared.test.ts +84 -0
  224. package/src/source-files-stay-composable.test.ts +41 -0
  225. package/src/testing/service-tests/agent-run-service-tests.ts +98 -0
  226. package/src/testing/service-tests/ai-storage-service-tests.ts +286 -0
  227. package/src/testing/service-tests/channel-store-tests.ts +98 -0
  228. package/src/testing/service-tests/credential-service-tests.ts +143 -0
  229. package/src/testing/service-tests/deployment-service-tests.ts +33 -0
  230. package/src/testing/service-tests/event-hub-store-tests.ts +48 -0
  231. package/src/testing/service-tests/secret-service-tests.ts +105 -0
  232. package/src/testing/service-tests/session-store-tests.ts +62 -0
  233. package/src/testing/service-tests/workflow-run-service-tests.ts +59 -0
  234. package/src/testing/service-tests/workflow-service-tests.ts +308 -0
  235. package/src/testing/service-tests.ts +31 -1111
  236. package/src/types/core.types.ts +15 -1
  237. package/src/types/state.types.ts +1 -1
  238. package/src/wirings/actor-flow/run-conversation.ts +1 -6
  239. package/src/wirings/ai-agent/agent-rpc.ts +120 -0
  240. package/src/wirings/ai-agent/ai-agent-agui.ts +1 -5
  241. package/src/wirings/ai-agent/ai-agent-interrupt.test.ts +2 -1
  242. package/src/wirings/ai-agent/ai-agent-memory.ts +8 -1
  243. package/src/wirings/ai-agent/ai-agent-prepare.ts +10 -12
  244. package/src/wirings/ai-agent/ai-agent-resume-authorization.test.ts +1 -0
  245. package/src/wirings/ai-agent/ai-agent-runner.test.ts +82 -2
  246. package/src/wirings/ai-agent/ai-agent-runner.ts +49 -120
  247. package/src/wirings/ai-agent/ai-agent-stream.test.ts +2 -0
  248. package/src/wirings/ai-agent/ai-agent-stream.ts +36 -63
  249. package/src/wirings/ai-agent/ai-agent-thread-ownership.test.ts +1 -1
  250. package/src/wirings/ai-agent/ai-agent-turn.ts +121 -0
  251. package/src/wirings/ai-agent/ai-agent.types.ts +1 -1
  252. package/src/wirings/ai-agent/voice-input.ts +1 -6
  253. package/src/wirings/ai-agent/voice-output.ts +2 -12
  254. package/src/wirings/channel/channel-common.ts +5 -2
  255. package/src/wirings/channel/channel-handler-shapes.test.ts +72 -0
  256. package/src/wirings/channel/channel-handler.ts +7 -7
  257. package/src/wirings/channel/channel-rpc-service.ts +0 -14
  258. package/src/wirings/channel/channel-rpc.test.ts +25 -4
  259. package/src/wirings/channel/channel-rpc.types.ts +14 -0
  260. package/src/wirings/channel/channel-runner.ts +33 -20
  261. package/src/wirings/channel/channel.types.ts +2 -0
  262. package/src/wirings/channel/local/local-channel-runner.ts +1 -1
  263. package/src/wirings/channel/pikku-abstract-channel-handler.ts +2 -1
  264. package/src/wirings/channel/serverless/serverless-channel-runner.ts +6 -3
  265. package/src/wirings/cli/channel/cli-channel-runner.ts +3 -1
  266. package/src/wirings/cli/channel/cli-raw-client-runner.ts +4 -1
  267. package/src/wirings/cli/cli-runner.ts +7 -4
  268. package/src/wirings/cli/cli.types.ts +0 -39
  269. package/src/wirings/cli/command-parser.test.ts +19 -0
  270. package/src/wirings/cli/command-parser.ts +17 -1
  271. package/src/wirings/gateway/gateway-channel-meta.test.ts +44 -0
  272. package/src/wirings/gateway/gateway-runner.ts +27 -19
  273. package/src/wirings/http/http-routes.ts +4 -1
  274. package/src/wirings/http/http-runner.ts +11 -26
  275. package/src/wirings/http/http.types.ts +0 -15
  276. package/src/wirings/http/index.ts +1 -7
  277. package/src/wirings/http/pikku-fetch-http-request.ts +2 -2
  278. package/src/wirings/http/web-request.ts +1 -1
  279. package/src/wirings/mcp/mcp-runner.ts +2 -16
  280. package/src/wirings/persona/persona-environments.test.ts +14 -3
  281. package/src/wirings/persona/persona.test.ts +13 -3
  282. package/src/wirings/persona/validate-personas.ts +5 -1
  283. package/src/wirings/queue/queue-runner.ts +1 -1
  284. package/src/wirings/rpc/addon-auth-tags.test.ts +1 -5
  285. package/src/wirings/rpc/rpc-runner.ts +58 -136
  286. package/src/wirings/rpc/rpc-types.ts +7 -0
  287. package/src/wirings/rpc/wire-addon.ts +3 -1
  288. package/src/wirings/secret/validate-secret-definitions.test.ts +22 -0
  289. package/src/wirings/secret/validate-secret-definitions.ts +2 -2
  290. package/src/wirings/trigger/pikku-trigger-service.ts +0 -5
  291. package/src/wirings/trigger/trigger-runner.ts +7 -5
  292. package/src/wirings/virtual-user/run-virtual-user.test.ts +18 -9
  293. package/src/wirings/virtual-user/run-virtual-user.ts +28 -15
  294. package/src/wirings/virtual-user/virtual-user-agents.test.ts +4 -1
  295. package/src/wirings/virtual-user/virtual-user-derive.ts +4 -25
  296. package/src/wirings/virtual-user/virtual-user-dispositions.test.ts +7 -2
  297. package/src/wirings/virtual-user/virtual-user-dispositions.ts +1 -4
  298. package/src/wirings/virtual-user/virtual-user-intents.test.ts +13 -3
  299. package/src/wirings/workflow/dsl/workflow-dsl.types.ts +35 -2
  300. package/src/wirings/workflow/feature.ts +7 -4
  301. package/src/wirings/workflow/graph/graph-node.ts +1 -1
  302. package/src/wirings/workflow/graph/graph-runner.test.ts +4 -2
  303. package/src/wirings/workflow/graph/graph-runner.ts +13 -16
  304. package/src/wirings/workflow/graph/workflow-graph.types.ts +0 -5
  305. package/src/wirings/workflow/index.ts +12 -4
  306. package/src/wirings/workflow/pikku-scenario-service.ts +54 -25
  307. package/src/wirings/workflow/pikku-workflow-service.test.ts +2 -2
  308. package/src/wirings/workflow/pikku-workflow-service.ts +190 -743
  309. package/src/wirings/workflow/scenario-hooks.test.ts +51 -0
  310. package/src/wirings/workflow/workflow-approval.ts +185 -0
  311. package/src/wirings/workflow/workflow-child-run-session.test.ts +89 -0
  312. package/src/wirings/workflow/workflow-constants.ts +47 -0
  313. package/src/wirings/workflow/workflow-dispatch-durability.test.ts +1 -1
  314. package/src/wirings/workflow/workflow-errors.ts +128 -0
  315. package/src/wirings/workflow/workflow-inline-authority.test.ts +12 -6
  316. package/src/wirings/workflow/workflow-meta-resolver.ts +45 -0
  317. package/src/wirings/workflow/workflow-missing-meta.test.ts +92 -0
  318. package/src/wirings/workflow/workflow-queue-routing.ts +85 -0
  319. package/src/wirings/workflow/workflow-queue-wiring.ts +149 -0
  320. package/src/wirings/workflow/workflow-recovery.ts +162 -0
  321. package/src/wirings/workflow/workflow-retry-policy.test.ts +1 -1
  322. package/src/wirings/workflow/workflow-run-authority.test.ts +215 -0
  323. package/src/wirings/workflow/workflow-run-engine.types.ts +91 -0
  324. package/src/wirings/workflow/workflow-run-ownership.ts +36 -0
  325. package/src/wirings/workflow/workflow-suspend.ts +61 -0
  326. package/src/wirings/workflow/workflow.types.ts +7 -0
  327. package/src/wirings-stay-decoupled.test.ts +123 -0
  328. package/tsconfig.json +1 -1
  329. package/tsconfig.tsbuildinfo +1 -1
  330. package/src/internal.ts +0 -10
@@ -1,53 +1,23 @@
1
- import { runPikkuFunc, addFunction } from '../../function/function-runner.js'
2
- import {
3
- pikkuWorkflowWorkerFunc,
4
- pikkuWorkflowOrchestratorFunc,
5
- pikkuWorkflowSleeperFunc,
6
- } from './workflow-queue-workers.js'
7
- import { wireQueueWorker } from '../queue/queue-runner.js'
1
+ import { runPikkuFunc } from '../../function/function-runner.js'
8
2
  import {
9
3
  getSingletonServices,
10
4
  getCreateWireServices,
11
5
  pikkuState,
12
6
  } from '../../pikku-state.js'
13
7
  import { getDurationInMilliseconds } from '../../time-utils.js'
14
-
15
- const resolveWorkflowMeta = (
16
- name: string
17
- ): { meta: any; packageName: string | null; resolvedName: string } | null => {
18
- const rootMeta = pikkuState(null, 'workflows', 'meta')
19
- if (rootMeta[name]) {
20
- return { meta: rootMeta[name], packageName: null, resolvedName: name }
21
- }
22
-
23
- const colonIndex = name.indexOf(':')
24
- if (colonIndex !== -1) {
25
- const namespace = name.substring(0, colonIndex)
26
- const localName = name.substring(colonIndex + 1)
27
- const addons = pikkuState(null, 'addons', 'packages')
28
- const pkgConfig = addons?.get(namespace)
29
- if (pkgConfig) {
30
- const addonMeta = pikkuState(pkgConfig.package, 'workflows', 'meta')
31
- if (addonMeta?.[localName]) {
32
- return {
33
- meta: addonMeta[localName],
34
- packageName: pkgConfig.package,
35
- resolvedName: localName,
36
- }
37
- }
38
- }
39
- }
40
-
41
- return null
42
- }
43
-
44
- const toKebab = (s: string) =>
45
- s.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase()
46
- import type { PikkuWire, SerializedError } from '../../types/core.types.js'
47
- import type { QueueService } from '../queue/queue.types.js'
8
+ import type {
9
+ CoreUserSession,
10
+ PikkuRawWire,
11
+ SerializedError,
12
+ } from '../../types/core.types.js'
13
+ import type {
14
+ GroupConcurrencyConfig,
15
+ JobGroup,
16
+ JobOptions,
17
+ QueueService,
18
+ } from '../queue/queue.types.js'
48
19
  import type {
49
20
  ApprovalOutcome,
50
- CoreWorkflow,
51
21
  PikkuWorkflowWire,
52
22
  StepState,
53
23
  StepStatus,
@@ -64,6 +34,7 @@ import type {
64
34
  WorkflowStepOptions,
65
35
  } from './workflow.types.js'
66
36
  import {
37
+ ChildWorkflowStartedException,
67
38
  continueGraph,
68
39
  executeGraphStep,
69
40
  runWorkflowGraph,
@@ -71,251 +42,65 @@ import {
71
42
  } from './graph/graph-runner.js'
72
43
  import type { WorkflowService } from '../../services/workflow-service.js'
73
44
  import type { ScenarioPersonas } from '../../services/personas-service.js'
74
- import {
75
- PikkuError,
76
- addError,
77
- isExpectedError,
78
- } from '../../errors/error-handler.js'
45
+ import { isExpectedError } from '../../errors/error-handler.js'
46
+ import { PikkuMissingMetaError } from '../../errors/errors.js'
79
47
  import { RPCNotFoundError } from '../rpc/rpc-runner.js'
80
- import { ChildWorkflowStartedException } from './graph/graph-runner.js'
48
+ import type { PikkuRPC } from '../rpc/rpc-types.js'
81
49
  import { deriveInvocationId } from './workflow-invocation-id.js'
50
+ import { assertWorkflowRunOwner } from './workflow-run-ownership.js'
82
51
  import {
83
52
  buildRunTimeline,
84
53
  reconstructStateAt,
85
54
  type RunTimeline,
86
55
  type ReconstructedRunState,
87
56
  } from './run-timeline.js'
57
+ import {
58
+ DEFAULT_STEP_RETRIES,
59
+ WORKFLOW_CHILD_POLL_MAX_MS,
60
+ WORKFLOW_END_STATES,
61
+ WORKFLOW_POLL_FACTOR,
62
+ WORKFLOW_POLL_MIN_MS,
63
+ WORKFLOW_TERMINAL_STATES,
64
+ } from './workflow-constants.js'
65
+ import {
66
+ WorkflowAsyncException,
67
+ WorkflowCancelledException,
68
+ WorkflowDispatchException,
69
+ WorkflowNotFoundError,
70
+ WorkflowRunCancelledError,
71
+ WorkflowRunFailedError,
72
+ WorkflowRunNotFoundError,
73
+ WorkflowStepFunctionMismatchError,
74
+ WorkflowStepNameNotString,
75
+ WorkflowSuspendedException,
76
+ } from './workflow-errors.js'
88
77
  import type {
89
- GroupConcurrencyConfig,
90
- JobGroup,
91
- JobOptions,
92
- PikkuWorkerConfig,
93
- } from '../queue/queue.types.js'
94
-
95
- export const DEFAULT_STEP_RETRIES = 5
96
-
97
- export class WorkflowAsyncException extends Error {
98
- constructor(
99
- public readonly runId: string,
100
- public readonly stepName: string
101
- ) {
102
- super(`Workflow paused at step: ${stepName}`)
103
- this.name = 'WorkflowAsyncException'
104
- }
105
- }
106
-
107
- export class WorkflowCancelledException extends Error {
108
- constructor(
109
- public readonly runId: string,
110
- public readonly reason?: string
111
- ) {
112
- super(reason || 'Workflow cancelled')
113
- this.name = 'WorkflowCancelledException'
114
- }
115
- }
116
-
117
- export class WorkflowSuspendedException extends Error {
118
- constructor(
119
- public readonly runId: string,
120
- public readonly reason: string
121
- ) {
122
- super(reason || 'Workflow suspended')
123
- this.name = 'WorkflowSuspendedException'
124
- }
125
- }
126
-
127
- export class WorkflowDispatchException extends Error {
128
- constructor(
129
- public readonly runId: string,
130
- public readonly stepName: string,
131
- options?: { cause?: unknown }
132
- ) {
133
- super(
134
- `Failed to dispatch workflow step '${stepName}' (run ${runId})`,
135
- options
136
- )
137
- this.name = 'WorkflowDispatchException'
138
- }
139
- }
140
-
141
- export class WorkflowNotFoundError extends PikkuError {
142
- constructor(name: string) {
143
- super(`Workflow not found: ${name}`)
144
- }
145
- }
146
- addError(WorkflowNotFoundError, {
147
- status: 404,
148
- message: 'Workflow not found.',
149
- })
150
-
151
- export class WorkflowRunNotFoundError extends PikkuError {
152
- constructor(runId: string) {
153
- super(`Workflow run not found: ${runId}`)
154
- }
155
- }
156
- addError(WorkflowRunNotFoundError, {
157
- status: 404,
158
- message: 'Workflow run not found.',
159
- })
160
-
161
- export class WorkflowRunFailedError extends PikkuError {
162
- public payload: { message?: string }
163
- constructor(message?: string) {
164
- super(`Workflow run failed: ${message ?? 'unknown'}`)
165
- this.payload = { message }
166
- }
167
- }
168
- addError(WorkflowRunFailedError, {
169
- status: 422,
170
- message: 'Workflow run failed.',
171
- })
172
-
173
- export class WorkflowRunCancelledError extends PikkuError {
174
- constructor() {
175
- super('Workflow was cancelled')
176
- }
177
- }
178
- addError(WorkflowRunCancelledError, {
179
- status: 409,
180
- message: 'Workflow was cancelled.',
181
- })
182
-
183
- export class WorkflowApprovalResolvedError extends PikkuError {
184
- public payload: {
185
- reason: string
186
- outcome: ApprovalOutcome<unknown>['status']
187
- }
188
- constructor(reason: string, outcome: ApprovalOutcome<unknown>['status']) {
189
- super(`Approval already ${outcome}: ${reason}`)
190
- this.payload = { reason, outcome }
191
- }
192
- }
193
- addError(WorkflowApprovalResolvedError, {
194
- status: 409,
195
- message: 'Approval has already been resolved.',
196
- })
197
-
198
- export class WorkflowServiceNotInitialized extends Error {}
199
- export class WorkflowStepNameNotString extends Error {
200
- constructor(stepName: any) {
201
- super(`Workflow step name must be a string. Received: ${typeof stepName}`)
202
- }
203
- }
204
-
205
- const WORKFLOW_END_STATES: ReadonlySet<string> = new Set([
206
- 'completed',
207
- 'failed',
208
- 'cancelled',
209
- 'suspended',
210
- ])
211
-
212
- export interface RunLifecycleContext {
213
- runId: string
214
- run: WorkflowRun
215
- workflowMeta: any
216
- workflow: CoreWorkflow
217
- wire: PikkuWire
218
- packageName: string | null
219
- }
220
-
221
- export interface WorkflowRunEngine {
222
- inlineStep(
223
- runId: string,
224
- logicalStepName: string,
225
- fn: Function,
226
- stepOptions?: WorkflowStepOptions,
227
- data?: any,
228
- funcName?: string
229
- ): Promise<any>
230
- updateRunStatus(
231
- runId: string,
232
- status: WorkflowStatus,
233
- output?: any,
234
- error?: SerializedError
235
- ): Promise<void>
236
- onChildWorkflowFailed(run: WorkflowRun, error: unknown): Promise<void>
237
- verifyStepName(stepName: unknown): void
238
- }
239
-
240
- export interface WorkflowRunExtension {
241
- attachRunContext(
242
- runId: string,
243
- workflowMeta: any,
244
- options?: Record<string, any>
245
- ): Promise<void>
246
- detachRunContext(runId: string): void
247
- decorateRunWire(
248
- wire: PikkuWire,
249
- context: {
250
- runId: string
251
- workflowMeta: any
252
- workflowWire: PikkuWorkflowWire
253
- }
254
- ): void
255
- decorateWorkflowWire(
256
- workflowWire: PikkuWorkflowWire,
257
- context: {
258
- name: string
259
- runId: string
260
- rpcService: any
261
- addonNamespace?: string | null
262
- }
263
- ): void
264
- onBeforeRunFunc(context: RunLifecycleContext): Promise<void>
265
- onAfterRunFunc(
266
- context: RunLifecycleContext,
267
- outcome: 'completed' | 'failed' | 'interrupted',
268
- failure: unknown
269
- ): Promise<void>
270
- }
271
-
272
- const WORKFLOW_TERMINAL_STATES: ReadonlySet<string> = new Set([
273
- 'completed',
274
- 'failed',
275
- 'cancelled',
276
- ])
277
-
278
- /** Idle window before a `running` run with nothing in flight is treated as stalled. */
279
- const DEFAULT_STALLED_RUN_MS = 5 * 60_000
280
-
281
- /** Runs re-driven per `recoverStalledRuns` call, so one sweep is bounded. */
282
- const DEFAULT_STALLED_RUN_LIMIT = 100
283
-
284
- /**
285
- * How long a step may sit `pending` before the relay assumes its dispatch was
286
- * lost. This is a bet that no healthy queue takes this long to move a job from
287
- * `pending` to `running`; set it above the observed p99 of that latency.
288
- */
289
- const DEFAULT_UNDISPATCHED_STEP_MS = 30_000
290
-
291
- /** Steps re-driven per `relayUndispatchedSteps` call, so one tick is bounded. */
292
- const DEFAULT_UNDISPATCHED_STEP_LIMIT = 100
293
-
294
- /** First wait before a step already re-dispatched once is re-dispatched again. */
295
- const REDISPATCH_BACKOFF_MS = 30_000
296
-
297
- /** Ceiling on the doubling backoff, so a permanently stuck step still gets swept. */
298
- const REDISPATCH_BACKOFF_MAX_MS = 10 * 60_000
299
-
300
- /** Bound on the in-process backoff map, so a long-lived process cannot grow it without bound. */
301
- const REDISPATCH_BACKOFF_MAX_ENTRIES = 10_000
302
-
303
- const WORKFLOW_POLL_MIN_MS = 10
304
-
305
- const WORKFLOW_POLL_FACTOR = 1.6
306
-
307
- const WORKFLOW_CHILD_POLL_MAX_MS = 500
308
-
309
- type RunContext = {
310
- activeExecutions: number
311
- inline?: boolean
312
- ordinals: Map<string, number>
313
- lastStep?: string
314
- replay?: {
315
- steps?: Map<string, StepState>
316
- run?: WorkflowRun
317
- }
318
- }
78
+ RunContext,
79
+ RunLifecycleContext,
80
+ WorkflowRunEngine,
81
+ WorkflowRunExtension,
82
+ } from './workflow-run-engine.types.js'
83
+ import { resolveWorkflowMeta } from './workflow-meta-resolver.js'
84
+ import {
85
+ jobGroupFor,
86
+ orchestratorQueueName,
87
+ resolveWorkflowConfig,
88
+ stepJobOptions,
89
+ stepWorkerQueueName,
90
+ } from './workflow-queue-routing.js'
91
+ import { wireWorkflowQueueWorkers } from './workflow-queue-wiring.js'
92
+ import {
93
+ approvalStepNameFor,
94
+ evaluateApprovalStep,
95
+ recordApprovalDecision,
96
+ type ApprovalStore,
97
+ } from './workflow-approval.js'
98
+ import { recordSuspension, suspendStepNameFor } from './workflow-suspend.js'
99
+ import {
100
+ RedispatchBackoff,
101
+ sweepStalledRuns,
102
+ sweepUndispatchedSteps,
103
+ } from './workflow-recovery.js'
319
104
 
320
105
  export abstract class PikkuWorkflowService implements WorkflowService {
321
106
  private runExtension?: WorkflowRunExtension
@@ -406,112 +191,12 @@ export abstract class PikkuWorkflowService implements WorkflowService {
406
191
  }
407
192
 
408
193
  public wireQueueWorkers(): void {
409
- const functions = pikkuState(null, 'function', 'functions')
410
- const functionsMeta = pikkuState(null, 'function', 'meta')
411
-
412
- const mkMeta = (funcId: string) => ({
413
- pikkuFuncId: funcId,
414
- sessionless: true,
415
- functionType: 'helper' as const,
416
- inputSchemaName: null,
417
- outputSchemaName: null,
194
+ wireWorkflowQueueWorkers({
195
+ strategy: this.queueStrategy,
196
+ concurrency: this.queueConcurrency,
197
+ groupConcurrency: this.queueGroupConcurrency,
198
+ getLogger: () => this.logger,
418
199
  })
419
-
420
- const queueMeta = pikkuState(null, 'queue', 'meta')
421
-
422
- const registerWorkflowFunc = (
423
- funcId: string,
424
- func: { func: unknown },
425
- queueName: string,
426
- config?: PikkuWorkerConfig
427
- ) => {
428
- if (functions.has(funcId)) return
429
- addFunction(funcId, func as never)
430
- if (!queueMeta[queueName]) {
431
- queueMeta[queueName] = { pikkuFuncId: funcId, name: queueName }
432
- }
433
- wireQueueWorker({ name: queueName, func, config } as never)
434
- if (!functionsMeta[funcId]) {
435
- functionsMeta[funcId] = mkMeta(funcId)
436
- }
437
- }
438
-
439
- const sharedGroups = this.queueStrategy === 'shared-groups'
440
- const sharedQueueConfig: PikkuWorkerConfig | undefined = sharedGroups
441
- ? {
442
- batchSize: this.queueConcurrency,
443
- groupConcurrency: this.queueGroupConcurrency,
444
- }
445
- : undefined
446
-
447
- registerWorkflowFunc(
448
- 'pikkuWorkflowOrchestrator',
449
- { func: pikkuWorkflowOrchestratorFunc },
450
- 'pikku-workflow-orchestrator',
451
- sharedQueueConfig
452
- )
453
- registerWorkflowFunc(
454
- 'pikkuWorkflowStepWorker',
455
- { func: pikkuWorkflowWorkerFunc },
456
- 'pikku-workflow-step-worker',
457
- sharedQueueConfig
458
- )
459
-
460
- const registerQueueWorkers = (queueMeta: Record<string, any>) => {
461
- for (const [queueName, meta] of Object.entries(queueMeta)) {
462
- if (functions.has(meta.pikkuFuncId)) continue
463
- if (queueName.startsWith('wf-orchestrator-')) {
464
- registerWorkflowFunc(
465
- meta.pikkuFuncId,
466
- { func: pikkuWorkflowOrchestratorFunc },
467
- queueName
468
- )
469
- } else if (queueName.startsWith('wf-step-')) {
470
- registerWorkflowFunc(
471
- meta.pikkuFuncId,
472
- { func: pikkuWorkflowWorkerFunc },
473
- queueName
474
- )
475
- }
476
- }
477
- }
478
-
479
- if (!sharedGroups) {
480
- registerQueueWorkers(pikkuState(null, 'queue', 'meta'))
481
-
482
- const addons = pikkuState(null, 'addons', 'packages')
483
- if (addons) {
484
- for (const [, addon] of addons) {
485
- const addonQueueMeta = pikkuState(addon.package, 'queue', 'meta')
486
- if (addonQueueMeta) {
487
- registerQueueWorkers(addonQueueMeta)
488
- }
489
- }
490
- }
491
- }
492
-
493
- const workflowCount = Object.keys(
494
- pikkuState(null, 'workflows', 'meta') ?? {}
495
- ).length
496
- const perWorkflowQueues = Object.keys(queueMeta).filter((name) =>
497
- name.startsWith('wf-orchestrator-')
498
- ).length
499
- if (workflowCount > 0 && perWorkflowQueues === 0) {
500
- this.logger?.warn?.(
501
- `[pikku] ${workflowCount} workflow(s) registered but no per-workflow orchestrator queues were found in queue meta. ` +
502
- `All workflows will share a single orchestrator queue, where one slow step blocks every other workflow behind it. ` +
503
- `Check that the generated bootstrap imports the queue-workers meta (pikku-queue-workers-wirings-meta.gen.js).`
504
- )
505
- }
506
-
507
- if (!functions.has('pikkuWorkflowSleeper')) {
508
- addFunction('pikkuWorkflowSleeper', {
509
- func: pikkuWorkflowSleeperFunc,
510
- })
511
- }
512
- if (!functionsMeta.pikkuWorkflowSleeper) {
513
- functionsMeta.pikkuWorkflowSleeper = mkMeta('pikkuWorkflowSleeper')
514
- }
515
200
  }
516
201
 
517
202
  protected async isInline(runId: string): Promise<boolean> {
@@ -774,7 +459,7 @@ export abstract class PikkuWorkflowService implements WorkflowService {
774
459
  const serialized: SerializedError = {
775
460
  message: error.message,
776
461
  stack: error.stack,
777
- code: (error as any).code,
462
+ code: (error as { code?: string }).code,
778
463
  expected: isExpectedError(error),
779
464
  }
780
465
  return mirror.setStepError(stepId, serialized)
@@ -796,7 +481,7 @@ export abstract class PikkuWorkflowService implements WorkflowService {
796
481
  (mirror, newStep) =>
797
482
  mirror.createRetryAttempt(failedStepId, {
798
483
  ...newStep,
799
- stepName: (newStep as any).stepName ?? '',
484
+ stepName: (newStep as { stepName?: string }).stepName ?? '',
800
485
  })
801
486
  )
802
487
  }
@@ -949,51 +634,6 @@ export abstract class PikkuWorkflowService implements WorkflowService {
949
634
  return []
950
635
  }
951
636
 
952
- /**
953
- * Re-drive runs whose next move was lost, and report which were resumed.
954
- *
955
- * Arming a step is two writes to two systems — the step row, then the queue
956
- * or scheduler job — so a process that dies between them leaves a run that is
957
- * `running` with nothing in flight. Nothing notices: the run parks on a step
958
- * that will never complete and never error, so it neither finishes nor fails.
959
- * (Seen on a `workflow.sleep()`: a deploy restart landed between the sleep
960
- * step's insert and its timer, parking the run permanently.)
961
- *
962
- * Replay is the recovery — `resumeWorkflow` re-orchestrates from persisted
963
- * step state, and every settled step is memoized, so resuming a run that was
964
- * not actually stuck costs an orchestration pass and changes nothing. That
965
- * idempotence is what makes an idle-time heuristic safe here; a run that is
966
- * legitimately mid-sleep is excluded anyway, since its step is `scheduled`.
967
- *
968
- * This is not self-starting. Call it from a scheduled task at whatever
969
- * interval suits the workload.
970
- */
971
- public async recoverStalledRuns(options?: {
972
- stalledAfterMs?: number
973
- limit?: number
974
- }): Promise<{ resumed: string[] }> {
975
- const before = new Date(
976
- Date.now() - (options?.stalledAfterMs ?? DEFAULT_STALLED_RUN_MS)
977
- )
978
- const runIds = await this.findStalledRunIds(
979
- before,
980
- options?.limit ?? DEFAULT_STALLED_RUN_LIMIT
981
- )
982
- const resumed: string[] = []
983
- for (const runId of runIds) {
984
- try {
985
- await this.resumeWorkflow(runId)
986
- resumed.push(runId)
987
- } catch (err) {
988
- // One unresumable run must not stop the sweep from recovering the rest.
989
- getSingletonServices()?.logger?.error(
990
- `Failed to resume stalled workflow run ${runId}: ${err instanceof Error ? err.message : String(err)}`
991
- )
992
- }
993
- }
994
- return { resumed }
995
- }
996
-
997
637
  /**
998
638
  * Runs holding a step that has sat `pending` since before `before`, paired
999
639
  * with the step that flagged them.
@@ -1014,110 +654,51 @@ export abstract class PikkuWorkflowService implements WorkflowService {
1014
654
  return []
1015
655
  }
1016
656
 
657
+ private readonly redispatchBackoff = new RedispatchBackoff()
658
+
659
+ private get sweepDeps() {
660
+ return {
661
+ resume: (runId: string) => this.resumeWorkflow(runId),
662
+ logger: getSingletonServices()?.logger,
663
+ }
664
+ }
665
+
1017
666
  /**
1018
- * Re-drive steps whose dispatch was lost, and report which runs were nudged.
1019
- *
1020
- * Arming a step is two writes to two systems: the step row lands `pending`,
1021
- * then a queue or scheduler job is published. Nothing spans both, so a crash
1022
- * in between leaves a durable row that nothing will ever pick up — the run
1023
- * neither finishes nor fails. (Seen on a `workflow.sleep()`: a deploy restart
1024
- * landed between the sleep step's insert and its timer.)
1025
- *
1026
- * The row is the outbox record and this is the relay. Age is the only signal
1027
- * available — a step `pending` because its dispatch was lost is
1028
- * indistinguishable from one whose job is merely still queued — so a step
1029
- * past `undispatchedAfterMs` is re-dispatched regardless, and correctness
1030
- * rests on the claim rather than on the guess being right. A redundant
1031
- * dispatch costs one queue message: the loser reads `running` and returns
1032
- * without invoking anything.
1033
- *
1034
- * Re-dispatches back off per step (doubling from 30s, capped at 10m) so a
1035
- * genuine queue backlog is not amplified by a tick that keeps firing at the
1036
- * steps the backlog is already delaying. The backoff is per process and
1037
- * advisory — losing it on restart costs extra dispatches, never correctness.
1038
- *
1039
- * This is not self-starting. Call it from a scheduled task; ~30s suits a
1040
- * queue whose `pending`→`running` latency is well under that.
667
+ * Re-drive runs whose next move was lost. Not self-starting call it from a
668
+ * scheduled task at whatever interval suits the workload.
1041
669
  */
1042
- public async relayUndispatchedSteps(options?: {
1043
- undispatchedAfterMs?: number
670
+ public async recoverStalledRuns(options?: {
671
+ stalledAfterMs?: number
1044
672
  limit?: number
1045
- }): Promise<{ redispatched: string[] }> {
1046
- const before = new Date(
1047
- Date.now() -
1048
- (options?.undispatchedAfterMs ?? DEFAULT_UNDISPATCHED_STEP_MS)
1049
- )
1050
- const steps = await this.findUndispatchedSteps(
1051
- before,
1052
- options?.limit ?? DEFAULT_UNDISPATCHED_STEP_LIMIT
673
+ }): Promise<{ resumed: string[] }> {
674
+ return sweepStalledRuns(
675
+ (before, limit) => this.findStalledRunIds(before, limit),
676
+ options,
677
+ this.sweepDeps
1053
678
  )
1054
-
1055
- // Backoff is keyed by run, not step, because the run is the unit of
1056
- // re-drive: `resumeWorkflow` replays the whole run and re-dispatches every
1057
- // step still owed a job. Holding off a single step while resuming its run
1058
- // would not suppress anything.
1059
- const now = Date.now()
1060
- const runIds = new Set<string>()
1061
- for (const { runId } of steps) {
1062
- const eligibleAt = this.redispatchBackoff.get(runId)
1063
- if (eligibleAt !== undefined && eligibleAt > now) {
1064
- continue
1065
- }
1066
- this.noteRedispatch(runId, now)
1067
- runIds.add(runId)
1068
- }
1069
-
1070
- const redispatched: string[] = []
1071
- for (const runId of runIds) {
1072
- try {
1073
- await this.resumeWorkflow(runId)
1074
- redispatched.push(runId)
1075
- } catch (err) {
1076
- // One unresumable run must not stop the tick from relaying the rest.
1077
- getSingletonServices()?.logger?.error(
1078
- `Failed to re-dispatch workflow run ${runId}: ${err instanceof Error ? err.message : String(err)}`
1079
- )
1080
- }
1081
- }
1082
- return { redispatched }
1083
679
  }
1084
680
 
1085
- /** Advisory, per-process record of when a run may next be re-dispatched. */
1086
- private readonly redispatchBackoff = new Map<string, number>()
1087
-
1088
- private readonly redispatchDelays = new Map<string, number>()
1089
-
1090
- private noteRedispatch(runId: string, now: number): void {
1091
- const previous = this.redispatchDelays.get(runId)
1092
- const delay = Math.min(
1093
- previous === undefined ? REDISPATCH_BACKOFF_MS : previous * 2,
1094
- REDISPATCH_BACKOFF_MAX_MS
681
+ /**
682
+ * Re-drive steps whose dispatch was lost. Not self-starting — call it from a
683
+ * scheduled task; ~30s suits a queue whose `pending`→`running` latency is
684
+ * well under that.
685
+ */
686
+ public async relayUndispatchedSteps(options?: {
687
+ undispatchedAfterMs?: number
688
+ limit?: number
689
+ }): Promise<{ redispatched: string[] }> {
690
+ return sweepUndispatchedSteps(
691
+ (before, limit) => this.findUndispatchedSteps(before, limit),
692
+ this.redispatchBackoff,
693
+ options,
694
+ this.sweepDeps
1095
695
  )
1096
- // A run that settles is never returned again, so entries are only evicted by
1097
- // this bound — oldest first, which is also least recently re-dispatched.
1098
- if (this.redispatchBackoff.size >= REDISPATCH_BACKOFF_MAX_ENTRIES) {
1099
- const oldest = this.redispatchBackoff.keys().next()
1100
- if (!oldest.done) {
1101
- this.redispatchBackoff.delete(oldest.value)
1102
- this.redispatchDelays.delete(oldest.value)
1103
- }
1104
- }
1105
- this.redispatchDelays.set(runId, delay)
1106
- this.redispatchBackoff.set(runId, now + delay)
1107
696
  }
1108
697
 
1109
698
  protected resolveStepJobOptions(
1110
699
  stepOptions?: WorkflowStepOptions
1111
700
  ): JobOptions {
1112
- const retries = stepOptions?.retries ?? DEFAULT_STEP_RETRIES
1113
- const retryDelay = stepOptions?.retryDelay
1114
- const backoff =
1115
- retryDelay !== undefined && retryDelay !== 'exponential'
1116
- ? { type: 'fixed', delay: getDurationInMilliseconds(retryDelay) }
1117
- : retries > 0 || retryDelay === 'exponential'
1118
- ? 'exponential'
1119
- : undefined
1120
- return { attempts: retries + 1, ...(backoff ? { backoff } : {}) }
701
+ return stepJobOptions(stepOptions)
1121
702
  }
1122
703
 
1123
704
  public async queueStepWorker(
@@ -1246,7 +827,7 @@ export abstract class PikkuWorkflowService implements WorkflowService {
1246
827
  name: string,
1247
828
  input: I,
1248
829
  wire: WorkflowRunWire,
1249
- rpcService: any,
830
+ rpcService: PikkuRPC,
1250
831
  options?: {
1251
832
  inline?: boolean
1252
833
  startNode?: string
@@ -1346,7 +927,7 @@ export abstract class PikkuWorkflowService implements WorkflowService {
1346
927
  public async runToCompletion<I>(
1347
928
  name: string,
1348
929
  input: I,
1349
- rpcService: any,
930
+ rpcService: PikkuRPC,
1350
931
  options?: { pollIntervalMs?: number; wire?: WorkflowRunWire }
1351
932
  ): Promise<any> {
1352
933
  const { runId } = await this.startWorkflow(
@@ -1468,7 +1049,10 @@ export abstract class PikkuWorkflowService implements WorkflowService {
1468
1049
  return stepName
1469
1050
  }
1470
1051
 
1471
- public async runWorkflowJob(runId: string, rpcService: any): Promise<void> {
1052
+ public async runWorkflowJob(
1053
+ runId: string,
1054
+ rpcService: PikkuRPC
1055
+ ): Promise<void> {
1472
1056
  await this.beginReplay(runId)
1473
1057
  try {
1474
1058
  await this.runWorkflowJobInner(runId, rpcService)
@@ -1479,7 +1063,7 @@ export abstract class PikkuWorkflowService implements WorkflowService {
1479
1063
 
1480
1064
  private async runWorkflowJobInner(
1481
1065
  runId: string,
1482
- rpcService: any
1066
+ rpcService: PikkuRPC
1483
1067
  ): Promise<void> {
1484
1068
  const run = await this.getRunIdentity(runId)
1485
1069
  if (!run) {
@@ -1517,6 +1101,12 @@ export abstract class PikkuWorkflowService implements WorkflowService {
1517
1101
  }
1518
1102
 
1519
1103
  const registrations = pikkuState(pkgName, 'workflows', 'registrations')
1104
+ if (!workflowMeta) {
1105
+ throw new PikkuMissingMetaError(
1106
+ `Missing generated metadata for workflow '${run.workflow}'`
1107
+ )
1108
+ }
1109
+
1520
1110
  const workflow = registrations.get(resolved?.resolvedName ?? run.workflow)
1521
1111
  if (!workflow) {
1522
1112
  throw new WorkflowNotFoundError(run.workflow)
@@ -1533,11 +1123,10 @@ export abstract class PikkuWorkflowService implements WorkflowService {
1533
1123
  addonNs
1534
1124
  )
1535
1125
  workflowWire.pikkuUserId = run.wire?.pikkuUserId
1536
- const wire: PikkuWire = {
1126
+ // No `rpc` yet — runPikkuFunc attaches it lazily for the invocation.
1127
+ const wire: PikkuRawWire = {
1537
1128
  workflow: workflowWire,
1538
1129
  pikkuUserId: run.wire?.pikkuUserId,
1539
- session: rpcService?.wire?.session,
1540
- rpc: rpcService?.wire?.rpc,
1541
1130
  }
1542
1131
  this.runExtension?.decorateRunWire(wire, {
1543
1132
  runId,
@@ -1649,7 +1238,7 @@ export abstract class PikkuWorkflowService implements WorkflowService {
1649
1238
  private async runVersionMismatchFallback(
1650
1239
  run: WorkflowRun,
1651
1240
  currentMeta: { source: string },
1652
- rpcService: any
1241
+ rpcService: PikkuRPC
1653
1242
  ): Promise<void> {
1654
1243
  const source = currentMeta.source
1655
1244
 
@@ -1680,7 +1269,7 @@ export abstract class PikkuWorkflowService implements WorkflowService {
1680
1269
  stepName: string,
1681
1270
  rpcName: string,
1682
1271
  data: any,
1683
- rpcService: any
1272
+ rpcService: PikkuRPC
1684
1273
  ): Promise<void> {
1685
1274
  this.enterExecution(runId)
1686
1275
  try {
@@ -1701,10 +1290,17 @@ export abstract class PikkuWorkflowService implements WorkflowService {
1701
1290
  stepName: string,
1702
1291
  rpcName: string,
1703
1292
  data: any,
1704
- rpcService: any
1293
+ rpcService: PikkuRPC
1705
1294
  ): Promise<void> {
1706
1295
  const claimed = await this.withStepLock(runId, stepName, async () => {
1707
1296
  const stepState = await this.getStepState(runId, stepName)
1297
+ // knowledge: decisions/security/a-step-runs-the-function-the-workflow-dispatched-it-with.md
1298
+ if (
1299
+ stepState.rpcName !== undefined &&
1300
+ stepState.rpcName !== (rpcName ?? null)
1301
+ ) {
1302
+ throw new WorkflowStepFunctionMismatchError(runId, stepName)
1303
+ }
1708
1304
  if (stepState.status === 'succeeded' || stepState.status === 'running') {
1709
1305
  return null
1710
1306
  }
@@ -1751,7 +1347,7 @@ export abstract class PikkuWorkflowService implements WorkflowService {
1751
1347
  runId,
1752
1348
  stepState.stepId,
1753
1349
  graphNodeId,
1754
- rpcName,
1350
+ workflowMeta!.nodes![graphNodeId].rpcName,
1755
1351
  data,
1756
1352
  run.workflow
1757
1353
  )
@@ -1763,7 +1359,7 @@ export abstract class PikkuWorkflowService implements WorkflowService {
1763
1359
  id: rpcName,
1764
1360
  parentRunId: runId,
1765
1361
  parentStepId: stepState.stepId,
1766
- pikkuUserId: rpcService.wire?.pikkuUserId,
1362
+ pikkuUserId: run.wire?.pikkuUserId,
1767
1363
  }
1768
1364
  const shouldInline = !getSingletonServices()?.queueService
1769
1365
  const { runId: childRunId } = await this.startWorkflow(
@@ -1838,7 +1434,7 @@ export abstract class PikkuWorkflowService implements WorkflowService {
1838
1434
 
1839
1435
  public async orchestrateWorkflow(
1840
1436
  runId: string,
1841
- rpcService: any
1437
+ rpcService: PikkuRPC
1842
1438
  ): Promise<void> {
1843
1439
  try {
1844
1440
  await this.runWorkflowJob(runId, rpcService)
@@ -1885,7 +1481,7 @@ export abstract class PikkuWorkflowService implements WorkflowService {
1885
1481
  stepState: StepState,
1886
1482
  rpcName: string,
1887
1483
  data: any,
1888
- rpcService: any,
1484
+ rpcService: PikkuRPC,
1889
1485
  knownRun?: WorkflowRun | null
1890
1486
  ): Promise<any> {
1891
1487
  const run = knownRun ?? (await this.getRunIdentity(runId))
@@ -1943,7 +1539,7 @@ export abstract class PikkuWorkflowService implements WorkflowService {
1943
1539
  runId: string,
1944
1540
  stepName: string,
1945
1541
  onErrorRpcName: string,
1946
- rpcService: any,
1542
+ rpcService: PikkuRPC,
1947
1543
  error: Error
1948
1544
  ): Promise<void> {
1949
1545
  await this.rpcStep(
@@ -1961,7 +1557,7 @@ export abstract class PikkuWorkflowService implements WorkflowService {
1961
1557
  logicalStepName: string,
1962
1558
  rpcName: string,
1963
1559
  data: any,
1964
- rpcService: any,
1560
+ rpcService: PikkuRPC,
1965
1561
  stepOptions?: WorkflowStepOptions
1966
1562
  ): Promise<any> {
1967
1563
  const fromStepName = this.lastStepName(runId)
@@ -2043,7 +1639,7 @@ export abstract class PikkuWorkflowService implements WorkflowService {
2043
1639
  type: 'workflow',
2044
1640
  id: rpcName,
2045
1641
  parentRunId: runId,
2046
- pikkuUserId: rpcService.wire?.pikkuUserId,
1642
+ pikkuUserId: (await this.getRunIdentity(runId))?.wire?.pikkuUserId,
2047
1643
  }
2048
1644
  const { runId: childRunId } = await this.startWorkflow(
2049
1645
  rpcName,
@@ -2182,59 +1778,18 @@ export abstract class PikkuWorkflowService implements WorkflowService {
2182
1778
  await this.setStepResult(stepState.stepId, null)
2183
1779
  }
2184
1780
 
2185
- private getSuspendStepName(reason: string): string {
2186
- return `__workflow_suspend:${reason}`
2187
- }
2188
-
2189
1781
  private async suspendStep(runId: string, reason: string): Promise<void> {
2190
1782
  const fromStepName = this.lastStepName(runId)
2191
- const suspendStepName = this.nextStepKey(
2192
- runId,
2193
- this.getSuspendStepName(reason)
2194
- )
2195
- await this.withStepLock(runId, suspendStepName, async () => {
2196
- let stepState: StepState
2197
- try {
2198
- stepState = await this.getStepState(runId, suspendStepName)
2199
- } catch {
2200
- stepState = await this.insertStepState(
2201
- runId,
2202
- suspendStepName,
2203
- 'pikkuWorkflowSuspend',
2204
- {
2205
- reason,
2206
- },
2207
- undefined,
2208
- fromStepName
2209
- )
2210
- }
2211
- if (!stepState.stepId) {
2212
- stepState = await this.insertStepState(
2213
- runId,
2214
- suspendStepName,
2215
- 'pikkuWorkflowSuspend',
2216
- {
2217
- reason,
2218
- },
2219
- undefined,
2220
- fromStepName
2221
- )
2222
- }
2223
-
2224
- if (stepState.status === 'succeeded') {
2225
- return
2226
- }
2227
-
2228
- if (stepState.status === 'pending') {
2229
- await this.setStepRunning(stepState.stepId)
2230
- }
2231
-
2232
- await this.setStepResult(stepState.stepId, {
1783
+ const suspendStepName = this.nextStepKey(runId, suspendStepNameFor(reason))
1784
+ await this.withStepLock(runId, suspendStepName, () =>
1785
+ recordSuspension(
1786
+ this.approvalStore,
1787
+ runId,
2233
1788
  reason,
2234
- suspendedAt: new Date().toISOString(),
2235
- })
2236
- throw new WorkflowSuspendedException(runId, reason)
2237
- })
1789
+ suspendStepName,
1790
+ fromStepName
1791
+ )
1792
+ )
2238
1793
  }
2239
1794
 
2240
1795
  private async scheduleRunWake(runId: string, delay: number): Promise<void> {
@@ -2259,49 +1814,44 @@ export abstract class PikkuWorkflowService implements WorkflowService {
2259
1814
  }
2260
1815
  }
2261
1816
 
2262
- private getApprovalStepName(reason: string): string {
2263
- return `__workflow_approval:${reason}`
2264
- }
2265
-
2266
- private approvalStateKey(stepName: string): string {
2267
- let hex = ''
2268
- for (const byte of new TextEncoder().encode(stepName)) {
2269
- hex += byte.toString(16).padStart(2, '0')
1817
+ private get approvalStore(): ApprovalStore {
1818
+ return {
1819
+ getStepState: (runId, stepName) => this.getStepState(runId, stepName),
1820
+ insertStepState: (
1821
+ runId,
1822
+ stepName,
1823
+ rpcName,
1824
+ input,
1825
+ output,
1826
+ fromStepName
1827
+ ) =>
1828
+ this.insertStepState(
1829
+ runId,
1830
+ stepName,
1831
+ rpcName,
1832
+ input,
1833
+ output,
1834
+ fromStepName
1835
+ ),
1836
+ setStepRunning: (stepId) => this.setStepRunning(stepId),
1837
+ setStepResult: (stepId, result) => this.setStepResult(stepId, result),
1838
+ getRunState: (runId) => this.getRunState(runId),
1839
+ updateRunState: (runId, key, value) =>
1840
+ this.updateRunState(runId, key, value),
1841
+ resumeWorkflow: (runId) => this.resumeWorkflow(runId),
1842
+ scheduleRunWake: (runId, delay) => this.scheduleRunWake(runId, delay),
2270
1843
  }
2271
- return `__approval_${hex}`
2272
1844
  }
2273
1845
 
2274
1846
  public async approveStep(
2275
1847
  runId: string,
2276
1848
  reason: string,
2277
- decision: unknown
1849
+ decision: unknown,
1850
+ session?: CoreUserSession
2278
1851
  ): Promise<void> {
2279
- const stepName = this.getApprovalStepName(reason)
2280
- const stateKey = this.approvalStateKey(stepName)
2281
-
2282
- let resolved: StepState | undefined
2283
- try {
2284
- resolved = await this.getStepState(runId, stepName)
2285
- } catch {
2286
- // knowledge: decisions/security/workflow-approval-payloads-are-validated-on-replay-inside-the-workflow.md
2287
- }
2288
- if (resolved?.stepId && resolved.status === 'succeeded') {
2289
- const outcome = resolved.result as ApprovalOutcome<unknown> | undefined
2290
- throw new WorkflowApprovalResolvedError(
2291
- reason,
2292
- outcome?.status ?? 'decided'
2293
- )
2294
- }
1852
+ assertWorkflowRunOwner((await this.getRunIdentity(runId))?.wire, session)
2295
1853
 
2296
- const state = await this.getRunState(runId)
2297
- const record = (state[stateKey] ?? {}) as Record<string, unknown>
2298
- await this.updateRunState(runId, stateKey, {
2299
- ...record,
2300
- decision,
2301
- decidedAt: new Date().toISOString(),
2302
- error: undefined,
2303
- })
2304
- await this.resumeWorkflow(runId)
1854
+ return recordApprovalDecision(this.approvalStore, runId, reason, decision)
2305
1855
  }
2306
1856
 
2307
1857
  private async approvalStep(
@@ -2312,100 +1862,24 @@ export abstract class PikkuWorkflowService implements WorkflowService {
2312
1862
  const fromStepName = this.lastStepName(runId)
2313
1863
  const approvalStepName = this.nextStepKey(
2314
1864
  runId,
2315
- this.getApprovalStepName(reason)
1865
+ approvalStepNameFor(reason)
1866
+ )
1867
+ return await this.withStepLock(runId, approvalStepName, () =>
1868
+ evaluateApprovalStep(
1869
+ this.approvalStore,
1870
+ runId,
1871
+ reason,
1872
+ approvalStepName,
1873
+ fromStepName,
1874
+ options
1875
+ )
2316
1876
  )
2317
- return await this.withStepLock(runId, approvalStepName, async () => {
2318
- let stepState: StepState
2319
- try {
2320
- stepState = await this.getStepState(runId, approvalStepName)
2321
- } catch {
2322
- stepState = await this.insertStepState(
2323
- runId,
2324
- approvalStepName,
2325
- 'pikkuWorkflowApproval',
2326
- { reason, expiry: options.expiry },
2327
- undefined,
2328
- fromStepName
2329
- )
2330
- }
2331
- if (!stepState.stepId) {
2332
- stepState = await this.insertStepState(
2333
- runId,
2334
- approvalStepName,
2335
- 'pikkuWorkflowApproval',
2336
- { reason, expiry: options.expiry },
2337
- undefined,
2338
- fromStepName
2339
- )
2340
- }
2341
-
2342
- if (stepState.status === 'succeeded') {
2343
- return stepState.result as ApprovalOutcome<unknown>
2344
- }
2345
-
2346
- const stateKey = this.approvalStateKey(approvalStepName)
2347
- let record = ((await this.getRunState(runId))[stateKey] ?? {}) as {
2348
- decision?: unknown
2349
- decidedAt?: string
2350
- expiresAt?: string
2351
- error?: unknown
2352
- }
2353
-
2354
- if (stepState.status === 'pending') {
2355
- await this.setStepRunning(stepState.stepId)
2356
- if (options.expiry !== undefined && !record.expiresAt) {
2357
- const expiresAt = new Date(
2358
- Date.now() + getDurationInMilliseconds(options.expiry)
2359
- ).toISOString()
2360
- record = { ...record, expiresAt }
2361
- await this.updateRunState(runId, stateKey, record)
2362
- await this.scheduleRunWake(
2363
- runId,
2364
- getDurationInMilliseconds(options.expiry)
2365
- )
2366
- }
2367
- }
2368
-
2369
- if (record.decision !== undefined) {
2370
- const validation = await options.schema['~standard'].validate(
2371
- record.decision
2372
- )
2373
- if (validation.issues) {
2374
- await this.updateRunState(runId, stateKey, {
2375
- ...record,
2376
- decision: undefined,
2377
- decidedAt: undefined,
2378
- error: validation.issues.map((issue) => ({
2379
- message: issue.message,
2380
- path: issue.path?.map((segment) =>
2381
- typeof segment === 'object' ? segment.key : segment
2382
- ),
2383
- })),
2384
- })
2385
- throw new WorkflowSuspendedException(runId, reason)
2386
- }
2387
- const outcome: ApprovalOutcome<unknown> = {
2388
- status: 'decided',
2389
- data: validation.value,
2390
- }
2391
- await this.setStepResult(stepState.stepId, outcome)
2392
- return outcome
2393
- }
2394
-
2395
- if (record.expiresAt && Date.now() >= Date.parse(record.expiresAt)) {
2396
- const outcome: ApprovalOutcome<unknown> = { status: 'expired' }
2397
- await this.setStepResult(stepState.stepId, outcome)
2398
- return outcome
2399
- }
2400
-
2401
- throw new WorkflowSuspendedException(runId, reason)
2402
- })
2403
1877
  }
2404
1878
 
2405
1879
  public createWorkflowWire(
2406
1880
  name: string,
2407
1881
  runId: string,
2408
- rpcService: any,
1882
+ rpcService: PikkuRPC,
2409
1883
  addonNamespace?: string | null
2410
1884
  ): PikkuWorkflowWire {
2411
1885
  const workflowWire: PikkuWorkflowWire = {
@@ -2478,45 +1952,18 @@ export abstract class PikkuWorkflowService implements WorkflowService {
2478
1952
  }
2479
1953
 
2480
1954
  private getConfig(): WorkflowServiceConfig {
2481
- const singletonServices = getSingletonServices()
2482
- const workflow = singletonServices.config?.workflow
2483
- return {
2484
- retries: workflow?.retries ?? DEFAULT_STEP_RETRIES,
2485
- retryDelay: workflow?.retryDelay ?? 0,
2486
- orchestratorQueueName:
2487
- workflow?.orchestratorQueueName ?? 'pikku-workflow-orchestrator',
2488
- stepWorkerQueueName:
2489
- workflow?.stepWorkerQueueName ?? 'pikku-workflow-step-worker',
2490
- sleeperRPCName: workflow?.sleeperRPCName ?? 'pikkuWorkflowSleeper',
2491
- }
1955
+ return resolveWorkflowConfig()
2492
1956
  }
2493
1957
 
2494
1958
  protected getOrchestratorQueueName(workflowName?: string): string {
2495
- if (workflowName && this.queueStrategy !== 'shared-groups') {
2496
- const perWorkflow = `wf-orchestrator-${toKebab(workflowName)}`
2497
- const meta = pikkuState(null, 'queue', 'meta')
2498
- if (meta[perWorkflow]) {
2499
- return perWorkflow
2500
- }
2501
- }
2502
- return this.getConfig().orchestratorQueueName
1959
+ return orchestratorQueueName(this.queueStrategy, workflowName)
2503
1960
  }
2504
1961
 
2505
1962
  protected getStepWorkerQueueName(rpcName?: string): string {
2506
- if (rpcName && this.queueStrategy !== 'shared-groups') {
2507
- const perStep = `wf-step-${toKebab(rpcName)}`
2508
- const meta = pikkuState(null, 'queue', 'meta')
2509
- if (meta[perStep]) {
2510
- return perStep
2511
- }
2512
- }
2513
- return this.getConfig().stepWorkerQueueName
1963
+ return stepWorkerQueueName(this.queueStrategy, rpcName)
2514
1964
  }
2515
1965
 
2516
1966
  protected getJobGroup(id?: string): JobGroup | undefined {
2517
- if (!id || this.queueStrategy !== 'shared-groups') {
2518
- return undefined
2519
- }
2520
- return { id, tier: id }
1967
+ return jobGroupFor(this.queueStrategy, id)
2521
1968
  }
2522
1969
  }