@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,168 +1,21 @@
1
- import { runPikkuFunc, addFunction } from '../../function/function-runner.js';
2
- import { pikkuWorkflowWorkerFunc, pikkuWorkflowOrchestratorFunc, pikkuWorkflowSleeperFunc, } from './workflow-queue-workers.js';
3
- import { wireQueueWorker } from '../queue/queue-runner.js';
1
+ import { runPikkuFunc } from '../../function/function-runner.js';
4
2
  import { getSingletonServices, getCreateWireServices, pikkuState, } from '../../pikku-state.js';
5
3
  import { getDurationInMilliseconds } from '../../time-utils.js';
6
- const resolveWorkflowMeta = (name) => {
7
- const rootMeta = pikkuState(null, 'workflows', 'meta');
8
- if (rootMeta[name]) {
9
- return { meta: rootMeta[name], packageName: null, resolvedName: name };
10
- }
11
- const colonIndex = name.indexOf(':');
12
- if (colonIndex !== -1) {
13
- const namespace = name.substring(0, colonIndex);
14
- const localName = name.substring(colonIndex + 1);
15
- const addons = pikkuState(null, 'addons', 'packages');
16
- const pkgConfig = addons?.get(namespace);
17
- if (pkgConfig) {
18
- const addonMeta = pikkuState(pkgConfig.package, 'workflows', 'meta');
19
- if (addonMeta?.[localName]) {
20
- return {
21
- meta: addonMeta[localName],
22
- packageName: pkgConfig.package,
23
- resolvedName: localName,
24
- };
25
- }
26
- }
27
- }
28
- return null;
29
- };
30
- const toKebab = (s) => s.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase();
31
- import { continueGraph, executeGraphStep, runWorkflowGraph, runFromMeta, } from './graph/graph-runner.js';
32
- import { PikkuError, addError, isExpectedError, } from '../../errors/error-handler.js';
4
+ import { ChildWorkflowStartedException, continueGraph, executeGraphStep, runWorkflowGraph, runFromMeta, } from './graph/graph-runner.js';
5
+ import { isExpectedError } from '../../errors/error-handler.js';
6
+ import { PikkuMissingMetaError } from '../../errors/errors.js';
33
7
  import { RPCNotFoundError } from '../rpc/rpc-runner.js';
34
- import { ChildWorkflowStartedException } from './graph/graph-runner.js';
35
8
  import { deriveInvocationId } from './workflow-invocation-id.js';
9
+ import { assertWorkflowRunOwner } from './workflow-run-ownership.js';
36
10
  import { buildRunTimeline, reconstructStateAt, } from './run-timeline.js';
37
- export const DEFAULT_STEP_RETRIES = 5;
38
- export class WorkflowAsyncException extends Error {
39
- runId;
40
- stepName;
41
- constructor(runId, stepName) {
42
- super(`Workflow paused at step: ${stepName}`);
43
- this.runId = runId;
44
- this.stepName = stepName;
45
- this.name = 'WorkflowAsyncException';
46
- }
47
- }
48
- export class WorkflowCancelledException extends Error {
49
- runId;
50
- reason;
51
- constructor(runId, reason) {
52
- super(reason || 'Workflow cancelled');
53
- this.runId = runId;
54
- this.reason = reason;
55
- this.name = 'WorkflowCancelledException';
56
- }
57
- }
58
- export class WorkflowSuspendedException extends Error {
59
- runId;
60
- reason;
61
- constructor(runId, reason) {
62
- super(reason || 'Workflow suspended');
63
- this.runId = runId;
64
- this.reason = reason;
65
- this.name = 'WorkflowSuspendedException';
66
- }
67
- }
68
- export class WorkflowDispatchException extends Error {
69
- runId;
70
- stepName;
71
- constructor(runId, stepName, options) {
72
- super(`Failed to dispatch workflow step '${stepName}' (run ${runId})`, options);
73
- this.runId = runId;
74
- this.stepName = stepName;
75
- this.name = 'WorkflowDispatchException';
76
- }
77
- }
78
- export class WorkflowNotFoundError extends PikkuError {
79
- constructor(name) {
80
- super(`Workflow not found: ${name}`);
81
- }
82
- }
83
- addError(WorkflowNotFoundError, {
84
- status: 404,
85
- message: 'Workflow not found.',
86
- });
87
- export class WorkflowRunNotFoundError extends PikkuError {
88
- constructor(runId) {
89
- super(`Workflow run not found: ${runId}`);
90
- }
91
- }
92
- addError(WorkflowRunNotFoundError, {
93
- status: 404,
94
- message: 'Workflow run not found.',
95
- });
96
- export class WorkflowRunFailedError extends PikkuError {
97
- payload;
98
- constructor(message) {
99
- super(`Workflow run failed: ${message ?? 'unknown'}`);
100
- this.payload = { message };
101
- }
102
- }
103
- addError(WorkflowRunFailedError, {
104
- status: 422,
105
- message: 'Workflow run failed.',
106
- });
107
- export class WorkflowRunCancelledError extends PikkuError {
108
- constructor() {
109
- super('Workflow was cancelled');
110
- }
111
- }
112
- addError(WorkflowRunCancelledError, {
113
- status: 409,
114
- message: 'Workflow was cancelled.',
115
- });
116
- export class WorkflowApprovalResolvedError extends PikkuError {
117
- payload;
118
- constructor(reason, outcome) {
119
- super(`Approval already ${outcome}: ${reason}`);
120
- this.payload = { reason, outcome };
121
- }
122
- }
123
- addError(WorkflowApprovalResolvedError, {
124
- status: 409,
125
- message: 'Approval has already been resolved.',
126
- });
127
- export class WorkflowServiceNotInitialized extends Error {
128
- }
129
- export class WorkflowStepNameNotString extends Error {
130
- constructor(stepName) {
131
- super(`Workflow step name must be a string. Received: ${typeof stepName}`);
132
- }
133
- }
134
- const WORKFLOW_END_STATES = new Set([
135
- 'completed',
136
- 'failed',
137
- 'cancelled',
138
- 'suspended',
139
- ]);
140
- const WORKFLOW_TERMINAL_STATES = new Set([
141
- 'completed',
142
- 'failed',
143
- 'cancelled',
144
- ]);
145
- /** Idle window before a `running` run with nothing in flight is treated as stalled. */
146
- const DEFAULT_STALLED_RUN_MS = 5 * 60_000;
147
- /** Runs re-driven per `recoverStalledRuns` call, so one sweep is bounded. */
148
- const DEFAULT_STALLED_RUN_LIMIT = 100;
149
- /**
150
- * How long a step may sit `pending` before the relay assumes its dispatch was
151
- * lost. This is a bet that no healthy queue takes this long to move a job from
152
- * `pending` to `running`; set it above the observed p99 of that latency.
153
- */
154
- const DEFAULT_UNDISPATCHED_STEP_MS = 30_000;
155
- /** Steps re-driven per `relayUndispatchedSteps` call, so one tick is bounded. */
156
- const DEFAULT_UNDISPATCHED_STEP_LIMIT = 100;
157
- /** First wait before a step already re-dispatched once is re-dispatched again. */
158
- const REDISPATCH_BACKOFF_MS = 30_000;
159
- /** Ceiling on the doubling backoff, so a permanently stuck step still gets swept. */
160
- const REDISPATCH_BACKOFF_MAX_MS = 10 * 60_000;
161
- /** Bound on the in-process backoff map, so a long-lived process cannot grow it without bound. */
162
- const REDISPATCH_BACKOFF_MAX_ENTRIES = 10_000;
163
- const WORKFLOW_POLL_MIN_MS = 10;
164
- const WORKFLOW_POLL_FACTOR = 1.6;
165
- const WORKFLOW_CHILD_POLL_MAX_MS = 500;
11
+ import { DEFAULT_STEP_RETRIES, WORKFLOW_CHILD_POLL_MAX_MS, WORKFLOW_END_STATES, WORKFLOW_POLL_FACTOR, WORKFLOW_POLL_MIN_MS, WORKFLOW_TERMINAL_STATES, } from './workflow-constants.js';
12
+ import { WorkflowAsyncException, WorkflowCancelledException, WorkflowDispatchException, WorkflowNotFoundError, WorkflowRunCancelledError, WorkflowRunFailedError, WorkflowRunNotFoundError, WorkflowStepFunctionMismatchError, WorkflowStepNameNotString, WorkflowSuspendedException, } from './workflow-errors.js';
13
+ import { resolveWorkflowMeta } from './workflow-meta-resolver.js';
14
+ import { jobGroupFor, orchestratorQueueName, resolveWorkflowConfig, stepJobOptions, stepWorkerQueueName, } from './workflow-queue-routing.js';
15
+ import { wireWorkflowQueueWorkers } from './workflow-queue-wiring.js';
16
+ import { approvalStepNameFor, evaluateApprovalStep, recordApprovalDecision, } from './workflow-approval.js';
17
+ import { recordSuspension, suspendStepNameFor } from './workflow-suspend.js';
18
+ import { RedispatchBackoff, sweepStalledRuns, sweepUndispatchedSteps, } from './workflow-recovery.js';
166
19
  export class PikkuWorkflowService {
167
20
  runExtension;
168
21
  runContexts = new Map();
@@ -236,76 +89,12 @@ export class PikkuWorkflowService {
236
89
  return written;
237
90
  }
238
91
  wireQueueWorkers() {
239
- const functions = pikkuState(null, 'function', 'functions');
240
- const functionsMeta = pikkuState(null, 'function', 'meta');
241
- const mkMeta = (funcId) => ({
242
- pikkuFuncId: funcId,
243
- sessionless: true,
244
- functionType: 'helper',
245
- inputSchemaName: null,
246
- outputSchemaName: null,
92
+ wireWorkflowQueueWorkers({
93
+ strategy: this.queueStrategy,
94
+ concurrency: this.queueConcurrency,
95
+ groupConcurrency: this.queueGroupConcurrency,
96
+ getLogger: () => this.logger,
247
97
  });
248
- const queueMeta = pikkuState(null, 'queue', 'meta');
249
- const registerWorkflowFunc = (funcId, func, queueName, config) => {
250
- if (functions.has(funcId))
251
- return;
252
- addFunction(funcId, func);
253
- if (!queueMeta[queueName]) {
254
- queueMeta[queueName] = { pikkuFuncId: funcId, name: queueName };
255
- }
256
- wireQueueWorker({ name: queueName, func, config });
257
- if (!functionsMeta[funcId]) {
258
- functionsMeta[funcId] = mkMeta(funcId);
259
- }
260
- };
261
- const sharedGroups = this.queueStrategy === 'shared-groups';
262
- const sharedQueueConfig = sharedGroups
263
- ? {
264
- batchSize: this.queueConcurrency,
265
- groupConcurrency: this.queueGroupConcurrency,
266
- }
267
- : undefined;
268
- registerWorkflowFunc('pikkuWorkflowOrchestrator', { func: pikkuWorkflowOrchestratorFunc }, 'pikku-workflow-orchestrator', sharedQueueConfig);
269
- registerWorkflowFunc('pikkuWorkflowStepWorker', { func: pikkuWorkflowWorkerFunc }, 'pikku-workflow-step-worker', sharedQueueConfig);
270
- const registerQueueWorkers = (queueMeta) => {
271
- for (const [queueName, meta] of Object.entries(queueMeta)) {
272
- if (functions.has(meta.pikkuFuncId))
273
- continue;
274
- if (queueName.startsWith('wf-orchestrator-')) {
275
- registerWorkflowFunc(meta.pikkuFuncId, { func: pikkuWorkflowOrchestratorFunc }, queueName);
276
- }
277
- else if (queueName.startsWith('wf-step-')) {
278
- registerWorkflowFunc(meta.pikkuFuncId, { func: pikkuWorkflowWorkerFunc }, queueName);
279
- }
280
- }
281
- };
282
- if (!sharedGroups) {
283
- registerQueueWorkers(pikkuState(null, 'queue', 'meta'));
284
- const addons = pikkuState(null, 'addons', 'packages');
285
- if (addons) {
286
- for (const [, addon] of addons) {
287
- const addonQueueMeta = pikkuState(addon.package, 'queue', 'meta');
288
- if (addonQueueMeta) {
289
- registerQueueWorkers(addonQueueMeta);
290
- }
291
- }
292
- }
293
- }
294
- const workflowCount = Object.keys(pikkuState(null, 'workflows', 'meta') ?? {}).length;
295
- const perWorkflowQueues = Object.keys(queueMeta).filter((name) => name.startsWith('wf-orchestrator-')).length;
296
- if (workflowCount > 0 && perWorkflowQueues === 0) {
297
- this.logger?.warn?.(`[pikku] ${workflowCount} workflow(s) registered but no per-workflow orchestrator queues were found in queue meta. ` +
298
- `All workflows will share a single orchestrator queue, where one slow step blocks every other workflow behind it. ` +
299
- `Check that the generated bootstrap imports the queue-workers meta (pikku-queue-workers-wirings-meta.gen.js).`);
300
- }
301
- if (!functions.has('pikkuWorkflowSleeper')) {
302
- addFunction('pikkuWorkflowSleeper', {
303
- func: pikkuWorkflowSleeperFunc,
304
- });
305
- }
306
- if (!functionsMeta.pikkuWorkflowSleeper) {
307
- functionsMeta.pikkuWorkflowSleeper = mkMeta('pikkuWorkflowSleeper');
308
- }
309
98
  }
310
99
  async isInline(runId) {
311
100
  const context = this.runContexts.get(runId);
@@ -465,41 +254,6 @@ export class PikkuWorkflowService {
465
254
  async findStalledRunIds(_before, _limit) {
466
255
  return [];
467
256
  }
468
- /**
469
- * Re-drive runs whose next move was lost, and report which were resumed.
470
- *
471
- * Arming a step is two writes to two systems — the step row, then the queue
472
- * or scheduler job — so a process that dies between them leaves a run that is
473
- * `running` with nothing in flight. Nothing notices: the run parks on a step
474
- * that will never complete and never error, so it neither finishes nor fails.
475
- * (Seen on a `workflow.sleep()`: a deploy restart landed between the sleep
476
- * step's insert and its timer, parking the run permanently.)
477
- *
478
- * Replay is the recovery — `resumeWorkflow` re-orchestrates from persisted
479
- * step state, and every settled step is memoized, so resuming a run that was
480
- * not actually stuck costs an orchestration pass and changes nothing. That
481
- * idempotence is what makes an idle-time heuristic safe here; a run that is
482
- * legitimately mid-sleep is excluded anyway, since its step is `scheduled`.
483
- *
484
- * This is not self-starting. Call it from a scheduled task at whatever
485
- * interval suits the workload.
486
- */
487
- async recoverStalledRuns(options) {
488
- const before = new Date(Date.now() - (options?.stalledAfterMs ?? DEFAULT_STALLED_RUN_MS));
489
- const runIds = await this.findStalledRunIds(before, options?.limit ?? DEFAULT_STALLED_RUN_LIMIT);
490
- const resumed = [];
491
- for (const runId of runIds) {
492
- try {
493
- await this.resumeWorkflow(runId);
494
- resumed.push(runId);
495
- }
496
- catch (err) {
497
- // One unresumable run must not stop the sweep from recovering the rest.
498
- getSingletonServices()?.logger?.error(`Failed to resume stalled workflow run ${runId}: ${err instanceof Error ? err.message : String(err)}`);
499
- }
500
- }
501
- return { resumed };
502
- }
503
257
  /**
504
258
  * Runs holding a step that has sat `pending` since before `before`, paired
505
259
  * with the step that flagged them.
@@ -516,89 +270,30 @@ export class PikkuWorkflowService {
516
270
  async findUndispatchedSteps(_before, _limit) {
517
271
  return [];
518
272
  }
273
+ redispatchBackoff = new RedispatchBackoff();
274
+ get sweepDeps() {
275
+ return {
276
+ resume: (runId) => this.resumeWorkflow(runId),
277
+ logger: getSingletonServices()?.logger,
278
+ };
279
+ }
519
280
  /**
520
- * Re-drive steps whose dispatch was lost, and report which runs were nudged.
521
- *
522
- * Arming a step is two writes to two systems: the step row lands `pending`,
523
- * then a queue or scheduler job is published. Nothing spans both, so a crash
524
- * in between leaves a durable row that nothing will ever pick up — the run
525
- * neither finishes nor fails. (Seen on a `workflow.sleep()`: a deploy restart
526
- * landed between the sleep step's insert and its timer.)
527
- *
528
- * The row is the outbox record and this is the relay. Age is the only signal
529
- * available a step `pending` because its dispatch was lost is
530
- * indistinguishable from one whose job is merely still queued — so a step
531
- * past `undispatchedAfterMs` is re-dispatched regardless, and correctness
532
- * rests on the claim rather than on the guess being right. A redundant
533
- * dispatch costs one queue message: the loser reads `running` and returns
534
- * without invoking anything.
535
- *
536
- * Re-dispatches back off per step (doubling from 30s, capped at 10m) so a
537
- * genuine queue backlog is not amplified by a tick that keeps firing at the
538
- * steps the backlog is already delaying. The backoff is per process and
539
- * advisory — losing it on restart costs extra dispatches, never correctness.
540
- *
541
- * This is not self-starting. Call it from a scheduled task; ~30s suits a
542
- * queue whose `pending`→`running` latency is well under that.
281
+ * Re-drive runs whose next move was lost. Not self-starting call it from a
282
+ * scheduled task at whatever interval suits the workload.
283
+ */
284
+ async recoverStalledRuns(options) {
285
+ return sweepStalledRuns((before, limit) => this.findStalledRunIds(before, limit), options, this.sweepDeps);
286
+ }
287
+ /**
288
+ * Re-drive steps whose dispatch was lost. Not self-starting — call it from a
289
+ * scheduled task; ~30s suits a queue whose `pending`→`running` latency is
290
+ * well under that.
543
291
  */
544
292
  async relayUndispatchedSteps(options) {
545
- const before = new Date(Date.now() -
546
- (options?.undispatchedAfterMs ?? DEFAULT_UNDISPATCHED_STEP_MS));
547
- const steps = await this.findUndispatchedSteps(before, options?.limit ?? DEFAULT_UNDISPATCHED_STEP_LIMIT);
548
- // Backoff is keyed by run, not step, because the run is the unit of
549
- // re-drive: `resumeWorkflow` replays the whole run and re-dispatches every
550
- // step still owed a job. Holding off a single step while resuming its run
551
- // would not suppress anything.
552
- const now = Date.now();
553
- const runIds = new Set();
554
- for (const { runId } of steps) {
555
- const eligibleAt = this.redispatchBackoff.get(runId);
556
- if (eligibleAt !== undefined && eligibleAt > now) {
557
- continue;
558
- }
559
- this.noteRedispatch(runId, now);
560
- runIds.add(runId);
561
- }
562
- const redispatched = [];
563
- for (const runId of runIds) {
564
- try {
565
- await this.resumeWorkflow(runId);
566
- redispatched.push(runId);
567
- }
568
- catch (err) {
569
- // One unresumable run must not stop the tick from relaying the rest.
570
- getSingletonServices()?.logger?.error(`Failed to re-dispatch workflow run ${runId}: ${err instanceof Error ? err.message : String(err)}`);
571
- }
572
- }
573
- return { redispatched };
574
- }
575
- /** Advisory, per-process record of when a run may next be re-dispatched. */
576
- redispatchBackoff = new Map();
577
- redispatchDelays = new Map();
578
- noteRedispatch(runId, now) {
579
- const previous = this.redispatchDelays.get(runId);
580
- const delay = Math.min(previous === undefined ? REDISPATCH_BACKOFF_MS : previous * 2, REDISPATCH_BACKOFF_MAX_MS);
581
- // A run that settles is never returned again, so entries are only evicted by
582
- // this bound — oldest first, which is also least recently re-dispatched.
583
- if (this.redispatchBackoff.size >= REDISPATCH_BACKOFF_MAX_ENTRIES) {
584
- const oldest = this.redispatchBackoff.keys().next();
585
- if (!oldest.done) {
586
- this.redispatchBackoff.delete(oldest.value);
587
- this.redispatchDelays.delete(oldest.value);
588
- }
589
- }
590
- this.redispatchDelays.set(runId, delay);
591
- this.redispatchBackoff.set(runId, now + delay);
293
+ return sweepUndispatchedSteps((before, limit) => this.findUndispatchedSteps(before, limit), this.redispatchBackoff, options, this.sweepDeps);
592
294
  }
593
295
  resolveStepJobOptions(stepOptions) {
594
- const retries = stepOptions?.retries ?? DEFAULT_STEP_RETRIES;
595
- const retryDelay = stepOptions?.retryDelay;
596
- const backoff = retryDelay !== undefined && retryDelay !== 'exponential'
597
- ? { type: 'fixed', delay: getDurationInMilliseconds(retryDelay) }
598
- : retries > 0 || retryDelay === 'exponential'
599
- ? 'exponential'
600
- : undefined;
601
- return { attempts: retries + 1, ...(backoff ? { backoff } : {}) };
296
+ return stepJobOptions(stepOptions);
602
297
  }
603
298
  async queueStepWorker(runId, stepName, rpcName, data, stepOptions, fromStepName) {
604
299
  const queueService = this.verifyQueueService();
@@ -859,6 +554,9 @@ export class PikkuWorkflowService {
859
554
  return;
860
555
  }
861
556
  const registrations = pikkuState(pkgName, 'workflows', 'registrations');
557
+ if (!workflowMeta) {
558
+ throw new PikkuMissingMetaError(`Missing generated metadata for workflow '${run.workflow}'`);
559
+ }
862
560
  const workflow = registrations.get(resolved?.resolvedName ?? run.workflow);
863
561
  if (!workflow) {
864
562
  throw new WorkflowNotFoundError(run.workflow);
@@ -869,11 +567,10 @@ export class PikkuWorkflowService {
869
567
  : null;
870
568
  const workflowWire = this.createWorkflowWire(run.workflow, runId, rpcService, addonNs);
871
569
  workflowWire.pikkuUserId = run.wire?.pikkuUserId;
570
+ // No `rpc` yet — runPikkuFunc attaches it lazily for the invocation.
872
571
  const wire = {
873
572
  workflow: workflowWire,
874
573
  pikkuUserId: run.wire?.pikkuUserId,
875
- session: rpcService?.wire?.session,
876
- rpc: rpcService?.wire?.rpc,
877
574
  };
878
575
  this.runExtension?.decorateRunWire(wire, {
879
576
  runId,
@@ -990,6 +687,11 @@ export class PikkuWorkflowService {
990
687
  async executeWorkflowStepInner(runId, stepName, rpcName, data, rpcService) {
991
688
  const claimed = await this.withStepLock(runId, stepName, async () => {
992
689
  const stepState = await this.getStepState(runId, stepName);
690
+ // knowledge: decisions/security/a-step-runs-the-function-the-workflow-dispatched-it-with.md
691
+ if (stepState.rpcName !== undefined &&
692
+ stepState.rpcName !== (rpcName ?? null)) {
693
+ throw new WorkflowStepFunctionMismatchError(runId, stepName);
694
+ }
993
695
  if (stepState.status === 'succeeded' || stepState.status === 'running') {
994
696
  return null;
995
697
  }
@@ -1027,7 +729,7 @@ export class PikkuWorkflowService {
1027
729
  }
1028
730
  }
1029
731
  if (graphNodeId) {
1030
- result = await executeGraphStep(this, rpcService, runId, stepState.stepId, graphNodeId, rpcName, data, run.workflow);
732
+ result = await executeGraphStep(this, rpcService, runId, stepState.stepId, graphNodeId, workflowMeta.nodes[graphNodeId].rpcName, data, run.workflow);
1031
733
  }
1032
734
  else {
1033
735
  const subWorkflowMeta = meta[rpcName];
@@ -1037,7 +739,7 @@ export class PikkuWorkflowService {
1037
739
  id: rpcName,
1038
740
  parentRunId: runId,
1039
741
  parentStepId: stepState.stepId,
1040
- pikkuUserId: rpcService.wire?.pikkuUserId,
742
+ pikkuUserId: run.wire?.pikkuUserId,
1041
743
  };
1042
744
  const shouldInline = !getSingletonServices()?.queueService;
1043
745
  const { runId: childRunId } = await this.startWorkflow(rpcName, data, childWire, rpcService, { inline: shouldInline });
@@ -1202,7 +904,7 @@ export class PikkuWorkflowService {
1202
904
  type: 'workflow',
1203
905
  id: rpcName,
1204
906
  parentRunId: runId,
1205
- pikkuUserId: rpcService.wire?.pikkuUserId,
907
+ pikkuUserId: (await this.getRunIdentity(runId))?.wire?.pikkuUserId,
1206
908
  };
1207
909
  const { runId: childRunId } = await this.startWorkflow(rpcName, data, childWire, rpcService, { inline: true });
1208
910
  await this.setStepChildRunId(currentStepState.stepId, childRunId);
@@ -1281,39 +983,10 @@ export class PikkuWorkflowService {
1281
983
  await new Promise((resolve) => setTimeout(resolve, getDurationInMilliseconds(duration)));
1282
984
  await this.setStepResult(stepState.stepId, null);
1283
985
  }
1284
- getSuspendStepName(reason) {
1285
- return `__workflow_suspend:${reason}`;
1286
- }
1287
986
  async suspendStep(runId, reason) {
1288
987
  const fromStepName = this.lastStepName(runId);
1289
- const suspendStepName = this.nextStepKey(runId, this.getSuspendStepName(reason));
1290
- await this.withStepLock(runId, suspendStepName, async () => {
1291
- let stepState;
1292
- try {
1293
- stepState = await this.getStepState(runId, suspendStepName);
1294
- }
1295
- catch {
1296
- stepState = await this.insertStepState(runId, suspendStepName, 'pikkuWorkflowSuspend', {
1297
- reason,
1298
- }, undefined, fromStepName);
1299
- }
1300
- if (!stepState.stepId) {
1301
- stepState = await this.insertStepState(runId, suspendStepName, 'pikkuWorkflowSuspend', {
1302
- reason,
1303
- }, undefined, fromStepName);
1304
- }
1305
- if (stepState.status === 'succeeded') {
1306
- return;
1307
- }
1308
- if (stepState.status === 'pending') {
1309
- await this.setStepRunning(stepState.stepId);
1310
- }
1311
- await this.setStepResult(stepState.stepId, {
1312
- reason,
1313
- suspendedAt: new Date().toISOString(),
1314
- });
1315
- throw new WorkflowSuspendedException(runId, reason);
1316
- });
988
+ const suspendStepName = this.nextStepKey(runId, suspendStepNameFor(reason));
989
+ await this.withStepLock(runId, suspendStepName, () => recordSuspension(this.approvalStore, runId, reason, suspendStepName, fromStepName));
1317
990
  }
1318
991
  async scheduleRunWake(runId, delay) {
1319
992
  try {
@@ -1331,96 +1004,26 @@ export class PikkuWorkflowService {
1331
1004
  this.logger?.warn(`Failed to schedule approval expiry wake for run ${runId}; expiry will still resolve on the next replay`, error);
1332
1005
  }
1333
1006
  }
1334
- getApprovalStepName(reason) {
1335
- return `__workflow_approval:${reason}`;
1336
- }
1337
- approvalStateKey(stepName) {
1338
- let hex = '';
1339
- for (const byte of new TextEncoder().encode(stepName)) {
1340
- hex += byte.toString(16).padStart(2, '0');
1341
- }
1342
- return `__approval_${hex}`;
1007
+ get approvalStore() {
1008
+ return {
1009
+ getStepState: (runId, stepName) => this.getStepState(runId, stepName),
1010
+ insertStepState: (runId, stepName, rpcName, input, output, fromStepName) => this.insertStepState(runId, stepName, rpcName, input, output, fromStepName),
1011
+ setStepRunning: (stepId) => this.setStepRunning(stepId),
1012
+ setStepResult: (stepId, result) => this.setStepResult(stepId, result),
1013
+ getRunState: (runId) => this.getRunState(runId),
1014
+ updateRunState: (runId, key, value) => this.updateRunState(runId, key, value),
1015
+ resumeWorkflow: (runId) => this.resumeWorkflow(runId),
1016
+ scheduleRunWake: (runId, delay) => this.scheduleRunWake(runId, delay),
1017
+ };
1343
1018
  }
1344
- async approveStep(runId, reason, decision) {
1345
- const stepName = this.getApprovalStepName(reason);
1346
- const stateKey = this.approvalStateKey(stepName);
1347
- let resolved;
1348
- try {
1349
- resolved = await this.getStepState(runId, stepName);
1350
- }
1351
- catch {
1352
- // knowledge: decisions/security/workflow-approval-payloads-are-validated-on-replay-inside-the-workflow.md
1353
- }
1354
- if (resolved?.stepId && resolved.status === 'succeeded') {
1355
- const outcome = resolved.result;
1356
- throw new WorkflowApprovalResolvedError(reason, outcome?.status ?? 'decided');
1357
- }
1358
- const state = await this.getRunState(runId);
1359
- const record = (state[stateKey] ?? {});
1360
- await this.updateRunState(runId, stateKey, {
1361
- ...record,
1362
- decision,
1363
- decidedAt: new Date().toISOString(),
1364
- error: undefined,
1365
- });
1366
- await this.resumeWorkflow(runId);
1019
+ async approveStep(runId, reason, decision, session) {
1020
+ assertWorkflowRunOwner((await this.getRunIdentity(runId))?.wire, session);
1021
+ return recordApprovalDecision(this.approvalStore, runId, reason, decision);
1367
1022
  }
1368
1023
  async approvalStep(runId, reason, options) {
1369
1024
  const fromStepName = this.lastStepName(runId);
1370
- const approvalStepName = this.nextStepKey(runId, this.getApprovalStepName(reason));
1371
- return await this.withStepLock(runId, approvalStepName, async () => {
1372
- let stepState;
1373
- try {
1374
- stepState = await this.getStepState(runId, approvalStepName);
1375
- }
1376
- catch {
1377
- stepState = await this.insertStepState(runId, approvalStepName, 'pikkuWorkflowApproval', { reason, expiry: options.expiry }, undefined, fromStepName);
1378
- }
1379
- if (!stepState.stepId) {
1380
- stepState = await this.insertStepState(runId, approvalStepName, 'pikkuWorkflowApproval', { reason, expiry: options.expiry }, undefined, fromStepName);
1381
- }
1382
- if (stepState.status === 'succeeded') {
1383
- return stepState.result;
1384
- }
1385
- const stateKey = this.approvalStateKey(approvalStepName);
1386
- let record = ((await this.getRunState(runId))[stateKey] ?? {});
1387
- if (stepState.status === 'pending') {
1388
- await this.setStepRunning(stepState.stepId);
1389
- if (options.expiry !== undefined && !record.expiresAt) {
1390
- const expiresAt = new Date(Date.now() + getDurationInMilliseconds(options.expiry)).toISOString();
1391
- record = { ...record, expiresAt };
1392
- await this.updateRunState(runId, stateKey, record);
1393
- await this.scheduleRunWake(runId, getDurationInMilliseconds(options.expiry));
1394
- }
1395
- }
1396
- if (record.decision !== undefined) {
1397
- const validation = await options.schema['~standard'].validate(record.decision);
1398
- if (validation.issues) {
1399
- await this.updateRunState(runId, stateKey, {
1400
- ...record,
1401
- decision: undefined,
1402
- decidedAt: undefined,
1403
- error: validation.issues.map((issue) => ({
1404
- message: issue.message,
1405
- path: issue.path?.map((segment) => typeof segment === 'object' ? segment.key : segment),
1406
- })),
1407
- });
1408
- throw new WorkflowSuspendedException(runId, reason);
1409
- }
1410
- const outcome = {
1411
- status: 'decided',
1412
- data: validation.value,
1413
- };
1414
- await this.setStepResult(stepState.stepId, outcome);
1415
- return outcome;
1416
- }
1417
- if (record.expiresAt && Date.now() >= Date.parse(record.expiresAt)) {
1418
- const outcome = { status: 'expired' };
1419
- await this.setStepResult(stepState.stepId, outcome);
1420
- return outcome;
1421
- }
1422
- throw new WorkflowSuspendedException(runId, reason);
1423
- });
1025
+ const approvalStepName = this.nextStepKey(runId, approvalStepNameFor(reason));
1026
+ return await this.withStepLock(runId, approvalStepName, () => evaluateApprovalStep(this.approvalStore, runId, reason, approvalStepName, fromStepName, options));
1424
1027
  }
1425
1028
  createWorkflowWire(name, runId, rpcService, addonNamespace) {
1426
1029
  const workflowWire = {
@@ -1466,40 +1069,15 @@ export class PikkuWorkflowService {
1466
1069
  }
1467
1070
  }
1468
1071
  getConfig() {
1469
- const singletonServices = getSingletonServices();
1470
- const workflow = singletonServices.config?.workflow;
1471
- return {
1472
- retries: workflow?.retries ?? DEFAULT_STEP_RETRIES,
1473
- retryDelay: workflow?.retryDelay ?? 0,
1474
- orchestratorQueueName: workflow?.orchestratorQueueName ?? 'pikku-workflow-orchestrator',
1475
- stepWorkerQueueName: workflow?.stepWorkerQueueName ?? 'pikku-workflow-step-worker',
1476
- sleeperRPCName: workflow?.sleeperRPCName ?? 'pikkuWorkflowSleeper',
1477
- };
1072
+ return resolveWorkflowConfig();
1478
1073
  }
1479
1074
  getOrchestratorQueueName(workflowName) {
1480
- if (workflowName && this.queueStrategy !== 'shared-groups') {
1481
- const perWorkflow = `wf-orchestrator-${toKebab(workflowName)}`;
1482
- const meta = pikkuState(null, 'queue', 'meta');
1483
- if (meta[perWorkflow]) {
1484
- return perWorkflow;
1485
- }
1486
- }
1487
- return this.getConfig().orchestratorQueueName;
1075
+ return orchestratorQueueName(this.queueStrategy, workflowName);
1488
1076
  }
1489
1077
  getStepWorkerQueueName(rpcName) {
1490
- if (rpcName && this.queueStrategy !== 'shared-groups') {
1491
- const perStep = `wf-step-${toKebab(rpcName)}`;
1492
- const meta = pikkuState(null, 'queue', 'meta');
1493
- if (meta[perStep]) {
1494
- return perStep;
1495
- }
1496
- }
1497
- return this.getConfig().stepWorkerQueueName;
1078
+ return stepWorkerQueueName(this.queueStrategy, rpcName);
1498
1079
  }
1499
1080
  getJobGroup(id) {
1500
- if (!id || this.queueStrategy !== 'shared-groups') {
1501
- return undefined;
1502
- }
1503
- return { id, tier: id };
1081
+ return jobGroupFor(this.queueStrategy, id);
1504
1082
  }
1505
1083
  }