@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
@@ -1629,6 +1629,7 @@ describe('resumeAIAgent', () => {
1629
1629
  getRun: async () => remainingRun,
1630
1630
  resolveApproval: async (toolCallId: string, status: string) => {
1631
1631
  resolveCalls.push({ toolCallId, status })
1632
+ return true
1632
1633
  },
1633
1634
  updateRun: async (_runId: string, patch: any) => {
1634
1635
  updates.push(patch)
@@ -1750,6 +1751,7 @@ describe('resumeAIAgent', () => {
1750
1751
  },
1751
1752
  resolveApproval: async (toolCallId: string, status: string) => {
1752
1753
  resolveCalls.push({ toolCallId, status })
1754
+ return true
1753
1755
  },
1754
1756
  updateRun: async (_runId: string, patch: any) => {
1755
1757
  updates.push(patch)
@@ -10,6 +10,7 @@ import type {
10
10
  AIAgentMemoryConfig,
11
11
  } from './ai-agent.types.js'
12
12
  import { pikkuState, getSingletonServices } from '../../pikku-state.js'
13
+ import { applyInputMiddleware } from './ai-agent-turn.js'
13
14
  import { AIProviderNotConfiguredError } from '../../errors/errors.js'
14
15
  import { randomUUID } from './ai-agent-utils.js'
15
16
  import { SPOKEN_TRANSCRIPT } from './voice-input.js'
@@ -676,38 +677,26 @@ export async function streamAIAgent(
676
677
  // One bag per run, shared by every middleware — see PikkuAIMiddlewareHooks.
677
678
  const sharedNotes: Record<string, unknown> = {}
678
679
 
679
- let modifiedMessages = runnerParams.messages
680
- let modifiedInstructions = runnerParams.instructions
681
- for (const mw of aiMiddlewares) {
682
- if (mw.modifyInput) {
683
- const result = await mw.modifyInput(singletonServices, {
684
- messages: modifiedMessages,
685
- instructions: modifiedInstructions,
686
- shared: sharedNotes,
687
- })
688
- modifiedMessages = result.messages
689
- modifiedInstructions = result.instructions
690
- }
691
- }
680
+ const { messages: modifiedMessages, instructions: modifiedInstructions } =
681
+ await applyInputMiddleware(
682
+ aiMiddlewares,
683
+ singletonServices,
684
+ {
685
+ messages: runnerParams.messages,
686
+ instructions: runnerParams.instructions,
687
+ },
688
+ sharedNotes
689
+ )
692
690
  runnerParams.messages = modifiedMessages
693
691
  runnerParams.instructions = modifiedInstructions
694
692
 
695
- // Sent on the raw channel, ahead of the run. A voice client sent audio and so
696
- // has no idea what it said; until this arrives its own message is a blank
697
- // bubble. Ahead of the run rather than alongside it because the answer starts
698
- // streaming within a few hundred milliseconds, and a question that appears
699
- // after its answer reads as the wrong question.
693
+ // knowledge: decisions/internals/the-transcript-event-is-sent-ahead-of-the-run.md
700
694
  const transcript = sharedNotes[SPOKEN_TRANSCRIPT]
701
695
  if (typeof transcript === 'string') {
702
696
  channel.send({ type: 'transcript', text: transcript })
703
697
  }
704
698
 
705
- // What goes into thread history is what the model was actually asked. For a
706
- // spoken turn that is not what arrived over the wire: the wire carried a
707
- // base64 audio blob, and persisting it would write megabytes of unreadable
708
- // data into the history while losing the only readable record of the turn.
709
- // Identity-checked rather than assumed — a middleware is free to rewrite the
710
- // message list into something with no relation to the turn.
699
+ // knowledge: decisions/internals/thread-history-records-the-transcript-not-the-audio.md
711
700
  const lastModified = modifiedMessages[modifiedMessages.length - 1]
712
701
  const persistedUserMessage =
713
702
  lastModified?.id === userMessage.id ? lastModified : userMessage
@@ -768,7 +757,7 @@ export async function streamAIAgent(
768
757
  {
769
758
  wireInheritedChannelMiddleware: meta?.channelMiddleware,
770
759
  wireChannelMiddleware: [
771
- ...((agent.channelMiddleware as any[]) ?? []),
760
+ ...(agent.channelMiddleware ?? []),
772
761
  ...streamMiddleware,
773
762
  ],
774
763
  }
@@ -792,10 +781,7 @@ export async function streamAIAgent(
792
781
 
793
782
  const credentialFilteredChannel: AIStreamChannel = {
794
783
  ...wrappedChannel,
795
- // Returns what the inner send returns. Middleware runs asynchronously, so
796
- // swallowing the promise here makes every `await channel.send(...)` upstream
797
- // a no-op — including the one that waits for a buffering hook to flush
798
- // before the channel closes.
784
+ // knowledge: decisions/internals/an-agent-stream-send-must-return-the-inner-sends-promise.md
799
785
  send: (event: AIStreamEvent) => {
800
786
  if (
801
787
  event.type === 'tool-result' &&
@@ -872,11 +858,7 @@ export async function streamAIAgent(
872
858
  runId
873
859
  )
874
860
 
875
- // Through the middleware rather than straight at the channel, and awaited.
876
- // `done` is the only signal a stream hook gets that the reply is over, and
877
- // the ones that buffer need it: `voiceOutput` speaks a trailing fragment
878
- // that never reached a full stop, and waits for audio it has already paid
879
- // to synthesize. Sent raw, that work is discarded by the `close()` below.
861
+ // knowledge: decisions/internals/the-agent-done-event-goes-through-the-middleware-and-is-awaited.md
880
862
  await outputChannel.send({ type: 'done' })
881
863
  channel.close()
882
864
  return persistingChannel.fullText
@@ -979,12 +961,7 @@ export async function interruptAIAgent(
979
961
  reason: input.reason ?? 'user',
980
962
  })
981
963
 
982
- // A run still marked `running` that this process cannot abort is executing on
983
- // another instance. Returning a bare `false` there is indistinguishable from
984
- // "it already finished", so the one deployment shape this registry does not
985
- // cover fails silently — an agent that simply refuses to shut up, with nothing
986
- // in the logs. Say so instead. See `signalRunInterrupt` for the fix (fan the
987
- // interrupt out over eventHub so every instance tries locally).
964
+ // knowledge: decisions/internals/an-agent-run-owned-by-another-instance-says-so.md
988
965
  if (!stopped && run.status === 'running') {
989
966
  logger?.warn(
990
967
  `Could not interrupt run ${input.runId}: it is running in another process. ` +
@@ -1037,10 +1014,18 @@ export async function resumeAIAgent(
1037
1014
 
1038
1015
  await assertAgentAuthorized(agent, params, packageName)
1039
1016
 
1040
- await aiRunState.resolveApproval(
1017
+ // The read above is not a claim — concurrent resumes all see the same pending
1018
+ // approval. `resolveApproval` is the claim, and the loser must not go on to
1019
+ // run the tool a second time.
1020
+ const claimed = await aiRunState.resolveApproval(
1041
1021
  input.toolCallId,
1042
1022
  input.approved ? 'approved' : 'denied'
1043
1023
  )
1024
+ if (!claimed) {
1025
+ throw new Error(
1026
+ `Approval for toolCallId ${input.toolCallId} was already resolved by another caller`
1027
+ )
1028
+ }
1044
1029
 
1045
1030
  const { storage } = resolveMemoryServices(agent, singletonServices)
1046
1031
  const memoryConfig = agent.memory
@@ -1317,24 +1302,15 @@ async function continueAfterToolResult(
1317
1302
  // One bag per run, shared by every middleware — see PikkuAIMiddlewareHooks.
1318
1303
  const sharedNotes: Record<string, unknown> = {}
1319
1304
 
1320
- let modifiedMessages = trimmedMessages
1321
- let modifiedInstructions = instructions
1322
- for (const mw of aiMiddlewares) {
1323
- if (mw.modifyInput) {
1324
- const result = await mw.modifyInput(singletonServices, {
1325
- messages: modifiedMessages,
1326
- instructions: modifiedInstructions,
1327
- shared: sharedNotes,
1328
- })
1329
- modifiedMessages = result.messages
1330
- modifiedInstructions = result.instructions
1331
- }
1332
- }
1305
+ const { messages: modifiedMessages, instructions: modifiedInstructions } =
1306
+ await applyInputMiddleware(
1307
+ aiMiddlewares,
1308
+ singletonServices,
1309
+ { messages: trimmedMessages, instructions: instructions },
1310
+ sharedNotes
1311
+ )
1333
1312
 
1334
- // Resuming is as interruptible as the first turn. It is the same person
1335
- // listening to the same voice, and after an approval it is where most of the
1336
- // reply actually gets spoken — an approved delete is followed by the agent
1337
- // talking about it, and that is a normal thing to talk over.
1313
+ // knowledge: decisions/internals/a-resumed-agent-turn-is-as-interruptible-as-the-first.md
1338
1314
  const interruptHandle = registerInterruptibleRun(run.runId)
1339
1315
 
1340
1316
  const streamMiddleware = aiMiddlewares
@@ -1369,7 +1345,7 @@ async function continueAfterToolResult(
1369
1345
  {
1370
1346
  wireInheritedChannelMiddleware: meta?.channelMiddleware,
1371
1347
  wireChannelMiddleware: [
1372
- ...((agent.channelMiddleware as any[]) ?? []),
1348
+ ...(agent.channelMiddleware ?? []),
1373
1349
  ...streamMiddleware,
1374
1350
  ],
1375
1351
  }
@@ -1467,10 +1443,7 @@ async function continueAfterToolResult(
1467
1443
  run.runId
1468
1444
  )
1469
1445
 
1470
- // Through the middleware, for the same reason as the first turn — and it
1471
- // matters more here. After an approval, most of what gets spoken is the
1472
- // agent talking about what it just did, so this is the half of the reply a
1473
- // dropped flush would silence.
1446
+ // knowledge: decisions/internals/the-agent-done-event-goes-through-the-middleware-and-is-awaited.md
1474
1447
  await wrappedChannel.send({ type: 'done' })
1475
1448
  channel.close()
1476
1449
  } catch (err) {
@@ -114,7 +114,7 @@ const createHarness = (
114
114
  ],
115
115
  messages: [],
116
116
  }),
117
- resolveApproval: async () => {},
117
+ resolveApproval: async () => true,
118
118
  },
119
119
  } as any)
120
120
 
@@ -0,0 +1,121 @@
1
+ import type { CoreSingletonServices } from '../../types/core.types.js'
2
+ import type { PikkuAIMiddlewareHooks } from './ai-agent.types.js'
3
+
4
+ /** Exactly what `modifyInput` accepts and returns, taken from the hook itself. */
5
+ type TurnInput = Pick<
6
+ Parameters<NonNullable<PikkuAIMiddlewareHooks['modifyInput']>>[1],
7
+ 'messages' | 'instructions'
8
+ >
9
+
10
+ /**
11
+ * Run every `modifyInput` hook in order, threading each result into the next.
12
+ *
13
+ * Shared by the four places a turn starts — first turn and resume, on both the
14
+ * streaming and non-streaming paths. `sharedNotes` is one bag per run that the
15
+ * caller keeps, because hooks use it to pass things forward (`voiceInput`
16
+ * leaves the transcript there for `voiceOutput`).
17
+ */
18
+ export const applyInputMiddleware = async (
19
+ aiMiddlewares: PikkuAIMiddlewareHooks[],
20
+ singletonServices: CoreSingletonServices,
21
+ input: TurnInput,
22
+ sharedNotes: Record<string, unknown>
23
+ ): Promise<TurnInput> => {
24
+ let { messages, instructions } = input
25
+ for (const mw of aiMiddlewares) {
26
+ if (!mw.modifyInput) continue
27
+ const result = await mw.modifyInput(singletonServices, {
28
+ messages,
29
+ instructions,
30
+ shared: sharedNotes,
31
+ })
32
+ messages = result.messages
33
+ instructions = result.instructions
34
+ }
35
+ return { messages, instructions }
36
+ }
37
+
38
+ /** The step fields both agent paths hand to `afterStep`, taken from the hook. */
39
+ type StepResult = Omit<
40
+ Parameters<NonNullable<PikkuAIMiddlewareHooks['afterStep']>>[1],
41
+ 'stepNumber'
42
+ >
43
+
44
+ /**
45
+ * Notify every `afterStep` hook. The payload is the step verbatim plus its
46
+ * ordinal — a hook that wants the run's totals accumulates them itself.
47
+ */
48
+ export const notifyAfterStep = async (
49
+ aiMiddlewares: PikkuAIMiddlewareHooks[],
50
+ singletonServices: CoreSingletonServices,
51
+ stepNumber: number,
52
+ stepResult: StepResult
53
+ ): Promise<void> => {
54
+ for (const mw of aiMiddlewares) {
55
+ if (!mw.afterStep) continue
56
+ await mw.afterStep(singletonServices, {
57
+ stepNumber,
58
+ text: stepResult.text,
59
+ toolCalls: stepResult.toolCalls,
60
+ toolResults: stepResult.toolResults,
61
+ usage: stepResult.usage,
62
+ finishReason: stepResult.finishReason,
63
+ })
64
+ }
65
+ }
66
+
67
+ /**
68
+ * The step shape kept on the run: each tool call paired with its result.
69
+ *
70
+ * A result is stringified unless it already is a string, because this is what
71
+ * gets persisted and read back as history — an object there is unreadable.
72
+ */
73
+ export const toAccumulatedStep = (stepResult: StepResult) => ({
74
+ usage: stepResult.usage,
75
+ toolCalls: stepResult.toolCalls.map((tc) => {
76
+ const tr = stepResult.toolResults.find(
77
+ (r) => r.toolCallId === tc.toolCallId
78
+ )
79
+ return {
80
+ name: tc.toolName,
81
+ args: tc.args as Record<string, unknown>,
82
+ result:
83
+ typeof tr?.result === 'string'
84
+ ? tr.result
85
+ : JSON.stringify(tr?.result ?? ''),
86
+ }
87
+ }),
88
+ })
89
+
90
+ type PendingApproval = {
91
+ toolName: string
92
+ args: unknown
93
+ reason?: string
94
+ }
95
+
96
+ type ToolDef = {
97
+ name: string
98
+ approvalDescriptionFn?: (args: any) => Promise<string> | string
99
+ }
100
+
101
+ /**
102
+ * Fill in each approval's human-readable reason from the tool's own
103
+ * `approvalDescriptionFn`, where it did not supply one.
104
+ *
105
+ * A description that throws is swallowed: the approval still has to be raised,
106
+ * and a gate that disappears because its label failed to render is the worst
107
+ * available outcome.
108
+ */
109
+ export const describeApprovals = async (
110
+ approvals: PendingApproval[],
111
+ tools: ToolDef[]
112
+ ): Promise<void> => {
113
+ for (const approval of approvals) {
114
+ if (approval.reason) continue
115
+ const toolDef = tools.find((t) => t.name === approval.toolName)
116
+ if (!toolDef?.approvalDescriptionFn) continue
117
+ try {
118
+ approval.reason = await toolDef.approvalDescriptionFn(approval.args)
119
+ } catch {}
120
+ }
121
+ }
@@ -413,7 +413,7 @@ export type AIStreamEvent =
413
413
  toolName: string
414
414
  args: unknown
415
415
  reason?: string
416
- runId?: string
416
+ runId: string
417
417
  agent?: string
418
418
  session?: string
419
419
  }
@@ -164,12 +164,7 @@ export const voiceInput = (config?: {
164
164
  // to send, and a message with no content is not a question.
165
165
  if (updatedContent.length === 0) throw new NoSpeechDetectedError()
166
166
 
167
- // Only when something was actually heard. A turn can carry audio that all
168
- // reads as non-speech and still have content — an image with a silent
169
- // caption clip — which leaves nothing above to throw. Recording `''` here
170
- // would send a transcript event saying the user said nothing, and a client
171
- // that tells "not transcribed yet" from "transcribed" by the key being
172
- // absent would render the turn as an empty bubble rather than a pending one.
167
+ // knowledge: decisions/internals/an-empty-transcript-is-not-recorded.md
173
168
  if (heard.length > 0) {
174
169
  shared[SPOKEN_TRANSCRIPT] = heard.join(' ')
175
170
  }
@@ -206,11 +206,7 @@ export const voiceOutput = (config?: {
206
206
  }
207
207
  if (!aiAgentRunner?.generateSpeech) return event
208
208
 
209
- // Only an explicit `false` silences the reply. `voiceInput` sets this on
210
- // every turn it handles, so `false` means a real user really typed; the
211
- // key being absent means nothing reported either way — no voice input is
212
- // wired — and that caller's replies are spoken exactly as they were before
213
- // this option existed.
209
+ // knowledge: decisions/internals/voice-output-speaks-unless-voice-input-explicitly-says-otherwise.md
214
210
  if (!config?.always && shared[SPOKEN_TURN] === false) return event
215
211
 
216
212
  /**
@@ -228,13 +224,7 @@ export const voiceOutput = (config?: {
228
224
  )
229
225
  }
230
226
 
231
- // Checked per sentence but announced once per reply: a bilingual answer
232
- // should still speak the half it can, and repeating the notice for
233
- // every sentence of a long one would bury the reply itself.
234
- //
235
- // Per sentence is also what makes the voice right. A reply that answers
236
- // in English and then quotes a Chinese title is two sentences in two
237
- // scripts, and each is synthesized in the voice its own script needs.
227
+ // knowledge: decisions/internals/speech-synthesis-picks-a-voice-per-sentence-and-warns-once.md
238
228
  let voice = config.voice
239
229
  if (config.speakableScripts) {
240
230
  const unspeakable = unspeakableScripts(text, config.speakableScripts)
@@ -1,3 +1,4 @@
1
+ import type { CorePikkuChannelMiddleware } from './channel.types.js'
1
2
  import type {
2
3
  CoreSingletonServices,
3
4
  CorePikkuMiddleware,
@@ -61,7 +62,9 @@ export const runChannelLifecycleWithMiddleware = async ({
61
62
  `${channelConfig.name}:${lifecycleType}:cm`,
62
63
  {
63
64
  wireInheritedChannelMiddleware: channelMiddlewareMeta,
64
- wireChannelMiddleware: channelConfig.channelMiddleware as any,
65
+ // knowledge: questions/channel-middleware-accepts-bare-factories-that-nothing-resolves.md
66
+ wireChannelMiddleware:
67
+ channelConfig.channelMiddleware as CorePikkuChannelMiddleware[],
65
68
  }
66
69
  )
67
70
 
@@ -76,7 +79,7 @@ export const runChannelLifecycleWithMiddleware = async ({
76
79
  const runLifecycle = async () => {
77
80
  return await runPikkuFunc('channel', channelConfig.name, meta.pikkuFuncId, {
78
81
  singletonServices: services,
79
- data: () => data as any,
82
+ data: () => data,
80
83
  wire,
81
84
  sessionService: userSession,
82
85
  tags: meta.tags ?? [],
@@ -0,0 +1,72 @@
1
+ import { describe, test } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+
4
+ import { pikkuState, resetPikkuState } from '../../pikku-state.js'
5
+ import { wireChannel } from './channel-runner.js'
6
+
7
+ const registeredFunc = (funcId: string) =>
8
+ pikkuState(null, 'function', 'functions').get(funcId)
9
+
10
+ const wire = (channel: any) => {
11
+ resetPikkuState()
12
+ pikkuState(null, 'channel', 'meta', {
13
+ shapes: {
14
+ name: 'shapes',
15
+ route: '/shapes',
16
+ input: null,
17
+ connect: { pikkuFuncId: 'shapes__connect' },
18
+ disconnect: { pikkuFuncId: 'shapes__disconnect' },
19
+ message: { pikkuFuncId: 'shapes__message' },
20
+ messageWirings: {},
21
+ },
22
+ } as any)
23
+ wireChannel({ name: 'shapes', route: '/shapes', ...channel })
24
+ }
25
+
26
+ /**
27
+ * knowledge: decisions/internals/channel-message-handlers-accept-three-config-shapes.md
28
+ */
29
+ describe('every channel handler shape registers a callable function config', () => {
30
+ const handler = async () => undefined
31
+
32
+ test('a direct function config registers as-is', () => {
33
+ wire({ onConnect: { func: handler } })
34
+
35
+ assert.equal(typeof registeredFunc('shapes__connect')?.func, 'function')
36
+ })
37
+
38
+ test('a wrapper around a function config registers the inner config', () => {
39
+ wire({ onConnect: { func: { func: handler }, middleware: [] } })
40
+
41
+ // Registering the wrapper itself leaves `.func` an object, and the function
42
+ // runner calls `.func(...)` — that is a TypeError at connect time.
43
+ assert.equal(
44
+ typeof registeredFunc('shapes__connect')?.func,
45
+ 'function',
46
+ 'the wrapper was registered instead of the config it wraps'
47
+ )
48
+ })
49
+
50
+ test('onDisconnect unwraps the same way', () => {
51
+ wire({ onDisconnect: { func: { func: handler }, middleware: [] } })
52
+
53
+ assert.equal(typeof registeredFunc('shapes__disconnect')?.func, 'function')
54
+ })
55
+
56
+ test('onMessage unwraps the same way', () => {
57
+ wire({ onMessage: { func: { func: handler } } })
58
+
59
+ assert.equal(typeof registeredFunc('shapes__message')?.func, 'function')
60
+ })
61
+
62
+ test('a simple wrapper — a function plus sibling middleware — stays whole', () => {
63
+ wire({ onConnect: { func: handler, middleware: [] } })
64
+
65
+ const registered = registeredFunc('shapes__connect')
66
+ assert.equal(typeof registered?.func, 'function')
67
+ assert.ok(
68
+ 'middleware' in (registered as object),
69
+ 'the sibling middleware must survive registration'
70
+ )
71
+ })
72
+ })
@@ -4,6 +4,7 @@ import type {
4
4
  CoreUserSession,
5
5
  } from '../../types/core.types.js'
6
6
  import type {
7
+ CorePikkuChannelMiddleware,
7
8
  BinaryData,
8
9
  ChannelMessageMeta,
9
10
  CoreChannel,
@@ -94,7 +95,7 @@ export const processMessageHandlers = (
94
95
  logger.error(
95
96
  `Channel ${channelConfig.name} with id ${channelHandler.getChannel().channelId} requires a session for ${routeMessage}. No session is attached to this websocket connection. Ensure auth middleware establishes a session during websocket upgrade, and configure sessionStore when the runtime needs persisted sessions.`
96
97
  )
97
- // TODO: Send error message back breaks typescript, but should be implemented somehow
98
+ // knowledge: questions/unauthorized-channel-replies-escape-the-declared-out-type.md
98
99
  channelHandler.getChannel().send(`Unauthorized for ${routeMessage}`)
99
100
  return
100
101
  }
@@ -111,10 +112,7 @@ export const processMessageHandlers = (
111
112
  // Get wire middleware: channel-level middleware + message-specific middleware
112
113
  const channelWireMiddleware = channelConfig.middleware || []
113
114
 
114
- // Check if onMessage is a wrapper object vs direct function config:
115
- // - Direct config: onMessage.func is a plain Function
116
- // - Wrapper: onMessage.func is a CorePikkuFunctionConfig (has onMessage.func.func)
117
- // - Simple wrapper: onMessage has both func (plain Function) and middleware properties
115
+ // knowledge: decisions/internals/channel-message-handlers-accept-three-config-shapes.md
118
116
  const isWrapper =
119
117
  onMessage &&
120
118
  typeof onMessage === 'object' &&
@@ -145,7 +143,9 @@ export const processMessageHandlers = (
145
143
  inheritedMiddleware,
146
144
  wireMiddleware,
147
145
  inheritedChannelMiddleware: channelMeta?.channelMiddleware,
148
- wireChannelMiddleware: wireChannelMiddleware as any,
146
+ // knowledge: questions/channel-middleware-accepts-bare-factories-that-nothing-resolves.md
147
+ wireChannelMiddleware:
148
+ wireChannelMiddleware as CorePikkuChannelMiddleware[],
149
149
  coerceDataFromSchema: true,
150
150
  tags: channelConfig.tags,
151
151
  sessionService: userSession,
@@ -191,7 +191,7 @@ export const processMessageHandlers = (
191
191
 
192
192
  processed = true
193
193
  const routeResult = await processMessage(
194
- data as any,
194
+ data,
195
195
  routes[routerValue],
196
196
  routingProperty,
197
197
  routerValue
@@ -10,20 +10,6 @@ import {
10
10
  type ChannelRPCValidator,
11
11
  } from './channel-rpc.types.js'
12
12
 
13
- /**
14
- * `remote` for channels that only ever flow one way — a server-sent stream, an
15
- * agent's output, a local CLI. Nothing is listening for a request on them, so
16
- * the call is refused rather than waiting out a timeout.
17
- */
18
- export const unsupportedChannelRemote = async (
19
- funcName: string
20
- ): Promise<never> => {
21
- throw new ChannelRPCError(
22
- `Cannot call "${funcName}" remotely: this channel has no peer that answers`,
23
- 'unsupported'
24
- )
25
- }
26
-
27
13
  /**
28
14
  * A `DeploymentService` whose transport is an open channel rather than an
29
15
  * address, so a server-side function can call back into a peer that has no
@@ -109,21 +109,42 @@ describe('ChannelRPCRegistry', () => {
109
109
  })
110
110
  })
111
111
 
112
- test('times out instead of hanging forever', async () => {
112
+ /**
113
+ * The registry unrefs its timeout timer so an in-flight call can never hold
114
+ * a process open. That makes the timeout unawaitable in a test: when the
115
+ * pending call is the only thing left, the loop drains before the timer
116
+ * fires, and node:test cancels the file — taking every later test with it.
117
+ *
118
+ * Driving the clock by hand removes the dependency on real time entirely.
119
+ * `t.mock.timers` is scoped to the test and resets itself, so the tests that
120
+ * do want a real clock are unaffected.
121
+ *
122
+ * knowledge: decisions/internals/an-unref-d-timer-cannot-be-awaited-under-node-test.md
123
+ */
124
+ test('times out instead of hanging forever', async (t) => {
125
+ t.mock.timers.enable({ apis: ['setTimeout'] })
126
+
113
127
  const registry = new ChannelRPCRegistry(10)
114
128
  const call = registry.register()
115
129
 
116
- await assert.rejects(call.promise, (e: ChannelRPCError) => {
130
+ const rejected = assert.rejects(call.promise, (e: ChannelRPCError) => {
117
131
  assert.equal(e.reason, 'timeout')
118
132
  return true
119
133
  })
134
+ t.mock.timers.tick(10)
135
+ await rejected
136
+
120
137
  assert.equal(registry.inFlight, 0, 'a timed out call is not leaked')
121
138
  })
122
139
 
123
- test('drops a late response for an already-timed-out call', async () => {
140
+ test('drops a late response for an already-timed-out call', async (t) => {
141
+ t.mock.timers.enable({ apis: ['setTimeout'] })
142
+
124
143
  const registry = new ChannelRPCRegistry(10)
125
144
  const call = registry.register()
126
- await assert.rejects(call.promise)
145
+ const rejected = assert.rejects(call.promise)
146
+ t.mock.timers.tick(10)
147
+ await rejected
127
148
 
128
149
  const settled = registry.settle({
129
150
  action: CHANNEL_RPC_RESPONSE,
@@ -148,3 +148,17 @@ export type ChannelRPCValidator = (
148
148
  funcName: string,
149
149
  value: unknown
150
150
  ) => Promise<void> | void
151
+
152
+ /**
153
+ * `remote` for wires with no peer that answers — a server-sent stream, an
154
+ * agent's output, a local CLI. Nothing is listening for a request on them, so
155
+ * the call is refused rather than waiting out a timeout.
156
+ */
157
+ export const unsupportedChannelRemote = async (
158
+ funcName: string
159
+ ): Promise<never> => {
160
+ throw new ChannelRPCError(
161
+ `Cannot call "${funcName}" remotely: this channel has no peer that answers`,
162
+ 'unsupported'
163
+ )
164
+ }