@pikku/core 0.12.78 → 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 (341) hide show
  1. package/CHANGELOG.md +234 -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.d.ts +10 -0
  24. package/dist/services/in-memory-workflow-service.js +25 -0
  25. package/dist/services/local-content-request-handler.d.ts +21 -0
  26. package/dist/services/local-content-request-handler.js +72 -53
  27. package/dist/services/local-content.d.ts +6 -0
  28. package/dist/services/local-content.js +14 -1
  29. package/dist/services/workflow-service.d.ts +4 -2
  30. package/dist/testing/service-tests/agent-run-service-tests.d.ts +10 -0
  31. package/dist/testing/service-tests/agent-run-service-tests.js +72 -0
  32. package/dist/testing/service-tests/ai-storage-service-tests.d.ts +3 -0
  33. package/dist/testing/service-tests/ai-storage-service-tests.js +226 -0
  34. package/dist/testing/service-tests/channel-store-tests.d.ts +3 -0
  35. package/dist/testing/service-tests/channel-store-tests.js +72 -0
  36. package/dist/testing/service-tests/credential-service-tests.d.ts +3 -0
  37. package/dist/testing/service-tests/credential-service-tests.js +109 -0
  38. package/dist/testing/service-tests/deployment-service-tests.d.ts +3 -0
  39. package/dist/testing/service-tests/deployment-service-tests.js +21 -0
  40. package/dist/testing/service-tests/event-hub-store-tests.d.ts +3 -0
  41. package/dist/testing/service-tests/event-hub-store-tests.js +34 -0
  42. package/dist/testing/service-tests/secret-service-tests.d.ts +3 -0
  43. package/dist/testing/service-tests/secret-service-tests.js +80 -0
  44. package/dist/testing/service-tests/session-store-tests.d.ts +3 -0
  45. package/dist/testing/service-tests/session-store-tests.js +43 -0
  46. package/dist/testing/service-tests/workflow-run-service-tests.d.ts +3 -0
  47. package/dist/testing/service-tests/workflow-run-service-tests.js +42 -0
  48. package/dist/testing/service-tests/workflow-service-tests.d.ts +3 -0
  49. package/dist/testing/service-tests/workflow-service-tests.js +150 -0
  50. package/dist/testing/service-tests.d.ts +6 -0
  51. package/dist/testing/service-tests.js +26 -791
  52. package/dist/types/core.types.d.ts +11 -1
  53. package/dist/types/state.types.d.ts +1 -1
  54. package/dist/utils/node-host-resolver.d.ts +12 -0
  55. package/dist/utils/node-host-resolver.js +16 -0
  56. package/dist/utils/safe-fetch.d.ts +18 -0
  57. package/dist/utils/safe-fetch.js +167 -29
  58. package/dist/wirings/actor-flow/run-conversation.js +1 -6
  59. package/dist/wirings/ai-agent/agent-rpc.d.ts +15 -0
  60. package/dist/wirings/ai-agent/agent-rpc.js +53 -0
  61. package/dist/wirings/ai-agent/ai-agent-agui.js +1 -5
  62. package/dist/wirings/ai-agent/ai-agent-memory.js +3 -1
  63. package/dist/wirings/ai-agent/ai-agent-prepare.js +3 -9
  64. package/dist/wirings/ai-agent/ai-agent-runner.js +28 -107
  65. package/dist/wirings/ai-agent/ai-agent-stream.js +20 -61
  66. package/dist/wirings/ai-agent/ai-agent-turn.d.ts +56 -0
  67. package/dist/wirings/ai-agent/ai-agent-turn.js +81 -0
  68. package/dist/wirings/ai-agent/ai-agent.types.d.ts +1 -1
  69. package/dist/wirings/ai-agent/voice-input.js +1 -6
  70. package/dist/wirings/ai-agent/voice-output.js +2 -12
  71. package/dist/wirings/channel/channel-common.js +1 -0
  72. package/dist/wirings/channel/channel-handler.js +3 -5
  73. package/dist/wirings/channel/channel-rpc-service.d.ts +0 -6
  74. package/dist/wirings/channel/channel-rpc-service.js +0 -8
  75. package/dist/wirings/channel/channel-rpc.types.d.ts +6 -0
  76. package/dist/wirings/channel/channel-rpc.types.js +8 -0
  77. package/dist/wirings/channel/channel-runner.d.ts +1 -3
  78. package/dist/wirings/channel/channel-runner.js +16 -8
  79. package/dist/wirings/channel/channel.types.d.ts +2 -0
  80. package/dist/wirings/channel/pikku-abstract-channel-handler.js +1 -0
  81. package/dist/wirings/channel/serverless/serverless-channel-runner.js +3 -0
  82. package/dist/wirings/cli/channel/cli-channel-runner.js +2 -0
  83. package/dist/wirings/cli/cli-runner.js +4 -2
  84. package/dist/wirings/cli/cli.types.d.ts +0 -8
  85. package/dist/wirings/cli/command-parser.js +13 -0
  86. package/dist/wirings/gateway/gateway-runner.js +41 -5
  87. package/dist/wirings/http/http-routes.js +2 -0
  88. package/dist/wirings/http/http-runner.d.ts +0 -10
  89. package/dist/wirings/http/http-runner.js +6 -13
  90. package/dist/wirings/http/http.types.d.ts +0 -10
  91. package/dist/wirings/http/index.d.ts +1 -1
  92. package/dist/wirings/http/index.js +1 -1
  93. package/dist/wirings/mcp/mcp-runner.d.ts +0 -7
  94. package/dist/wirings/mcp/mcp-runner.js +0 -6
  95. package/dist/wirings/rpc/rpc-runner.d.ts +8 -51
  96. package/dist/wirings/rpc/rpc-runner.js +53 -86
  97. package/dist/wirings/rpc/rpc-types.d.ts +3 -0
  98. package/dist/wirings/secret/validate-secret-definitions.js +2 -0
  99. package/dist/wirings/trigger/pikku-trigger-service.d.ts +0 -4
  100. package/dist/wirings/trigger/trigger-runner.js +1 -0
  101. package/dist/wirings/virtual-user/run-virtual-user.js +11 -11
  102. package/dist/wirings/virtual-user/virtual-user-derive.js +4 -25
  103. package/dist/wirings/virtual-user/virtual-user-dispositions.js +1 -4
  104. package/dist/wirings/workflow/dsl/workflow-dsl.types.d.ts +29 -2
  105. package/dist/wirings/workflow/feature.js +7 -4
  106. package/dist/wirings/workflow/graph/graph-runner.d.ts +1 -2
  107. package/dist/wirings/workflow/graph/graph-runner.js +8 -7
  108. package/dist/wirings/workflow/graph/workflow-graph.types.d.ts +0 -4
  109. package/dist/wirings/workflow/index.d.ts +7 -2
  110. package/dist/wirings/workflow/index.js +5 -1
  111. package/dist/wirings/workflow/pikku-scenario-service.d.ts +17 -3
  112. package/dist/wirings/workflow/pikku-scenario-service.js +38 -10
  113. package/dist/wirings/workflow/pikku-workflow-service.d.ts +44 -111
  114. package/dist/wirings/workflow/pikku-workflow-service.js +86 -404
  115. package/dist/wirings/workflow/workflow-approval.d.ts +39 -0
  116. package/dist/wirings/workflow/workflow-approval.js +114 -0
  117. package/dist/wirings/workflow/workflow-constants.d.ts +26 -0
  118. package/dist/wirings/workflow/workflow-constants.js +35 -0
  119. package/dist/wirings/workflow/workflow-errors.d.ts +58 -0
  120. package/dist/wirings/workflow/workflow-errors.js +112 -0
  121. package/dist/wirings/workflow/workflow-meta-resolver.d.ts +11 -0
  122. package/dist/wirings/workflow/workflow-meta-resolver.js +31 -0
  123. package/dist/wirings/workflow/workflow-queue-routing.d.ts +8 -0
  124. package/dist/wirings/workflow/workflow-queue-routing.js +38 -0
  125. package/dist/wirings/workflow/workflow-queue-wiring.d.ts +20 -0
  126. package/dist/wirings/workflow/workflow-queue-wiring.js +79 -0
  127. package/dist/wirings/workflow/workflow-recovery.d.ts +68 -0
  128. package/dist/wirings/workflow/workflow-recovery.js +101 -0
  129. package/dist/wirings/workflow/workflow-run-engine.types.d.ts +54 -0
  130. package/dist/wirings/workflow/workflow-run-engine.types.js +1 -0
  131. package/dist/wirings/workflow/workflow-run-ownership.d.ts +16 -0
  132. package/dist/wirings/workflow/workflow-run-ownership.js +29 -0
  133. package/dist/wirings/workflow/workflow-suspend.d.ts +12 -0
  134. package/dist/wirings/workflow/workflow-suspend.js +33 -0
  135. package/dist/wirings/workflow/workflow.types.d.ts +7 -0
  136. package/knowledge/decisions/internals/a-non-streaming-agent-run-registers-with-airunstate-too.md +22 -0
  137. package/knowledge/decisions/internals/a-resumed-agent-turn-is-as-interruptible-as-the-first.md +20 -0
  138. package/knowledge/decisions/internals/a-scenario-step-template-is-offered-unfilled.md +21 -0
  139. package/knowledge/decisions/internals/a-virtual-user-decides-whether-to-trust-memory-once-per-turn.md +21 -0
  140. package/knowledge/decisions/internals/a-wall-clock-threshold-is-a-load-test-in-disguise.md +46 -0
  141. package/knowledge/decisions/internals/a-workflow-wire-is-built-from-the-run-not-from-the-rpc-service.md +45 -0
  142. package/knowledge/decisions/internals/agent-context-waits-for-a-tool-result-still-being-written.md +21 -0
  143. package/knowledge/decisions/internals/agent-speech-travels-as-a-custom-agui-event.md +22 -0
  144. package/knowledge/decisions/internals/an-agent-interrupt-is-not-a-failure.md +23 -0
  145. package/knowledge/decisions/internals/an-agent-run-owned-by-another-instance-says-so.md +25 -0
  146. package/knowledge/decisions/internals/an-agent-stream-send-must-return-the-inner-sends-promise.md +22 -0
  147. package/knowledge/decisions/internals/an-empty-text-part-is-omitted-from-an-agent-message.md +20 -0
  148. package/knowledge/decisions/internals/an-empty-transcript-is-not-recorded.md +22 -0
  149. package/knowledge/decisions/internals/an-unref-d-timer-cannot-be-awaited-under-node-test.md +66 -0
  150. package/knowledge/decisions/internals/channel-state-accessors-are-unsound-generics-that-every-implementation-asserts.md +33 -0
  151. package/knowledge/decisions/internals/gateway-listener-middleware-runs-without-an-rpc-on-the-wire.md +43 -0
  152. package/knowledge/decisions/internals/hot-reload-writes-into-the-function-map-captured-at-startup.md +26 -0
  153. package/knowledge/decisions/internals/index.md +38 -26
  154. package/knowledge/decisions/internals/only-exposed-functions-enter-a-virtual-user-catalogue.md +21 -0
  155. package/knowledge/decisions/internals/scenario-given-and-when-are-sugar-but-then-is-not.md +24 -0
  156. package/knowledge/decisions/internals/side-effects-are-an-allowlist-not-a-boolean.md +34 -0
  157. package/knowledge/decisions/internals/speech-synthesis-picks-a-voice-per-sentence-and-warns-once.md +24 -0
  158. package/knowledge/decisions/internals/the-actor-prompt-says-json-because-of-json-object-mode.md +22 -0
  159. package/knowledge/decisions/internals/the-agent-done-event-goes-through-the-middleware-and-is-awaited.md +26 -0
  160. package/knowledge/decisions/internals/the-api-report-pins-members-not-just-names.md +43 -0
  161. package/knowledge/decisions/internals/the-ecosystem-entry-point-carries-the-adapter-surface.md +58 -0
  162. package/knowledge/decisions/internals/the-middleware-resolution-cache-is-deliberately-unbounded.md +40 -0
  163. package/knowledge/decisions/internals/the-per-invocation-rpc-view-is-a-class.md +40 -0
  164. package/knowledge/decisions/internals/the-persona-runtime-is-exported-from-the-persona-entry-point.md +28 -0
  165. package/knowledge/decisions/internals/the-transcript-event-is-sent-ahead-of-the-run.md +25 -0
  166. package/knowledge/decisions/internals/the-virtual-user-catalogue-is-the-only-gate-on-what-may-be-called.md +21 -0
  167. package/knowledge/decisions/internals/the-worker-disposition-is-the-one-that-is-not-testing.md +22 -0
  168. package/knowledge/decisions/internals/thread-history-records-the-transcript-not-the-audio.md +25 -0
  169. package/knowledge/decisions/internals/virtual-user-step-order-comes-from-insertion-order.md +21 -0
  170. package/knowledge/decisions/internals/voice-output-speaks-unless-voice-input-explicitly-says-otherwise.md +24 -0
  171. package/knowledge/decisions/internals/wiring-registries-erase-the-generics-their-wire-functions-capture.md +35 -0
  172. package/knowledge/decisions/security/a-dropped-audit-write-is-always-logged.md +4 -2
  173. package/knowledge/decisions/security/a-graph-run-starts-at-an-entry-node-the-graph-declared.md +33 -0
  174. package/knowledge/decisions/security/a-permission-gets-a-wire-it-cannot-reply-on.md +33 -0
  175. package/knowledge/decisions/security/a-step-runs-the-function-the-workflow-dispatched-it-with.md +37 -0
  176. package/knowledge/decisions/security/a-virtual-user-is-never-offered-a-step-that-would-forge-its-own-oracle.md +28 -0
  177. package/knowledge/decisions/security/a-workflow-run-is-read-and-approved-by-its-owner.md +34 -0
  178. package/knowledge/decisions/security/an-agent-approval-is-claimed-before-the-tool-runs.md +33 -0
  179. package/knowledge/decisions/security/an-upload-is-counted-as-it-arrives-not-buffered-then-measured.md +24 -0
  180. package/knowledge/decisions/security/gateway-handlers-run-through-the-function-runner-gate.md +13 -3
  181. package/knowledge/decisions/security/index.md +7 -0
  182. package/knowledge/questions/channel-middleware-accepts-bare-factories-that-nothing-resolves.md +35 -0
  183. package/knowledge/questions/index.md +2 -1
  184. package/knowledge/questions/unauthorized-channel-replies-escape-the-declared-out-type.md +44 -0
  185. package/package.json +14 -3
  186. package/scripts/generate-api-report.d.mts +1 -0
  187. package/scripts/generate-api-report.mjs +89 -0
  188. package/scripts/generate-api-report.mts +218 -0
  189. package/src/api-report.test.ts +69 -0
  190. package/src/column-form.ts +1 -4
  191. package/src/crypto-utils.test.ts +15 -2
  192. package/src/dev/hot-reload.ts +2 -7
  193. package/src/ecosystem.ts +40 -0
  194. package/src/errors/error-handler.ts +5 -3
  195. package/src/errors/error.test.ts +4 -1
  196. package/src/function/function-runner.test.ts +1 -1
  197. package/src/function/function-runner.ts +28 -39
  198. package/src/function/functions.types.ts +15 -9
  199. package/src/handle-error.ts +1 -1
  200. package/src/index.ts +3 -22
  201. package/src/middleware/auth-cookie.test.ts +50 -0
  202. package/src/middleware/auth-cookie.ts +38 -2
  203. package/src/middleware/cors.ts +2 -1
  204. package/src/middleware/index.ts +1 -1
  205. package/src/middleware/remote-auth.test.ts +43 -0
  206. package/src/middleware/remote-auth.ts +17 -3
  207. package/src/middleware-runner.ts +4 -2
  208. package/src/no-any-casts.test.ts +57 -0
  209. package/src/permissions.test.ts +3 -1
  210. package/src/permissions.ts +22 -24
  211. package/src/pikku-state.ts +17 -6
  212. package/src/public-surface.json +578 -0
  213. package/src/public-surface.json.README +25 -0
  214. package/src/public-surface.test.ts +105 -0
  215. package/src/removed-legacy-exports.test.ts +62 -0
  216. package/src/schema.test.ts +78 -0
  217. package/src/schema.ts +36 -1
  218. package/src/services/ai-run-state-service.ts +7 -1
  219. package/src/services/audit-service.ts +2 -2
  220. package/src/services/in-memory-ai-run-state-service.ts +3 -2
  221. package/src/services/in-memory-workflow-service.ts +25 -0
  222. package/src/services/index.ts +1 -4
  223. package/src/services/local-content-request-handler.test.ts +43 -5
  224. package/src/services/local-content-request-handler.ts +103 -74
  225. package/src/services/local-content.ts +15 -1
  226. package/src/services/local-email-service.ts +5 -1
  227. package/src/services/system-role-guard.test.ts +4 -1
  228. package/src/services/workflow-service.ts +9 -2
  229. package/src/side-effects-are-declared.test.ts +84 -0
  230. package/src/source-files-stay-composable.test.ts +41 -0
  231. package/src/testing/service-tests/agent-run-service-tests.ts +98 -0
  232. package/src/testing/service-tests/ai-storage-service-tests.ts +286 -0
  233. package/src/testing/service-tests/channel-store-tests.ts +98 -0
  234. package/src/testing/service-tests/credential-service-tests.ts +143 -0
  235. package/src/testing/service-tests/deployment-service-tests.ts +33 -0
  236. package/src/testing/service-tests/event-hub-store-tests.ts +48 -0
  237. package/src/testing/service-tests/secret-service-tests.ts +105 -0
  238. package/src/testing/service-tests/session-store-tests.ts +62 -0
  239. package/src/testing/service-tests/workflow-run-service-tests.ts +59 -0
  240. package/src/testing/service-tests/workflow-service-tests.ts +308 -0
  241. package/src/testing/service-tests.ts +31 -1111
  242. package/src/types/core.types.ts +15 -1
  243. package/src/types/state.types.ts +1 -1
  244. package/src/utils/node-host-resolver.ts +20 -0
  245. package/src/utils/safe-fetch.test.ts +143 -1
  246. package/src/utils/safe-fetch.ts +200 -25
  247. package/src/wirings/actor-flow/run-conversation.ts +1 -6
  248. package/src/wirings/ai-agent/agent-rpc.ts +120 -0
  249. package/src/wirings/ai-agent/ai-agent-agui.ts +1 -5
  250. package/src/wirings/ai-agent/ai-agent-interrupt.test.ts +2 -1
  251. package/src/wirings/ai-agent/ai-agent-memory.ts +8 -1
  252. package/src/wirings/ai-agent/ai-agent-prepare.ts +10 -12
  253. package/src/wirings/ai-agent/ai-agent-resume-authorization.test.ts +1 -0
  254. package/src/wirings/ai-agent/ai-agent-runner.test.ts +82 -2
  255. package/src/wirings/ai-agent/ai-agent-runner.ts +49 -120
  256. package/src/wirings/ai-agent/ai-agent-stream.test.ts +2 -0
  257. package/src/wirings/ai-agent/ai-agent-stream.ts +36 -63
  258. package/src/wirings/ai-agent/ai-agent-thread-ownership.test.ts +1 -1
  259. package/src/wirings/ai-agent/ai-agent-turn.ts +121 -0
  260. package/src/wirings/ai-agent/ai-agent.types.ts +1 -1
  261. package/src/wirings/ai-agent/voice-input.ts +1 -6
  262. package/src/wirings/ai-agent/voice-output.ts +2 -12
  263. package/src/wirings/channel/channel-common.ts +5 -2
  264. package/src/wirings/channel/channel-handler-shapes.test.ts +72 -0
  265. package/src/wirings/channel/channel-handler.ts +7 -7
  266. package/src/wirings/channel/channel-rpc-service.ts +0 -14
  267. package/src/wirings/channel/channel-rpc.test.ts +25 -4
  268. package/src/wirings/channel/channel-rpc.types.ts +14 -0
  269. package/src/wirings/channel/channel-runner.ts +33 -20
  270. package/src/wirings/channel/channel.types.ts +2 -0
  271. package/src/wirings/channel/local/local-channel-runner.ts +1 -1
  272. package/src/wirings/channel/pikku-abstract-channel-handler.ts +2 -1
  273. package/src/wirings/channel/serverless/serverless-channel-runner.ts +6 -3
  274. package/src/wirings/cli/channel/cli-channel-runner.ts +3 -1
  275. package/src/wirings/cli/channel/cli-raw-client-runner.ts +4 -1
  276. package/src/wirings/cli/cli-runner.ts +7 -4
  277. package/src/wirings/cli/cli.types.ts +0 -39
  278. package/src/wirings/cli/command-parser.test.ts +19 -0
  279. package/src/wirings/cli/command-parser.ts +17 -1
  280. package/src/wirings/gateway/gateway-authorization.test.ts +131 -0
  281. package/src/wirings/gateway/gateway-channel-meta.test.ts +44 -0
  282. package/src/wirings/gateway/gateway-runner.ts +62 -22
  283. package/src/wirings/http/http-routes.ts +4 -1
  284. package/src/wirings/http/http-runner.ts +11 -26
  285. package/src/wirings/http/http.types.ts +0 -15
  286. package/src/wirings/http/index.ts +1 -7
  287. package/src/wirings/http/pikku-fetch-http-request.ts +2 -2
  288. package/src/wirings/http/web-request.ts +1 -1
  289. package/src/wirings/mcp/mcp-runner.ts +2 -16
  290. package/src/wirings/persona/persona-environments.test.ts +14 -3
  291. package/src/wirings/persona/persona.test.ts +13 -3
  292. package/src/wirings/persona/validate-personas.ts +5 -1
  293. package/src/wirings/queue/queue-runner.ts +1 -1
  294. package/src/wirings/rpc/addon-auth-tags.test.ts +1 -5
  295. package/src/wirings/rpc/rpc-runner.ts +58 -136
  296. package/src/wirings/rpc/rpc-types.ts +7 -0
  297. package/src/wirings/rpc/wire-addon.ts +3 -1
  298. package/src/wirings/secret/validate-secret-definitions.test.ts +69 -0
  299. package/src/wirings/secret/validate-secret-definitions.ts +2 -0
  300. package/src/wirings/trigger/pikku-trigger-service.ts +0 -5
  301. package/src/wirings/trigger/trigger-runner.ts +7 -5
  302. package/src/wirings/virtual-user/run-virtual-user.test.ts +18 -9
  303. package/src/wirings/virtual-user/run-virtual-user.ts +28 -15
  304. package/src/wirings/virtual-user/virtual-user-agents.test.ts +4 -1
  305. package/src/wirings/virtual-user/virtual-user-derive.ts +4 -25
  306. package/src/wirings/virtual-user/virtual-user-dispositions.test.ts +7 -2
  307. package/src/wirings/virtual-user/virtual-user-dispositions.ts +1 -4
  308. package/src/wirings/virtual-user/virtual-user-intents.test.ts +13 -3
  309. package/src/wirings/workflow/dsl/workflow-dsl.types.ts +35 -2
  310. package/src/wirings/workflow/feature.ts +7 -4
  311. package/src/wirings/workflow/graph/graph-node.ts +1 -1
  312. package/src/wirings/workflow/graph/graph-runner.test.ts +4 -2
  313. package/src/wirings/workflow/graph/graph-runner.ts +13 -16
  314. package/src/wirings/workflow/graph/workflow-graph.types.ts +0 -5
  315. package/src/wirings/workflow/index.ts +12 -4
  316. package/src/wirings/workflow/pikku-scenario-service.ts +54 -25
  317. package/src/wirings/workflow/pikku-workflow-service.test.ts +2 -2
  318. package/src/wirings/workflow/pikku-workflow-service.ts +208 -630
  319. package/src/wirings/workflow/scenario-hooks.test.ts +51 -0
  320. package/src/wirings/workflow/workflow-approval.ts +185 -0
  321. package/src/wirings/workflow/workflow-child-run-session.test.ts +89 -0
  322. package/src/wirings/workflow/workflow-constants.ts +47 -0
  323. package/src/wirings/workflow/workflow-dispatch-durability.test.ts +1 -1
  324. package/src/wirings/workflow/workflow-dispatch-relay.test.ts +128 -0
  325. package/src/wirings/workflow/workflow-errors.ts +128 -0
  326. package/src/wirings/workflow/workflow-inline-authority.test.ts +12 -6
  327. package/src/wirings/workflow/workflow-meta-resolver.ts +45 -0
  328. package/src/wirings/workflow/workflow-missing-meta.test.ts +92 -0
  329. package/src/wirings/workflow/workflow-queue-routing.ts +85 -0
  330. package/src/wirings/workflow/workflow-queue-wiring.ts +149 -0
  331. package/src/wirings/workflow/workflow-recovery.ts +162 -0
  332. package/src/wirings/workflow/workflow-retry-policy.test.ts +1 -1
  333. package/src/wirings/workflow/workflow-run-authority.test.ts +215 -0
  334. package/src/wirings/workflow/workflow-run-engine.types.ts +91 -0
  335. package/src/wirings/workflow/workflow-run-ownership.ts +36 -0
  336. package/src/wirings/workflow/workflow-suspend.ts +61 -0
  337. package/src/wirings/workflow/workflow.types.ts +7 -0
  338. package/src/wirings-stay-decoupled.test.ts +123 -0
  339. package/tsconfig.json +1 -1
  340. package/tsconfig.tsbuildinfo +1 -1
  341. package/src/internal.ts +0 -10
@@ -1,154 +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
- const WORKFLOW_POLL_MIN_MS = 10;
150
- const WORKFLOW_POLL_FACTOR = 1.6;
151
- 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';
152
19
  export class PikkuWorkflowService {
153
20
  runExtension;
154
21
  runContexts = new Map();
@@ -222,76 +89,12 @@ export class PikkuWorkflowService {
222
89
  return written;
223
90
  }
224
91
  wireQueueWorkers() {
225
- const functions = pikkuState(null, 'function', 'functions');
226
- const functionsMeta = pikkuState(null, 'function', 'meta');
227
- const mkMeta = (funcId) => ({
228
- pikkuFuncId: funcId,
229
- sessionless: true,
230
- functionType: 'helper',
231
- inputSchemaName: null,
232
- outputSchemaName: null,
92
+ wireWorkflowQueueWorkers({
93
+ strategy: this.queueStrategy,
94
+ concurrency: this.queueConcurrency,
95
+ groupConcurrency: this.queueGroupConcurrency,
96
+ getLogger: () => this.logger,
233
97
  });
234
- const queueMeta = pikkuState(null, 'queue', 'meta');
235
- const registerWorkflowFunc = (funcId, func, queueName, config) => {
236
- if (functions.has(funcId))
237
- return;
238
- addFunction(funcId, func);
239
- if (!queueMeta[queueName]) {
240
- queueMeta[queueName] = { pikkuFuncId: funcId, name: queueName };
241
- }
242
- wireQueueWorker({ name: queueName, func, config });
243
- if (!functionsMeta[funcId]) {
244
- functionsMeta[funcId] = mkMeta(funcId);
245
- }
246
- };
247
- const sharedGroups = this.queueStrategy === 'shared-groups';
248
- const sharedQueueConfig = sharedGroups
249
- ? {
250
- batchSize: this.queueConcurrency,
251
- groupConcurrency: this.queueGroupConcurrency,
252
- }
253
- : undefined;
254
- registerWorkflowFunc('pikkuWorkflowOrchestrator', { func: pikkuWorkflowOrchestratorFunc }, 'pikku-workflow-orchestrator', sharedQueueConfig);
255
- registerWorkflowFunc('pikkuWorkflowStepWorker', { func: pikkuWorkflowWorkerFunc }, 'pikku-workflow-step-worker', sharedQueueConfig);
256
- const registerQueueWorkers = (queueMeta) => {
257
- for (const [queueName, meta] of Object.entries(queueMeta)) {
258
- if (functions.has(meta.pikkuFuncId))
259
- continue;
260
- if (queueName.startsWith('wf-orchestrator-')) {
261
- registerWorkflowFunc(meta.pikkuFuncId, { func: pikkuWorkflowOrchestratorFunc }, queueName);
262
- }
263
- else if (queueName.startsWith('wf-step-')) {
264
- registerWorkflowFunc(meta.pikkuFuncId, { func: pikkuWorkflowWorkerFunc }, queueName);
265
- }
266
- }
267
- };
268
- if (!sharedGroups) {
269
- registerQueueWorkers(pikkuState(null, 'queue', 'meta'));
270
- const addons = pikkuState(null, 'addons', 'packages');
271
- if (addons) {
272
- for (const [, addon] of addons) {
273
- const addonQueueMeta = pikkuState(addon.package, 'queue', 'meta');
274
- if (addonQueueMeta) {
275
- registerQueueWorkers(addonQueueMeta);
276
- }
277
- }
278
- }
279
- }
280
- const workflowCount = Object.keys(pikkuState(null, 'workflows', 'meta') ?? {}).length;
281
- const perWorkflowQueues = Object.keys(queueMeta).filter((name) => name.startsWith('wf-orchestrator-')).length;
282
- if (workflowCount > 0 && perWorkflowQueues === 0) {
283
- this.logger?.warn?.(`[pikku] ${workflowCount} workflow(s) registered but no per-workflow orchestrator queues were found in queue meta. ` +
284
- `All workflows will share a single orchestrator queue, where one slow step blocks every other workflow behind it. ` +
285
- `Check that the generated bootstrap imports the queue-workers meta (pikku-queue-workers-wirings-meta.gen.js).`);
286
- }
287
- if (!functions.has('pikkuWorkflowSleeper')) {
288
- addFunction('pikkuWorkflowSleeper', {
289
- func: pikkuWorkflowSleeperFunc,
290
- });
291
- }
292
- if (!functionsMeta.pikkuWorkflowSleeper) {
293
- functionsMeta.pikkuWorkflowSleeper = mkMeta('pikkuWorkflowSleeper');
294
- }
295
98
  }
296
99
  async isInline(runId) {
297
100
  const context = this.runContexts.get(runId);
@@ -452,49 +255,45 @@ export class PikkuWorkflowService {
452
255
  return [];
453
256
  }
454
257
  /**
455
- * Re-drive runs whose next move was lost, and report which were resumed.
456
- *
457
- * Arming a step is two writes to two systems — the step row, then the queue
458
- * or scheduler job — so a process that dies between them leaves a run that is
459
- * `running` with nothing in flight. Nothing notices: the run parks on a step
460
- * that will never complete and never error, so it neither finishes nor fails.
461
- * (Seen on a `workflow.sleep()`: a deploy restart landed between the sleep
462
- * step's insert and its timer, parking the run permanently.)
258
+ * Runs holding a step that has sat `pending` since before `before`, paired
259
+ * with the step that flagged them.
463
260
  *
464
- * Replay is the recovery `resumeWorkflow` re-orchestrates from persisted
465
- * step state, and every settled step is memoized, so resuming a run that was
466
- * not actually stuck costs an orchestration pass and changes nothing. That
467
- * idempotence is what makes an idle-time heuristic safe here; a run that is
468
- * legitimately mid-sleep is excluded anyway, since its step is `scheduled`.
469
- *
470
- * This is not self-starting. Call it from a scheduled task at whatever
471
- * interval suits the workload.
261
+ * Returns nothing by default so a store that cannot express the query keeps
262
+ * working unchanged and, because it does not opt in, gains no re-dispatches
263
+ * either. A store must have an atomic `withStepLock` before overriding this,
264
+ * or no concurrency for one to exclude: the relay makes duplicate dispatch
265
+ * routine, and the claim in `executeWorkflowStepInner` is what keeps a
266
+ * duplicate from becoming a second execution. `kysely-postgres` and
267
+ * `kysely-mysql` qualify on the lock, `in-memory` on being inline and
268
+ * single-process; `mongodb` and `kysely-sqlite` qualify on neither.
269
+ */
270
+ async findUndispatchedSteps(_before, _limit) {
271
+ return [];
272
+ }
273
+ redispatchBackoff = new RedispatchBackoff();
274
+ get sweepDeps() {
275
+ return {
276
+ resume: (runId) => this.resumeWorkflow(runId),
277
+ logger: getSingletonServices()?.logger,
278
+ };
279
+ }
280
+ /**
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.
472
283
  */
473
284
  async recoverStalledRuns(options) {
474
- const before = new Date(Date.now() - (options?.stalledAfterMs ?? DEFAULT_STALLED_RUN_MS));
475
- const runIds = await this.findStalledRunIds(before, options?.limit ?? DEFAULT_STALLED_RUN_LIMIT);
476
- const resumed = [];
477
- for (const runId of runIds) {
478
- try {
479
- await this.resumeWorkflow(runId);
480
- resumed.push(runId);
481
- }
482
- catch (err) {
483
- // One unresumable run must not stop the sweep from recovering the rest.
484
- getSingletonServices()?.logger?.error(`Failed to resume stalled workflow run ${runId}: ${err instanceof Error ? err.message : String(err)}`);
485
- }
486
- }
487
- return { resumed };
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.
291
+ */
292
+ async relayUndispatchedSteps(options) {
293
+ return sweepUndispatchedSteps((before, limit) => this.findUndispatchedSteps(before, limit), this.redispatchBackoff, options, this.sweepDeps);
488
294
  }
489
295
  resolveStepJobOptions(stepOptions) {
490
- const retries = stepOptions?.retries ?? DEFAULT_STEP_RETRIES;
491
- const retryDelay = stepOptions?.retryDelay;
492
- const backoff = retryDelay !== undefined && retryDelay !== 'exponential'
493
- ? { type: 'fixed', delay: getDurationInMilliseconds(retryDelay) }
494
- : retries > 0 || retryDelay === 'exponential'
495
- ? 'exponential'
496
- : undefined;
497
- return { attempts: retries + 1, ...(backoff ? { backoff } : {}) };
296
+ return stepJobOptions(stepOptions);
498
297
  }
499
298
  async queueStepWorker(runId, stepName, rpcName, data, stepOptions, fromStepName) {
500
299
  const queueService = this.verifyQueueService();
@@ -755,6 +554,9 @@ export class PikkuWorkflowService {
755
554
  return;
756
555
  }
757
556
  const registrations = pikkuState(pkgName, 'workflows', 'registrations');
557
+ if (!workflowMeta) {
558
+ throw new PikkuMissingMetaError(`Missing generated metadata for workflow '${run.workflow}'`);
559
+ }
758
560
  const workflow = registrations.get(resolved?.resolvedName ?? run.workflow);
759
561
  if (!workflow) {
760
562
  throw new WorkflowNotFoundError(run.workflow);
@@ -765,11 +567,10 @@ export class PikkuWorkflowService {
765
567
  : null;
766
568
  const workflowWire = this.createWorkflowWire(run.workflow, runId, rpcService, addonNs);
767
569
  workflowWire.pikkuUserId = run.wire?.pikkuUserId;
570
+ // No `rpc` yet — runPikkuFunc attaches it lazily for the invocation.
768
571
  const wire = {
769
572
  workflow: workflowWire,
770
573
  pikkuUserId: run.wire?.pikkuUserId,
771
- session: rpcService?.wire?.session,
772
- rpc: rpcService?.wire?.rpc,
773
574
  };
774
575
  this.runExtension?.decorateRunWire(wire, {
775
576
  runId,
@@ -886,6 +687,11 @@ export class PikkuWorkflowService {
886
687
  async executeWorkflowStepInner(runId, stepName, rpcName, data, rpcService) {
887
688
  const claimed = await this.withStepLock(runId, stepName, async () => {
888
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
+ }
889
695
  if (stepState.status === 'succeeded' || stepState.status === 'running') {
890
696
  return null;
891
697
  }
@@ -923,7 +729,7 @@ export class PikkuWorkflowService {
923
729
  }
924
730
  }
925
731
  if (graphNodeId) {
926
- 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);
927
733
  }
928
734
  else {
929
735
  const subWorkflowMeta = meta[rpcName];
@@ -933,7 +739,7 @@ export class PikkuWorkflowService {
933
739
  id: rpcName,
934
740
  parentRunId: runId,
935
741
  parentStepId: stepState.stepId,
936
- pikkuUserId: rpcService.wire?.pikkuUserId,
742
+ pikkuUserId: run.wire?.pikkuUserId,
937
743
  };
938
744
  const shouldInline = !getSingletonServices()?.queueService;
939
745
  const { runId: childRunId } = await this.startWorkflow(rpcName, data, childWire, rpcService, { inline: shouldInline });
@@ -1098,7 +904,7 @@ export class PikkuWorkflowService {
1098
904
  type: 'workflow',
1099
905
  id: rpcName,
1100
906
  parentRunId: runId,
1101
- pikkuUserId: rpcService.wire?.pikkuUserId,
907
+ pikkuUserId: (await this.getRunIdentity(runId))?.wire?.pikkuUserId,
1102
908
  };
1103
909
  const { runId: childRunId } = await this.startWorkflow(rpcName, data, childWire, rpcService, { inline: true });
1104
910
  await this.setStepChildRunId(currentStepState.stepId, childRunId);
@@ -1177,39 +983,10 @@ export class PikkuWorkflowService {
1177
983
  await new Promise((resolve) => setTimeout(resolve, getDurationInMilliseconds(duration)));
1178
984
  await this.setStepResult(stepState.stepId, null);
1179
985
  }
1180
- getSuspendStepName(reason) {
1181
- return `__workflow_suspend:${reason}`;
1182
- }
1183
986
  async suspendStep(runId, reason) {
1184
987
  const fromStepName = this.lastStepName(runId);
1185
- const suspendStepName = this.nextStepKey(runId, this.getSuspendStepName(reason));
1186
- await this.withStepLock(runId, suspendStepName, async () => {
1187
- let stepState;
1188
- try {
1189
- stepState = await this.getStepState(runId, suspendStepName);
1190
- }
1191
- catch {
1192
- stepState = await this.insertStepState(runId, suspendStepName, 'pikkuWorkflowSuspend', {
1193
- reason,
1194
- }, undefined, fromStepName);
1195
- }
1196
- if (!stepState.stepId) {
1197
- stepState = await this.insertStepState(runId, suspendStepName, 'pikkuWorkflowSuspend', {
1198
- reason,
1199
- }, undefined, fromStepName);
1200
- }
1201
- if (stepState.status === 'succeeded') {
1202
- return;
1203
- }
1204
- if (stepState.status === 'pending') {
1205
- await this.setStepRunning(stepState.stepId);
1206
- }
1207
- await this.setStepResult(stepState.stepId, {
1208
- reason,
1209
- suspendedAt: new Date().toISOString(),
1210
- });
1211
- throw new WorkflowSuspendedException(runId, reason);
1212
- });
988
+ const suspendStepName = this.nextStepKey(runId, suspendStepNameFor(reason));
989
+ await this.withStepLock(runId, suspendStepName, () => recordSuspension(this.approvalStore, runId, reason, suspendStepName, fromStepName));
1213
990
  }
1214
991
  async scheduleRunWake(runId, delay) {
1215
992
  try {
@@ -1227,96 +1004,26 @@ export class PikkuWorkflowService {
1227
1004
  this.logger?.warn(`Failed to schedule approval expiry wake for run ${runId}; expiry will still resolve on the next replay`, error);
1228
1005
  }
1229
1006
  }
1230
- getApprovalStepName(reason) {
1231
- return `__workflow_approval:${reason}`;
1232
- }
1233
- approvalStateKey(stepName) {
1234
- let hex = '';
1235
- for (const byte of new TextEncoder().encode(stepName)) {
1236
- hex += byte.toString(16).padStart(2, '0');
1237
- }
1238
- 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
+ };
1239
1018
  }
1240
- async approveStep(runId, reason, decision) {
1241
- const stepName = this.getApprovalStepName(reason);
1242
- const stateKey = this.approvalStateKey(stepName);
1243
- let resolved;
1244
- try {
1245
- resolved = await this.getStepState(runId, stepName);
1246
- }
1247
- catch {
1248
- // knowledge: decisions/security/workflow-approval-payloads-are-validated-on-replay-inside-the-workflow.md
1249
- }
1250
- if (resolved?.stepId && resolved.status === 'succeeded') {
1251
- const outcome = resolved.result;
1252
- throw new WorkflowApprovalResolvedError(reason, outcome?.status ?? 'decided');
1253
- }
1254
- const state = await this.getRunState(runId);
1255
- const record = (state[stateKey] ?? {});
1256
- await this.updateRunState(runId, stateKey, {
1257
- ...record,
1258
- decision,
1259
- decidedAt: new Date().toISOString(),
1260
- error: undefined,
1261
- });
1262
- 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);
1263
1022
  }
1264
1023
  async approvalStep(runId, reason, options) {
1265
1024
  const fromStepName = this.lastStepName(runId);
1266
- const approvalStepName = this.nextStepKey(runId, this.getApprovalStepName(reason));
1267
- return await this.withStepLock(runId, approvalStepName, async () => {
1268
- let stepState;
1269
- try {
1270
- stepState = await this.getStepState(runId, approvalStepName);
1271
- }
1272
- catch {
1273
- stepState = await this.insertStepState(runId, approvalStepName, 'pikkuWorkflowApproval', { reason, expiry: options.expiry }, undefined, fromStepName);
1274
- }
1275
- if (!stepState.stepId) {
1276
- stepState = await this.insertStepState(runId, approvalStepName, 'pikkuWorkflowApproval', { reason, expiry: options.expiry }, undefined, fromStepName);
1277
- }
1278
- if (stepState.status === 'succeeded') {
1279
- return stepState.result;
1280
- }
1281
- const stateKey = this.approvalStateKey(approvalStepName);
1282
- let record = ((await this.getRunState(runId))[stateKey] ?? {});
1283
- if (stepState.status === 'pending') {
1284
- await this.setStepRunning(stepState.stepId);
1285
- if (options.expiry !== undefined && !record.expiresAt) {
1286
- const expiresAt = new Date(Date.now() + getDurationInMilliseconds(options.expiry)).toISOString();
1287
- record = { ...record, expiresAt };
1288
- await this.updateRunState(runId, stateKey, record);
1289
- await this.scheduleRunWake(runId, getDurationInMilliseconds(options.expiry));
1290
- }
1291
- }
1292
- if (record.decision !== undefined) {
1293
- const validation = await options.schema['~standard'].validate(record.decision);
1294
- if (validation.issues) {
1295
- await this.updateRunState(runId, stateKey, {
1296
- ...record,
1297
- decision: undefined,
1298
- decidedAt: undefined,
1299
- error: validation.issues.map((issue) => ({
1300
- message: issue.message,
1301
- path: issue.path?.map((segment) => typeof segment === 'object' ? segment.key : segment),
1302
- })),
1303
- });
1304
- throw new WorkflowSuspendedException(runId, reason);
1305
- }
1306
- const outcome = {
1307
- status: 'decided',
1308
- data: validation.value,
1309
- };
1310
- await this.setStepResult(stepState.stepId, outcome);
1311
- return outcome;
1312
- }
1313
- if (record.expiresAt && Date.now() >= Date.parse(record.expiresAt)) {
1314
- const outcome = { status: 'expired' };
1315
- await this.setStepResult(stepState.stepId, outcome);
1316
- return outcome;
1317
- }
1318
- throw new WorkflowSuspendedException(runId, reason);
1319
- });
1025
+ const approvalStepName = this.nextStepKey(runId, approvalStepNameFor(reason));
1026
+ return await this.withStepLock(runId, approvalStepName, () => evaluateApprovalStep(this.approvalStore, runId, reason, approvalStepName, fromStepName, options));
1320
1027
  }
1321
1028
  createWorkflowWire(name, runId, rpcService, addonNamespace) {
1322
1029
  const workflowWire = {
@@ -1362,40 +1069,15 @@ export class PikkuWorkflowService {
1362
1069
  }
1363
1070
  }
1364
1071
  getConfig() {
1365
- const singletonServices = getSingletonServices();
1366
- const workflow = singletonServices.config?.workflow;
1367
- return {
1368
- retries: workflow?.retries ?? DEFAULT_STEP_RETRIES,
1369
- retryDelay: workflow?.retryDelay ?? 0,
1370
- orchestratorQueueName: workflow?.orchestratorQueueName ?? 'pikku-workflow-orchestrator',
1371
- stepWorkerQueueName: workflow?.stepWorkerQueueName ?? 'pikku-workflow-step-worker',
1372
- sleeperRPCName: workflow?.sleeperRPCName ?? 'pikkuWorkflowSleeper',
1373
- };
1072
+ return resolveWorkflowConfig();
1374
1073
  }
1375
1074
  getOrchestratorQueueName(workflowName) {
1376
- if (workflowName && this.queueStrategy !== 'shared-groups') {
1377
- const perWorkflow = `wf-orchestrator-${toKebab(workflowName)}`;
1378
- const meta = pikkuState(null, 'queue', 'meta');
1379
- if (meta[perWorkflow]) {
1380
- return perWorkflow;
1381
- }
1382
- }
1383
- return this.getConfig().orchestratorQueueName;
1075
+ return orchestratorQueueName(this.queueStrategy, workflowName);
1384
1076
  }
1385
1077
  getStepWorkerQueueName(rpcName) {
1386
- if (rpcName && this.queueStrategy !== 'shared-groups') {
1387
- const perStep = `wf-step-${toKebab(rpcName)}`;
1388
- const meta = pikkuState(null, 'queue', 'meta');
1389
- if (meta[perStep]) {
1390
- return perStep;
1391
- }
1392
- }
1393
- return this.getConfig().stepWorkerQueueName;
1078
+ return stepWorkerQueueName(this.queueStrategy, rpcName);
1394
1079
  }
1395
1080
  getJobGroup(id) {
1396
- if (!id || this.queueStrategy !== 'shared-groups') {
1397
- return undefined;
1398
- }
1399
- return { id, tier: id };
1081
+ return jobGroupFor(this.queueStrategy, id);
1400
1082
  }
1401
1083
  }
@@ -0,0 +1,39 @@
1
+ import type { ApprovalOutcome, StepState, WorkflowApprovalOptions } from './workflow.types.js';
2
+ /** The durable step name an approval point is recorded under. */
3
+ export declare const approvalStepNameFor: (reason: string) => string;
4
+ /**
5
+ * The run-state key holding an approval's decision.
6
+ *
7
+ * Hex-encoded because the key is composed from a caller-supplied reason, and
8
+ * run state is a flat record — an unescaped reason could collide with, or
9
+ * shadow, another key.
10
+ */
11
+ export declare const approvalStateKey: (stepName: string) => string;
12
+ /** What the approval gate needs from the workflow service. */
13
+ export type ApprovalStore = {
14
+ getStepState: (runId: string, stepName: string) => Promise<StepState>;
15
+ insertStepState: (runId: string, stepName: string, rpcName: string, input: unknown, output: undefined, fromStepName?: string) => Promise<StepState>;
16
+ setStepRunning: (stepId: string) => Promise<void>;
17
+ setStepResult: (stepId: string, result: unknown) => Promise<void>;
18
+ getRunState: (runId: string) => Promise<Record<string, unknown>>;
19
+ updateRunState: (runId: string, key: string, value: unknown) => Promise<void>;
20
+ resumeWorkflow: (runId: string) => Promise<void>;
21
+ scheduleRunWake: (runId: string, delay: number) => Promise<void>;
22
+ };
23
+ /**
24
+ * Record a decision against an approval point and wake the run.
25
+ *
26
+ * A decision arriving for an already-settled approval is refused rather than
27
+ * overwriting it: the run has moved on, and a second answer would be recorded
28
+ * against a gate nobody is waiting at.
29
+ */
30
+ export declare const recordApprovalDecision: (store: ApprovalStore, runId: string, reason: string, decision: unknown) => Promise<void>;
31
+ /**
32
+ * Evaluate an approval gate on replay.
33
+ *
34
+ * The payload is validated here rather than where it was submitted, because the
35
+ * schema is a value on the workflow and only the workflow has it. A payload
36
+ * that fails validation is cleared and the run suspends again, so a bad
37
+ * submission cannot settle the gate.
38
+ */
39
+ export declare const evaluateApprovalStep: (store: ApprovalStore, runId: string, reason: string, approvalStepName: string, fromStepName: string | undefined, options: WorkflowApprovalOptions) => Promise<ApprovalOutcome<unknown>>;