@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.
- package/CHANGELOG.md +234 -0
- package/dist/dev/hot-reload.js +1 -6
- package/dist/ecosystem.d.ts +27 -0
- package/dist/ecosystem.js +26 -0
- package/dist/errors/error-handler.js +4 -2
- package/dist/function/function-runner.js +20 -42
- package/dist/function/functions.types.d.ts +9 -5
- package/dist/index.d.ts +4 -9
- package/dist/index.js +3 -8
- package/dist/middleware/auth-cookie.d.ts +0 -4
- package/dist/middleware/auth-cookie.js +38 -2
- package/dist/middleware/cors.js +1 -0
- package/dist/middleware/index.d.ts +1 -1
- package/dist/middleware/index.js +1 -1
- package/dist/middleware/remote-auth.js +14 -3
- package/dist/permissions.d.ts +8 -10
- package/dist/permissions.js +0 -11
- package/dist/pikku-state.js +12 -5
- package/dist/schema.js +35 -1
- package/dist/services/ai-run-state-service.d.ts +7 -1
- package/dist/services/in-memory-ai-run-state-service.d.ts +1 -1
- package/dist/services/in-memory-ai-run-state-service.js +2 -1
- package/dist/services/in-memory-workflow-service.d.ts +10 -0
- package/dist/services/in-memory-workflow-service.js +25 -0
- package/dist/services/local-content-request-handler.d.ts +21 -0
- package/dist/services/local-content-request-handler.js +72 -53
- package/dist/services/local-content.d.ts +6 -0
- package/dist/services/local-content.js +14 -1
- package/dist/services/workflow-service.d.ts +4 -2
- package/dist/testing/service-tests/agent-run-service-tests.d.ts +10 -0
- package/dist/testing/service-tests/agent-run-service-tests.js +72 -0
- package/dist/testing/service-tests/ai-storage-service-tests.d.ts +3 -0
- package/dist/testing/service-tests/ai-storage-service-tests.js +226 -0
- package/dist/testing/service-tests/channel-store-tests.d.ts +3 -0
- package/dist/testing/service-tests/channel-store-tests.js +72 -0
- package/dist/testing/service-tests/credential-service-tests.d.ts +3 -0
- package/dist/testing/service-tests/credential-service-tests.js +109 -0
- package/dist/testing/service-tests/deployment-service-tests.d.ts +3 -0
- package/dist/testing/service-tests/deployment-service-tests.js +21 -0
- package/dist/testing/service-tests/event-hub-store-tests.d.ts +3 -0
- package/dist/testing/service-tests/event-hub-store-tests.js +34 -0
- package/dist/testing/service-tests/secret-service-tests.d.ts +3 -0
- package/dist/testing/service-tests/secret-service-tests.js +80 -0
- package/dist/testing/service-tests/session-store-tests.d.ts +3 -0
- package/dist/testing/service-tests/session-store-tests.js +43 -0
- package/dist/testing/service-tests/workflow-run-service-tests.d.ts +3 -0
- package/dist/testing/service-tests/workflow-run-service-tests.js +42 -0
- package/dist/testing/service-tests/workflow-service-tests.d.ts +3 -0
- package/dist/testing/service-tests/workflow-service-tests.js +150 -0
- package/dist/testing/service-tests.d.ts +6 -0
- package/dist/testing/service-tests.js +26 -791
- package/dist/types/core.types.d.ts +11 -1
- package/dist/types/state.types.d.ts +1 -1
- package/dist/utils/node-host-resolver.d.ts +12 -0
- package/dist/utils/node-host-resolver.js +16 -0
- package/dist/utils/safe-fetch.d.ts +18 -0
- package/dist/utils/safe-fetch.js +167 -29
- package/dist/wirings/actor-flow/run-conversation.js +1 -6
- package/dist/wirings/ai-agent/agent-rpc.d.ts +15 -0
- package/dist/wirings/ai-agent/agent-rpc.js +53 -0
- package/dist/wirings/ai-agent/ai-agent-agui.js +1 -5
- package/dist/wirings/ai-agent/ai-agent-memory.js +3 -1
- package/dist/wirings/ai-agent/ai-agent-prepare.js +3 -9
- package/dist/wirings/ai-agent/ai-agent-runner.js +28 -107
- package/dist/wirings/ai-agent/ai-agent-stream.js +20 -61
- package/dist/wirings/ai-agent/ai-agent-turn.d.ts +56 -0
- package/dist/wirings/ai-agent/ai-agent-turn.js +81 -0
- package/dist/wirings/ai-agent/ai-agent.types.d.ts +1 -1
- package/dist/wirings/ai-agent/voice-input.js +1 -6
- package/dist/wirings/ai-agent/voice-output.js +2 -12
- package/dist/wirings/channel/channel-common.js +1 -0
- package/dist/wirings/channel/channel-handler.js +3 -5
- package/dist/wirings/channel/channel-rpc-service.d.ts +0 -6
- package/dist/wirings/channel/channel-rpc-service.js +0 -8
- package/dist/wirings/channel/channel-rpc.types.d.ts +6 -0
- package/dist/wirings/channel/channel-rpc.types.js +8 -0
- package/dist/wirings/channel/channel-runner.d.ts +1 -3
- package/dist/wirings/channel/channel-runner.js +16 -8
- package/dist/wirings/channel/channel.types.d.ts +2 -0
- package/dist/wirings/channel/pikku-abstract-channel-handler.js +1 -0
- package/dist/wirings/channel/serverless/serverless-channel-runner.js +3 -0
- package/dist/wirings/cli/channel/cli-channel-runner.js +2 -0
- package/dist/wirings/cli/cli-runner.js +4 -2
- package/dist/wirings/cli/cli.types.d.ts +0 -8
- package/dist/wirings/cli/command-parser.js +13 -0
- package/dist/wirings/gateway/gateway-runner.js +41 -5
- package/dist/wirings/http/http-routes.js +2 -0
- package/dist/wirings/http/http-runner.d.ts +0 -10
- package/dist/wirings/http/http-runner.js +6 -13
- package/dist/wirings/http/http.types.d.ts +0 -10
- package/dist/wirings/http/index.d.ts +1 -1
- package/dist/wirings/http/index.js +1 -1
- package/dist/wirings/mcp/mcp-runner.d.ts +0 -7
- package/dist/wirings/mcp/mcp-runner.js +0 -6
- package/dist/wirings/rpc/rpc-runner.d.ts +8 -51
- package/dist/wirings/rpc/rpc-runner.js +53 -86
- package/dist/wirings/rpc/rpc-types.d.ts +3 -0
- package/dist/wirings/secret/validate-secret-definitions.js +2 -0
- package/dist/wirings/trigger/pikku-trigger-service.d.ts +0 -4
- package/dist/wirings/trigger/trigger-runner.js +1 -0
- package/dist/wirings/virtual-user/run-virtual-user.js +11 -11
- package/dist/wirings/virtual-user/virtual-user-derive.js +4 -25
- package/dist/wirings/virtual-user/virtual-user-dispositions.js +1 -4
- package/dist/wirings/workflow/dsl/workflow-dsl.types.d.ts +29 -2
- package/dist/wirings/workflow/feature.js +7 -4
- package/dist/wirings/workflow/graph/graph-runner.d.ts +1 -2
- package/dist/wirings/workflow/graph/graph-runner.js +8 -7
- package/dist/wirings/workflow/graph/workflow-graph.types.d.ts +0 -4
- package/dist/wirings/workflow/index.d.ts +7 -2
- package/dist/wirings/workflow/index.js +5 -1
- package/dist/wirings/workflow/pikku-scenario-service.d.ts +17 -3
- package/dist/wirings/workflow/pikku-scenario-service.js +38 -10
- package/dist/wirings/workflow/pikku-workflow-service.d.ts +44 -111
- package/dist/wirings/workflow/pikku-workflow-service.js +86 -404
- package/dist/wirings/workflow/workflow-approval.d.ts +39 -0
- package/dist/wirings/workflow/workflow-approval.js +114 -0
- package/dist/wirings/workflow/workflow-constants.d.ts +26 -0
- package/dist/wirings/workflow/workflow-constants.js +35 -0
- package/dist/wirings/workflow/workflow-errors.d.ts +58 -0
- package/dist/wirings/workflow/workflow-errors.js +112 -0
- package/dist/wirings/workflow/workflow-meta-resolver.d.ts +11 -0
- package/dist/wirings/workflow/workflow-meta-resolver.js +31 -0
- package/dist/wirings/workflow/workflow-queue-routing.d.ts +8 -0
- package/dist/wirings/workflow/workflow-queue-routing.js +38 -0
- package/dist/wirings/workflow/workflow-queue-wiring.d.ts +20 -0
- package/dist/wirings/workflow/workflow-queue-wiring.js +79 -0
- package/dist/wirings/workflow/workflow-recovery.d.ts +68 -0
- package/dist/wirings/workflow/workflow-recovery.js +101 -0
- package/dist/wirings/workflow/workflow-run-engine.types.d.ts +54 -0
- package/dist/wirings/workflow/workflow-run-engine.types.js +1 -0
- package/dist/wirings/workflow/workflow-run-ownership.d.ts +16 -0
- package/dist/wirings/workflow/workflow-run-ownership.js +29 -0
- package/dist/wirings/workflow/workflow-suspend.d.ts +12 -0
- package/dist/wirings/workflow/workflow-suspend.js +33 -0
- package/dist/wirings/workflow/workflow.types.d.ts +7 -0
- package/knowledge/decisions/internals/a-non-streaming-agent-run-registers-with-airunstate-too.md +22 -0
- package/knowledge/decisions/internals/a-resumed-agent-turn-is-as-interruptible-as-the-first.md +20 -0
- package/knowledge/decisions/internals/a-scenario-step-template-is-offered-unfilled.md +21 -0
- package/knowledge/decisions/internals/a-virtual-user-decides-whether-to-trust-memory-once-per-turn.md +21 -0
- package/knowledge/decisions/internals/a-wall-clock-threshold-is-a-load-test-in-disguise.md +46 -0
- package/knowledge/decisions/internals/a-workflow-wire-is-built-from-the-run-not-from-the-rpc-service.md +45 -0
- package/knowledge/decisions/internals/agent-context-waits-for-a-tool-result-still-being-written.md +21 -0
- package/knowledge/decisions/internals/agent-speech-travels-as-a-custom-agui-event.md +22 -0
- package/knowledge/decisions/internals/an-agent-interrupt-is-not-a-failure.md +23 -0
- package/knowledge/decisions/internals/an-agent-run-owned-by-another-instance-says-so.md +25 -0
- package/knowledge/decisions/internals/an-agent-stream-send-must-return-the-inner-sends-promise.md +22 -0
- package/knowledge/decisions/internals/an-empty-text-part-is-omitted-from-an-agent-message.md +20 -0
- package/knowledge/decisions/internals/an-empty-transcript-is-not-recorded.md +22 -0
- package/knowledge/decisions/internals/an-unref-d-timer-cannot-be-awaited-under-node-test.md +66 -0
- package/knowledge/decisions/internals/channel-state-accessors-are-unsound-generics-that-every-implementation-asserts.md +33 -0
- package/knowledge/decisions/internals/gateway-listener-middleware-runs-without-an-rpc-on-the-wire.md +43 -0
- package/knowledge/decisions/internals/hot-reload-writes-into-the-function-map-captured-at-startup.md +26 -0
- package/knowledge/decisions/internals/index.md +38 -26
- package/knowledge/decisions/internals/only-exposed-functions-enter-a-virtual-user-catalogue.md +21 -0
- package/knowledge/decisions/internals/scenario-given-and-when-are-sugar-but-then-is-not.md +24 -0
- package/knowledge/decisions/internals/side-effects-are-an-allowlist-not-a-boolean.md +34 -0
- package/knowledge/decisions/internals/speech-synthesis-picks-a-voice-per-sentence-and-warns-once.md +24 -0
- package/knowledge/decisions/internals/the-actor-prompt-says-json-because-of-json-object-mode.md +22 -0
- package/knowledge/decisions/internals/the-agent-done-event-goes-through-the-middleware-and-is-awaited.md +26 -0
- package/knowledge/decisions/internals/the-api-report-pins-members-not-just-names.md +43 -0
- package/knowledge/decisions/internals/the-ecosystem-entry-point-carries-the-adapter-surface.md +58 -0
- package/knowledge/decisions/internals/the-middleware-resolution-cache-is-deliberately-unbounded.md +40 -0
- package/knowledge/decisions/internals/the-per-invocation-rpc-view-is-a-class.md +40 -0
- package/knowledge/decisions/internals/the-persona-runtime-is-exported-from-the-persona-entry-point.md +28 -0
- package/knowledge/decisions/internals/the-transcript-event-is-sent-ahead-of-the-run.md +25 -0
- package/knowledge/decisions/internals/the-virtual-user-catalogue-is-the-only-gate-on-what-may-be-called.md +21 -0
- package/knowledge/decisions/internals/the-worker-disposition-is-the-one-that-is-not-testing.md +22 -0
- package/knowledge/decisions/internals/thread-history-records-the-transcript-not-the-audio.md +25 -0
- package/knowledge/decisions/internals/virtual-user-step-order-comes-from-insertion-order.md +21 -0
- package/knowledge/decisions/internals/voice-output-speaks-unless-voice-input-explicitly-says-otherwise.md +24 -0
- package/knowledge/decisions/internals/wiring-registries-erase-the-generics-their-wire-functions-capture.md +35 -0
- package/knowledge/decisions/security/a-dropped-audit-write-is-always-logged.md +4 -2
- package/knowledge/decisions/security/a-graph-run-starts-at-an-entry-node-the-graph-declared.md +33 -0
- package/knowledge/decisions/security/a-permission-gets-a-wire-it-cannot-reply-on.md +33 -0
- package/knowledge/decisions/security/a-step-runs-the-function-the-workflow-dispatched-it-with.md +37 -0
- package/knowledge/decisions/security/a-virtual-user-is-never-offered-a-step-that-would-forge-its-own-oracle.md +28 -0
- package/knowledge/decisions/security/a-workflow-run-is-read-and-approved-by-its-owner.md +34 -0
- package/knowledge/decisions/security/an-agent-approval-is-claimed-before-the-tool-runs.md +33 -0
- package/knowledge/decisions/security/an-upload-is-counted-as-it-arrives-not-buffered-then-measured.md +24 -0
- package/knowledge/decisions/security/gateway-handlers-run-through-the-function-runner-gate.md +13 -3
- package/knowledge/decisions/security/index.md +7 -0
- package/knowledge/questions/channel-middleware-accepts-bare-factories-that-nothing-resolves.md +35 -0
- package/knowledge/questions/index.md +2 -1
- package/knowledge/questions/unauthorized-channel-replies-escape-the-declared-out-type.md +44 -0
- package/package.json +14 -3
- package/scripts/generate-api-report.d.mts +1 -0
- package/scripts/generate-api-report.mjs +89 -0
- package/scripts/generate-api-report.mts +218 -0
- package/src/api-report.test.ts +69 -0
- package/src/column-form.ts +1 -4
- package/src/crypto-utils.test.ts +15 -2
- package/src/dev/hot-reload.ts +2 -7
- package/src/ecosystem.ts +40 -0
- package/src/errors/error-handler.ts +5 -3
- package/src/errors/error.test.ts +4 -1
- package/src/function/function-runner.test.ts +1 -1
- package/src/function/function-runner.ts +28 -39
- package/src/function/functions.types.ts +15 -9
- package/src/handle-error.ts +1 -1
- package/src/index.ts +3 -22
- package/src/middleware/auth-cookie.test.ts +50 -0
- package/src/middleware/auth-cookie.ts +38 -2
- package/src/middleware/cors.ts +2 -1
- package/src/middleware/index.ts +1 -1
- package/src/middleware/remote-auth.test.ts +43 -0
- package/src/middleware/remote-auth.ts +17 -3
- package/src/middleware-runner.ts +4 -2
- package/src/no-any-casts.test.ts +57 -0
- package/src/permissions.test.ts +3 -1
- package/src/permissions.ts +22 -24
- package/src/pikku-state.ts +17 -6
- package/src/public-surface.json +578 -0
- package/src/public-surface.json.README +25 -0
- package/src/public-surface.test.ts +105 -0
- package/src/removed-legacy-exports.test.ts +62 -0
- package/src/schema.test.ts +78 -0
- package/src/schema.ts +36 -1
- package/src/services/ai-run-state-service.ts +7 -1
- package/src/services/audit-service.ts +2 -2
- package/src/services/in-memory-ai-run-state-service.ts +3 -2
- package/src/services/in-memory-workflow-service.ts +25 -0
- package/src/services/index.ts +1 -4
- package/src/services/local-content-request-handler.test.ts +43 -5
- package/src/services/local-content-request-handler.ts +103 -74
- package/src/services/local-content.ts +15 -1
- package/src/services/local-email-service.ts +5 -1
- package/src/services/system-role-guard.test.ts +4 -1
- package/src/services/workflow-service.ts +9 -2
- package/src/side-effects-are-declared.test.ts +84 -0
- package/src/source-files-stay-composable.test.ts +41 -0
- package/src/testing/service-tests/agent-run-service-tests.ts +98 -0
- package/src/testing/service-tests/ai-storage-service-tests.ts +286 -0
- package/src/testing/service-tests/channel-store-tests.ts +98 -0
- package/src/testing/service-tests/credential-service-tests.ts +143 -0
- package/src/testing/service-tests/deployment-service-tests.ts +33 -0
- package/src/testing/service-tests/event-hub-store-tests.ts +48 -0
- package/src/testing/service-tests/secret-service-tests.ts +105 -0
- package/src/testing/service-tests/session-store-tests.ts +62 -0
- package/src/testing/service-tests/workflow-run-service-tests.ts +59 -0
- package/src/testing/service-tests/workflow-service-tests.ts +308 -0
- package/src/testing/service-tests.ts +31 -1111
- package/src/types/core.types.ts +15 -1
- package/src/types/state.types.ts +1 -1
- package/src/utils/node-host-resolver.ts +20 -0
- package/src/utils/safe-fetch.test.ts +143 -1
- package/src/utils/safe-fetch.ts +200 -25
- package/src/wirings/actor-flow/run-conversation.ts +1 -6
- package/src/wirings/ai-agent/agent-rpc.ts +120 -0
- package/src/wirings/ai-agent/ai-agent-agui.ts +1 -5
- package/src/wirings/ai-agent/ai-agent-interrupt.test.ts +2 -1
- package/src/wirings/ai-agent/ai-agent-memory.ts +8 -1
- package/src/wirings/ai-agent/ai-agent-prepare.ts +10 -12
- package/src/wirings/ai-agent/ai-agent-resume-authorization.test.ts +1 -0
- package/src/wirings/ai-agent/ai-agent-runner.test.ts +82 -2
- package/src/wirings/ai-agent/ai-agent-runner.ts +49 -120
- package/src/wirings/ai-agent/ai-agent-stream.test.ts +2 -0
- package/src/wirings/ai-agent/ai-agent-stream.ts +36 -63
- package/src/wirings/ai-agent/ai-agent-thread-ownership.test.ts +1 -1
- package/src/wirings/ai-agent/ai-agent-turn.ts +121 -0
- package/src/wirings/ai-agent/ai-agent.types.ts +1 -1
- package/src/wirings/ai-agent/voice-input.ts +1 -6
- package/src/wirings/ai-agent/voice-output.ts +2 -12
- package/src/wirings/channel/channel-common.ts +5 -2
- package/src/wirings/channel/channel-handler-shapes.test.ts +72 -0
- package/src/wirings/channel/channel-handler.ts +7 -7
- package/src/wirings/channel/channel-rpc-service.ts +0 -14
- package/src/wirings/channel/channel-rpc.test.ts +25 -4
- package/src/wirings/channel/channel-rpc.types.ts +14 -0
- package/src/wirings/channel/channel-runner.ts +33 -20
- package/src/wirings/channel/channel.types.ts +2 -0
- package/src/wirings/channel/local/local-channel-runner.ts +1 -1
- package/src/wirings/channel/pikku-abstract-channel-handler.ts +2 -1
- package/src/wirings/channel/serverless/serverless-channel-runner.ts +6 -3
- package/src/wirings/cli/channel/cli-channel-runner.ts +3 -1
- package/src/wirings/cli/channel/cli-raw-client-runner.ts +4 -1
- package/src/wirings/cli/cli-runner.ts +7 -4
- package/src/wirings/cli/cli.types.ts +0 -39
- package/src/wirings/cli/command-parser.test.ts +19 -0
- package/src/wirings/cli/command-parser.ts +17 -1
- package/src/wirings/gateway/gateway-authorization.test.ts +131 -0
- package/src/wirings/gateway/gateway-channel-meta.test.ts +44 -0
- package/src/wirings/gateway/gateway-runner.ts +62 -22
- package/src/wirings/http/http-routes.ts +4 -1
- package/src/wirings/http/http-runner.ts +11 -26
- package/src/wirings/http/http.types.ts +0 -15
- package/src/wirings/http/index.ts +1 -7
- package/src/wirings/http/pikku-fetch-http-request.ts +2 -2
- package/src/wirings/http/web-request.ts +1 -1
- package/src/wirings/mcp/mcp-runner.ts +2 -16
- package/src/wirings/persona/persona-environments.test.ts +14 -3
- package/src/wirings/persona/persona.test.ts +13 -3
- package/src/wirings/persona/validate-personas.ts +5 -1
- package/src/wirings/queue/queue-runner.ts +1 -1
- package/src/wirings/rpc/addon-auth-tags.test.ts +1 -5
- package/src/wirings/rpc/rpc-runner.ts +58 -136
- package/src/wirings/rpc/rpc-types.ts +7 -0
- package/src/wirings/rpc/wire-addon.ts +3 -1
- package/src/wirings/secret/validate-secret-definitions.test.ts +69 -0
- package/src/wirings/secret/validate-secret-definitions.ts +2 -0
- package/src/wirings/trigger/pikku-trigger-service.ts +0 -5
- package/src/wirings/trigger/trigger-runner.ts +7 -5
- package/src/wirings/virtual-user/run-virtual-user.test.ts +18 -9
- package/src/wirings/virtual-user/run-virtual-user.ts +28 -15
- package/src/wirings/virtual-user/virtual-user-agents.test.ts +4 -1
- package/src/wirings/virtual-user/virtual-user-derive.ts +4 -25
- package/src/wirings/virtual-user/virtual-user-dispositions.test.ts +7 -2
- package/src/wirings/virtual-user/virtual-user-dispositions.ts +1 -4
- package/src/wirings/virtual-user/virtual-user-intents.test.ts +13 -3
- package/src/wirings/workflow/dsl/workflow-dsl.types.ts +35 -2
- package/src/wirings/workflow/feature.ts +7 -4
- package/src/wirings/workflow/graph/graph-node.ts +1 -1
- package/src/wirings/workflow/graph/graph-runner.test.ts +4 -2
- package/src/wirings/workflow/graph/graph-runner.ts +13 -16
- package/src/wirings/workflow/graph/workflow-graph.types.ts +0 -5
- package/src/wirings/workflow/index.ts +12 -4
- package/src/wirings/workflow/pikku-scenario-service.ts +54 -25
- package/src/wirings/workflow/pikku-workflow-service.test.ts +2 -2
- package/src/wirings/workflow/pikku-workflow-service.ts +208 -630
- package/src/wirings/workflow/scenario-hooks.test.ts +51 -0
- package/src/wirings/workflow/workflow-approval.ts +185 -0
- package/src/wirings/workflow/workflow-child-run-session.test.ts +89 -0
- package/src/wirings/workflow/workflow-constants.ts +47 -0
- package/src/wirings/workflow/workflow-dispatch-durability.test.ts +1 -1
- package/src/wirings/workflow/workflow-dispatch-relay.test.ts +128 -0
- package/src/wirings/workflow/workflow-errors.ts +128 -0
- package/src/wirings/workflow/workflow-inline-authority.test.ts +12 -6
- package/src/wirings/workflow/workflow-meta-resolver.ts +45 -0
- package/src/wirings/workflow/workflow-missing-meta.test.ts +92 -0
- package/src/wirings/workflow/workflow-queue-routing.ts +85 -0
- package/src/wirings/workflow/workflow-queue-wiring.ts +149 -0
- package/src/wirings/workflow/workflow-recovery.ts +162 -0
- package/src/wirings/workflow/workflow-retry-policy.test.ts +1 -1
- package/src/wirings/workflow/workflow-run-authority.test.ts +215 -0
- package/src/wirings/workflow/workflow-run-engine.types.ts +91 -0
- package/src/wirings/workflow/workflow-run-ownership.ts +36 -0
- package/src/wirings/workflow/workflow-suspend.ts +61 -0
- package/src/wirings/workflow/workflow.types.ts +7 -0
- package/src/wirings-stay-decoupled.test.ts +123 -0
- package/tsconfig.json +1 -1
- package/tsconfig.tsbuildinfo +1 -1
- package/src/internal.ts +0 -10
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { pikkuState } from '../../pikku-state.js'
|
|
2
|
+
import type { WorkflowRuntimeMeta } from './workflow.types.js'
|
|
3
|
+
|
|
4
|
+
export type ResolvedWorkflowMeta = {
|
|
5
|
+
meta: WorkflowRuntimeMeta
|
|
6
|
+
packageName: string | null
|
|
7
|
+
resolvedName: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Resolves a workflow name against the root registry, then — for a
|
|
12
|
+
* `namespace:name` form — against the addon package that claims the namespace.
|
|
13
|
+
*/
|
|
14
|
+
export const resolveWorkflowMeta = (
|
|
15
|
+
name: string
|
|
16
|
+
): ResolvedWorkflowMeta | null => {
|
|
17
|
+
const rootMeta = pikkuState(null, 'workflows', 'meta')
|
|
18
|
+
if (rootMeta[name]) {
|
|
19
|
+
return { meta: rootMeta[name], packageName: null, resolvedName: name }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const colonIndex = name.indexOf(':')
|
|
23
|
+
if (colonIndex === -1) {
|
|
24
|
+
return null
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const namespace = name.substring(0, colonIndex)
|
|
28
|
+
const localName = name.substring(colonIndex + 1)
|
|
29
|
+
const addons = pikkuState(null, 'addons', 'packages')
|
|
30
|
+
const pkgConfig = addons?.get(namespace)
|
|
31
|
+
if (!pkgConfig) {
|
|
32
|
+
return null
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const addonMeta = pikkuState(pkgConfig.package, 'workflows', 'meta')
|
|
36
|
+
if (!addonMeta?.[localName]) {
|
|
37
|
+
return null
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
meta: addonMeta[localName],
|
|
42
|
+
packageName: pkgConfig.package,
|
|
43
|
+
resolvedName: localName,
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { describe, test } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
|
|
4
|
+
import { InMemoryWorkflowService } from '../../services/in-memory-workflow-service.js'
|
|
5
|
+
import { pikkuState, resetPikkuState } from '../../pikku-state.js'
|
|
6
|
+
import { PikkuMissingMetaError } from '../../errors/errors.js'
|
|
7
|
+
import { WorkflowNotFoundError } from './workflow-errors.js'
|
|
8
|
+
|
|
9
|
+
const silentLogger = { error() {}, info() {}, warn() {}, debug() {} }
|
|
10
|
+
|
|
11
|
+
const startRun = async (workflowName: string) => {
|
|
12
|
+
pikkuState(null, 'package', 'singletonServices', {
|
|
13
|
+
logger: silentLogger,
|
|
14
|
+
} as any)
|
|
15
|
+
const service = new InMemoryWorkflowService()
|
|
16
|
+
const runId = await service.createRun(workflowName, {}, false, '', {
|
|
17
|
+
type: 'test',
|
|
18
|
+
})
|
|
19
|
+
return { service, runId }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
describe('a run whose workflow has no generated meta', () => {
|
|
23
|
+
test('reports missing metadata rather than crashing on an undefined meta', async () => {
|
|
24
|
+
resetPikkuState()
|
|
25
|
+
const { service, runId } = await startRun('flowWithoutMeta')
|
|
26
|
+
|
|
27
|
+
// The registration exists, so this is not an unknown workflow — codegen or
|
|
28
|
+
// the generated bootstrap import is what is missing.
|
|
29
|
+
pikkuState(null, 'workflows', 'registrations').set('flowWithoutMeta', {
|
|
30
|
+
name: 'flowWithoutMeta',
|
|
31
|
+
func: async () => undefined,
|
|
32
|
+
} as any)
|
|
33
|
+
|
|
34
|
+
await assert.rejects(
|
|
35
|
+
service.runWorkflowJob(runId, {}),
|
|
36
|
+
(error: unknown) => {
|
|
37
|
+
assert.ok(
|
|
38
|
+
error instanceof PikkuMissingMetaError,
|
|
39
|
+
`expected PikkuMissingMetaError, got ${(error as Error)?.constructor?.name}: ${(error as Error)?.message}`
|
|
40
|
+
)
|
|
41
|
+
assert.match(String((error as Error).message), /flowWithoutMeta/)
|
|
42
|
+
return true
|
|
43
|
+
}
|
|
44
|
+
)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
test('a workflow with neither meta nor a registration is still missing meta', async () => {
|
|
48
|
+
resetPikkuState()
|
|
49
|
+
const { service, runId } = await startRun('unknownFlow')
|
|
50
|
+
|
|
51
|
+
await assert.rejects(
|
|
52
|
+
service.runWorkflowJob(runId, {}),
|
|
53
|
+
(error: unknown) => {
|
|
54
|
+
assert.ok(
|
|
55
|
+
error instanceof PikkuMissingMetaError,
|
|
56
|
+
`expected PikkuMissingMetaError, got ${(error as Error)?.constructor?.name}`
|
|
57
|
+
)
|
|
58
|
+
return true
|
|
59
|
+
}
|
|
60
|
+
)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
test('a registered workflow that does have meta gets past both guards', async () => {
|
|
64
|
+
resetPikkuState()
|
|
65
|
+
const { service, runId } = await startRun('flowWithMeta')
|
|
66
|
+
|
|
67
|
+
pikkuState(null, 'workflows', 'meta', {
|
|
68
|
+
flowWithMeta: {
|
|
69
|
+
name: 'flowWithMeta',
|
|
70
|
+
pikkuFuncId: 'flowWithMeta',
|
|
71
|
+
source: 'dsl',
|
|
72
|
+
},
|
|
73
|
+
} as any)
|
|
74
|
+
|
|
75
|
+
// Guards the tests above: without this the assertions would pass for a
|
|
76
|
+
// service that threw PikkuMissingMetaError unconditionally.
|
|
77
|
+
await assert.rejects(
|
|
78
|
+
service.runWorkflowJob(runId, {}),
|
|
79
|
+
(error: unknown) => {
|
|
80
|
+
assert.ok(
|
|
81
|
+
!(error instanceof PikkuMissingMetaError),
|
|
82
|
+
'meta was present, so the missing-meta guard must not fire'
|
|
83
|
+
)
|
|
84
|
+
assert.ok(
|
|
85
|
+
error instanceof WorkflowNotFoundError,
|
|
86
|
+
`expected WorkflowNotFoundError, got ${(error as Error)?.constructor?.name}`
|
|
87
|
+
)
|
|
88
|
+
return true
|
|
89
|
+
}
|
|
90
|
+
)
|
|
91
|
+
})
|
|
92
|
+
})
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { getSingletonServices, pikkuState } from '../../pikku-state.js'
|
|
2
|
+
import { getDurationInMilliseconds } from '../../time-utils.js'
|
|
3
|
+
import type { JobGroup, JobOptions } from '../queue/queue.types.js'
|
|
4
|
+
import type {
|
|
5
|
+
WorkflowServiceConfig,
|
|
6
|
+
WorkflowStepOptions,
|
|
7
|
+
} from './workflow.types.js'
|
|
8
|
+
import { DEFAULT_STEP_RETRIES } from './workflow-constants.js'
|
|
9
|
+
|
|
10
|
+
export type WorkflowQueueStrategy = 'per-workflow' | 'shared-groups'
|
|
11
|
+
|
|
12
|
+
const toKebab = (s: string) =>
|
|
13
|
+
s.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase()
|
|
14
|
+
|
|
15
|
+
export const resolveWorkflowConfig = (): WorkflowServiceConfig => {
|
|
16
|
+
const workflow = getSingletonServices().config?.workflow
|
|
17
|
+
return {
|
|
18
|
+
retries: workflow?.retries ?? DEFAULT_STEP_RETRIES,
|
|
19
|
+
retryDelay: workflow?.retryDelay ?? 0,
|
|
20
|
+
orchestratorQueueName:
|
|
21
|
+
workflow?.orchestratorQueueName ?? 'pikku-workflow-orchestrator',
|
|
22
|
+
stepWorkerQueueName:
|
|
23
|
+
workflow?.stepWorkerQueueName ?? 'pikku-workflow-step-worker',
|
|
24
|
+
sleeperRPCName: workflow?.sleeperRPCName ?? 'pikkuWorkflowSleeper',
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Falls back to the shared queue whenever the dedicated one was not generated
|
|
30
|
+
* into queue meta, so a workflow added without a rebuild still runs.
|
|
31
|
+
*/
|
|
32
|
+
const dedicatedQueueName = (
|
|
33
|
+
prefix: string,
|
|
34
|
+
name: string | undefined,
|
|
35
|
+
strategy: WorkflowQueueStrategy,
|
|
36
|
+
fallback: string
|
|
37
|
+
): string => {
|
|
38
|
+
if (!name || strategy === 'shared-groups') {
|
|
39
|
+
return fallback
|
|
40
|
+
}
|
|
41
|
+
const dedicated = `${prefix}${toKebab(name)}`
|
|
42
|
+
return pikkuState(null, 'queue', 'meta')[dedicated] ? dedicated : fallback
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const orchestratorQueueName = (
|
|
46
|
+
strategy: WorkflowQueueStrategy,
|
|
47
|
+
workflowName?: string
|
|
48
|
+
): string =>
|
|
49
|
+
dedicatedQueueName(
|
|
50
|
+
'wf-orchestrator-',
|
|
51
|
+
workflowName,
|
|
52
|
+
strategy,
|
|
53
|
+
resolveWorkflowConfig().orchestratorQueueName
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
export const stepWorkerQueueName = (
|
|
57
|
+
strategy: WorkflowQueueStrategy,
|
|
58
|
+
rpcName?: string
|
|
59
|
+
): string =>
|
|
60
|
+
dedicatedQueueName(
|
|
61
|
+
'wf-step-',
|
|
62
|
+
rpcName,
|
|
63
|
+
strategy,
|
|
64
|
+
resolveWorkflowConfig().stepWorkerQueueName
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
export const jobGroupFor = (
|
|
68
|
+
strategy: WorkflowQueueStrategy,
|
|
69
|
+
id?: string
|
|
70
|
+
): JobGroup | undefined =>
|
|
71
|
+
id && strategy === 'shared-groups' ? { id, tier: id } : undefined
|
|
72
|
+
|
|
73
|
+
export const stepJobOptions = (
|
|
74
|
+
stepOptions?: WorkflowStepOptions
|
|
75
|
+
): JobOptions => {
|
|
76
|
+
const retries = stepOptions?.retries ?? DEFAULT_STEP_RETRIES
|
|
77
|
+
const retryDelay = stepOptions?.retryDelay
|
|
78
|
+
const backoff =
|
|
79
|
+
retryDelay !== undefined && retryDelay !== 'exponential'
|
|
80
|
+
? { type: 'fixed', delay: getDurationInMilliseconds(retryDelay) }
|
|
81
|
+
: retries > 0 || retryDelay === 'exponential'
|
|
82
|
+
? 'exponential'
|
|
83
|
+
: undefined
|
|
84
|
+
return { attempts: retries + 1, ...(backoff ? { backoff } : {}) }
|
|
85
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { addFunction } from '../../function/function-runner.js'
|
|
2
|
+
import { pikkuState } from '../../pikku-state.js'
|
|
3
|
+
import { wireQueueWorker } from '../queue/queue-runner.js'
|
|
4
|
+
import type {
|
|
5
|
+
GroupConcurrencyConfig,
|
|
6
|
+
PikkuWorkerConfig,
|
|
7
|
+
} from '../queue/queue.types.js'
|
|
8
|
+
import type { Logger } from '../../services/logger.js'
|
|
9
|
+
import {
|
|
10
|
+
pikkuWorkflowOrchestratorFunc,
|
|
11
|
+
pikkuWorkflowSleeperFunc,
|
|
12
|
+
pikkuWorkflowWorkerFunc,
|
|
13
|
+
} from './workflow-queue-workers.js'
|
|
14
|
+
import type { WorkflowQueueStrategy } from './workflow-queue-routing.js'
|
|
15
|
+
|
|
16
|
+
const ORCHESTRATOR_QUEUE_PREFIX = 'wf-orchestrator-'
|
|
17
|
+
const STEP_QUEUE_PREFIX = 'wf-step-'
|
|
18
|
+
|
|
19
|
+
const helperFunctionMeta = (funcId: string) => ({
|
|
20
|
+
pikkuFuncId: funcId,
|
|
21
|
+
sessionless: true,
|
|
22
|
+
functionType: 'helper' as const,
|
|
23
|
+
inputSchemaName: null,
|
|
24
|
+
outputSchemaName: null,
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
export type WorkflowQueueWiringOptions = {
|
|
28
|
+
strategy: WorkflowQueueStrategy
|
|
29
|
+
concurrency: number
|
|
30
|
+
groupConcurrency: number | GroupConcurrencyConfig
|
|
31
|
+
/**
|
|
32
|
+
* Resolved only when there is something to warn about: reading the logger
|
|
33
|
+
* throws while singleton services are uninitialized, and wiring queues before
|
|
34
|
+
* that point is legitimate.
|
|
35
|
+
*/
|
|
36
|
+
getLogger: () => Logger | undefined
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Registers the orchestrator, step-worker and sleeper functions and binds them
|
|
41
|
+
* to their queues. Idempotent: a queue already carrying a registered function
|
|
42
|
+
* is left alone.
|
|
43
|
+
*/
|
|
44
|
+
export const wireWorkflowQueueWorkers = ({
|
|
45
|
+
strategy,
|
|
46
|
+
concurrency,
|
|
47
|
+
groupConcurrency,
|
|
48
|
+
getLogger,
|
|
49
|
+
}: WorkflowQueueWiringOptions): void => {
|
|
50
|
+
const functions = pikkuState(null, 'function', 'functions')
|
|
51
|
+
const functionsMeta = pikkuState(null, 'function', 'meta')
|
|
52
|
+
const queueMeta = pikkuState(null, 'queue', 'meta')
|
|
53
|
+
|
|
54
|
+
const registerWorkflowFunc = (
|
|
55
|
+
funcId: string,
|
|
56
|
+
func: { func: unknown },
|
|
57
|
+
queueName: string,
|
|
58
|
+
config?: PikkuWorkerConfig
|
|
59
|
+
) => {
|
|
60
|
+
if (functions.has(funcId)) return
|
|
61
|
+
addFunction(funcId, func as never)
|
|
62
|
+
if (!queueMeta[queueName]) {
|
|
63
|
+
queueMeta[queueName] = { pikkuFuncId: funcId, name: queueName }
|
|
64
|
+
}
|
|
65
|
+
wireQueueWorker({ name: queueName, func, config } as never)
|
|
66
|
+
if (!functionsMeta[funcId]) {
|
|
67
|
+
functionsMeta[funcId] = helperFunctionMeta(funcId)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const sharedGroups = strategy === 'shared-groups'
|
|
72
|
+
const sharedQueueConfig: PikkuWorkerConfig | undefined = sharedGroups
|
|
73
|
+
? { batchSize: concurrency, groupConcurrency }
|
|
74
|
+
: undefined
|
|
75
|
+
|
|
76
|
+
registerWorkflowFunc(
|
|
77
|
+
'pikkuWorkflowOrchestrator',
|
|
78
|
+
{ func: pikkuWorkflowOrchestratorFunc },
|
|
79
|
+
'pikku-workflow-orchestrator',
|
|
80
|
+
sharedQueueConfig
|
|
81
|
+
)
|
|
82
|
+
registerWorkflowFunc(
|
|
83
|
+
'pikkuWorkflowStepWorker',
|
|
84
|
+
{ func: pikkuWorkflowWorkerFunc },
|
|
85
|
+
'pikku-workflow-step-worker',
|
|
86
|
+
sharedQueueConfig
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
const registerDedicatedQueues = (meta: Record<string, any>) => {
|
|
90
|
+
for (const [queueName, entry] of Object.entries(meta)) {
|
|
91
|
+
if (functions.has(entry.pikkuFuncId)) continue
|
|
92
|
+
if (queueName.startsWith(ORCHESTRATOR_QUEUE_PREFIX)) {
|
|
93
|
+
registerWorkflowFunc(
|
|
94
|
+
entry.pikkuFuncId,
|
|
95
|
+
{ func: pikkuWorkflowOrchestratorFunc },
|
|
96
|
+
queueName
|
|
97
|
+
)
|
|
98
|
+
} else if (queueName.startsWith(STEP_QUEUE_PREFIX)) {
|
|
99
|
+
registerWorkflowFunc(
|
|
100
|
+
entry.pikkuFuncId,
|
|
101
|
+
{ func: pikkuWorkflowWorkerFunc },
|
|
102
|
+
queueName
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (!sharedGroups) {
|
|
109
|
+
registerDedicatedQueues(pikkuState(null, 'queue', 'meta'))
|
|
110
|
+
|
|
111
|
+
const addons = pikkuState(null, 'addons', 'packages')
|
|
112
|
+
for (const [, addon] of addons ?? []) {
|
|
113
|
+
const addonQueueMeta = pikkuState(addon.package, 'queue', 'meta')
|
|
114
|
+
if (addonQueueMeta) {
|
|
115
|
+
registerDedicatedQueues(addonQueueMeta)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
warnOnMissingDedicatedQueues(queueMeta, getLogger)
|
|
121
|
+
|
|
122
|
+
if (!functions.has('pikkuWorkflowSleeper')) {
|
|
123
|
+
addFunction('pikkuWorkflowSleeper', { func: pikkuWorkflowSleeperFunc })
|
|
124
|
+
}
|
|
125
|
+
if (!functionsMeta.pikkuWorkflowSleeper) {
|
|
126
|
+
functionsMeta.pikkuWorkflowSleeper = helperFunctionMeta(
|
|
127
|
+
'pikkuWorkflowSleeper'
|
|
128
|
+
)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const warnOnMissingDedicatedQueues = (
|
|
133
|
+
queueMeta: Record<string, unknown>,
|
|
134
|
+
getLogger: () => Logger | undefined
|
|
135
|
+
) => {
|
|
136
|
+
const workflowCount = Object.keys(
|
|
137
|
+
pikkuState(null, 'workflows', 'meta') ?? {}
|
|
138
|
+
).length
|
|
139
|
+
const dedicatedQueues = Object.keys(queueMeta).filter((name) =>
|
|
140
|
+
name.startsWith(ORCHESTRATOR_QUEUE_PREFIX)
|
|
141
|
+
).length
|
|
142
|
+
if (workflowCount > 0 && dedicatedQueues === 0) {
|
|
143
|
+
getLogger()?.warn?.(
|
|
144
|
+
`[pikku] ${workflowCount} workflow(s) registered but no per-workflow orchestrator queues were found in queue meta. ` +
|
|
145
|
+
`All workflows will share a single orchestrator queue, where one slow step blocks every other workflow behind it. ` +
|
|
146
|
+
`Check that the generated bootstrap imports the queue-workers meta (pikku-queue-workers-wirings-meta.gen.js).`
|
|
147
|
+
)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import type { Logger } from '../../services/logger.js'
|
|
2
|
+
import {
|
|
3
|
+
DEFAULT_STALLED_RUN_LIMIT,
|
|
4
|
+
DEFAULT_STALLED_RUN_MS,
|
|
5
|
+
DEFAULT_UNDISPATCHED_STEP_LIMIT,
|
|
6
|
+
DEFAULT_UNDISPATCHED_STEP_MS,
|
|
7
|
+
REDISPATCH_BACKOFF_MAX_ENTRIES,
|
|
8
|
+
REDISPATCH_BACKOFF_MAX_MS,
|
|
9
|
+
REDISPATCH_BACKOFF_MS,
|
|
10
|
+
} from './workflow-constants.js'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Per-process, advisory record of when a run may next be re-dispatched.
|
|
14
|
+
*
|
|
15
|
+
* Keyed by run rather than by step because the run is the unit of re-drive:
|
|
16
|
+
* `resumeWorkflow` replays the whole run and re-dispatches every step still
|
|
17
|
+
* owed a job, so holding off a single step while resuming its run would
|
|
18
|
+
* suppress nothing.
|
|
19
|
+
*
|
|
20
|
+
* Losing this on restart costs extra dispatches, never correctness.
|
|
21
|
+
*/
|
|
22
|
+
export class RedispatchBackoff {
|
|
23
|
+
private readonly eligibleAt = new Map<string, number>()
|
|
24
|
+
private readonly delays = new Map<string, number>()
|
|
25
|
+
|
|
26
|
+
public isEligible(runId: string, now: number): boolean {
|
|
27
|
+
const at = this.eligibleAt.get(runId)
|
|
28
|
+
return at === undefined || at <= now
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
public note(runId: string, now: number): void {
|
|
32
|
+
const previous = this.delays.get(runId)
|
|
33
|
+
const delay = Math.min(
|
|
34
|
+
previous === undefined ? REDISPATCH_BACKOFF_MS : previous * 2,
|
|
35
|
+
REDISPATCH_BACKOFF_MAX_MS
|
|
36
|
+
)
|
|
37
|
+
// A run that settles is never returned again, so entries are only evicted
|
|
38
|
+
// by this bound — oldest first, which is also least recently re-dispatched.
|
|
39
|
+
if (this.eligibleAt.size >= REDISPATCH_BACKOFF_MAX_ENTRIES) {
|
|
40
|
+
const oldest = this.eligibleAt.keys().next()
|
|
41
|
+
if (!oldest.done) {
|
|
42
|
+
this.eligibleAt.delete(oldest.value)
|
|
43
|
+
this.delays.delete(oldest.value)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
this.delays.set(runId, delay)
|
|
47
|
+
this.eligibleAt.set(runId, now + delay)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
type SweepDeps = {
|
|
52
|
+
resume: (runId: string) => Promise<void>
|
|
53
|
+
logger?: Logger
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const resumeEach = async (
|
|
57
|
+
runIds: Iterable<string>,
|
|
58
|
+
{ resume, logger }: SweepDeps,
|
|
59
|
+
failure: (runId: string, detail: string) => string
|
|
60
|
+
): Promise<string[]> => {
|
|
61
|
+
const succeeded: string[] = []
|
|
62
|
+
for (const runId of runIds) {
|
|
63
|
+
try {
|
|
64
|
+
await resume(runId)
|
|
65
|
+
succeeded.push(runId)
|
|
66
|
+
} catch (err) {
|
|
67
|
+
// One unresumable run must not stop the sweep from recovering the rest.
|
|
68
|
+
logger?.error(
|
|
69
|
+
failure(runId, err instanceof Error ? err.message : String(err))
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return succeeded
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Re-drive runs whose next move was lost, and report which were resumed.
|
|
78
|
+
*
|
|
79
|
+
* Arming a step is two writes to two systems — the step row, then the queue or
|
|
80
|
+
* scheduler job — so a process that dies between them leaves a run that is
|
|
81
|
+
* `running` with nothing in flight. Nothing notices: the run parks on a step
|
|
82
|
+
* that will never complete and never error, so it neither finishes nor fails.
|
|
83
|
+
* (Seen on a `workflow.sleep()`: a deploy restart landed between the sleep
|
|
84
|
+
* step's insert and its timer, parking the run permanently.)
|
|
85
|
+
*
|
|
86
|
+
* Replay is the recovery — `resumeWorkflow` re-orchestrates from persisted step
|
|
87
|
+
* state, and every settled step is memoized, so resuming a run that was not
|
|
88
|
+
* actually stuck costs an orchestration pass and changes nothing. That
|
|
89
|
+
* idempotence is what makes an idle-time heuristic safe here; a run that is
|
|
90
|
+
* legitimately mid-sleep is excluded anyway, since its step is `scheduled`.
|
|
91
|
+
*/
|
|
92
|
+
export const sweepStalledRuns = async (
|
|
93
|
+
findStalledRunIds: (before: Date, limit: number) => Promise<string[]>,
|
|
94
|
+
options: { stalledAfterMs?: number; limit?: number } | undefined,
|
|
95
|
+
deps: SweepDeps
|
|
96
|
+
): Promise<{ resumed: string[] }> => {
|
|
97
|
+
const before = new Date(
|
|
98
|
+
Date.now() - (options?.stalledAfterMs ?? DEFAULT_STALLED_RUN_MS)
|
|
99
|
+
)
|
|
100
|
+
const runIds = await findStalledRunIds(
|
|
101
|
+
before,
|
|
102
|
+
options?.limit ?? DEFAULT_STALLED_RUN_LIMIT
|
|
103
|
+
)
|
|
104
|
+
return {
|
|
105
|
+
resumed: await resumeEach(
|
|
106
|
+
runIds,
|
|
107
|
+
deps,
|
|
108
|
+
(runId, detail) =>
|
|
109
|
+
`Failed to resume stalled workflow run ${runId}: ${detail}`
|
|
110
|
+
),
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Re-drive steps whose dispatch was lost, and report which runs were nudged.
|
|
116
|
+
*
|
|
117
|
+
* The step row is the outbox record and this is the relay. Age is the only
|
|
118
|
+
* signal available — a step `pending` because its dispatch was lost is
|
|
119
|
+
* indistinguishable from one whose job is merely still queued — so a step past
|
|
120
|
+
* `undispatchedAfterMs` is re-dispatched regardless, and correctness rests on
|
|
121
|
+
* the claim in `executeWorkflowStepInner` rather than on the guess being right.
|
|
122
|
+
* A redundant dispatch costs one queue message: the loser reads `running` and
|
|
123
|
+
* returns without invoking anything.
|
|
124
|
+
*
|
|
125
|
+
* Re-dispatches back off per run (doubling from 30s, capped at 10m) so a
|
|
126
|
+
* genuine queue backlog is not amplified by a tick that keeps firing at the
|
|
127
|
+
* steps the backlog is already delaying.
|
|
128
|
+
*/
|
|
129
|
+
export const sweepUndispatchedSteps = async (
|
|
130
|
+
findUndispatchedSteps: (
|
|
131
|
+
before: Date,
|
|
132
|
+
limit: number
|
|
133
|
+
) => Promise<Array<{ runId: string; stepId: string }>>,
|
|
134
|
+
backoff: RedispatchBackoff,
|
|
135
|
+
options: { undispatchedAfterMs?: number; limit?: number } | undefined,
|
|
136
|
+
deps: SweepDeps
|
|
137
|
+
): Promise<{ redispatched: string[] }> => {
|
|
138
|
+
const before = new Date(
|
|
139
|
+
Date.now() - (options?.undispatchedAfterMs ?? DEFAULT_UNDISPATCHED_STEP_MS)
|
|
140
|
+
)
|
|
141
|
+
const steps = await findUndispatchedSteps(
|
|
142
|
+
before,
|
|
143
|
+
options?.limit ?? DEFAULT_UNDISPATCHED_STEP_LIMIT
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
const now = Date.now()
|
|
147
|
+
const runIds = new Set<string>()
|
|
148
|
+
for (const { runId } of steps) {
|
|
149
|
+
if (!backoff.isEligible(runId, now)) continue
|
|
150
|
+
backoff.note(runId, now)
|
|
151
|
+
runIds.add(runId)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
redispatched: await resumeEach(
|
|
156
|
+
runIds,
|
|
157
|
+
deps,
|
|
158
|
+
(runId, detail) =>
|
|
159
|
+
`Failed to re-dispatch workflow run ${runId}: ${detail}`
|
|
160
|
+
),
|
|
161
|
+
}
|
|
162
|
+
}
|
|
@@ -3,7 +3,7 @@ import assert from 'node:assert/strict'
|
|
|
3
3
|
|
|
4
4
|
import { InMemoryWorkflowService } from '../../services/in-memory-workflow-service.js'
|
|
5
5
|
import { pikkuState } from '../../pikku-state.js'
|
|
6
|
-
import { DEFAULT_STEP_RETRIES } from './
|
|
6
|
+
import { DEFAULT_STEP_RETRIES } from './workflow-constants.js'
|
|
7
7
|
import type { PikkuWorkflowService } from './pikku-workflow-service.js'
|
|
8
8
|
import { deriveInvocationId } from './workflow-invocation-id.js'
|
|
9
9
|
import type { WorkflowStepOptions } from './workflow.types.js'
|