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