@nodetool-ai/websocket 0.7.0-rc.22 → 0.8.1
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/README.md +50 -0
- package/dist/.nodetool-build-stamp +1 -0
- package/dist/agent/run-node-tool.d.ts +39 -0
- package/dist/agent/run-node-tool.d.ts.map +1 -0
- package/dist/agent/run-node-tool.js +55 -0
- package/dist/agent/run-node-tool.js.map +1 -0
- package/dist/chat-prediction-forwarder.d.ts +13 -0
- package/dist/chat-prediction-forwarder.d.ts.map +1 -0
- package/dist/chat-prediction-forwarder.js +22 -0
- package/dist/chat-prediction-forwarder.js.map +1 -0
- package/dist/chat-tool-call-repair.d.ts +45 -0
- package/dist/chat-tool-call-repair.d.ts.map +1 -0
- package/dist/chat-tool-call-repair.js +100 -0
- package/dist/chat-tool-call-repair.js.map +1 -0
- package/dist/chat-turn-registry.d.ts +169 -0
- package/dist/chat-turn-registry.d.ts.map +1 -0
- package/dist/chat-turn-registry.js +326 -0
- package/dist/chat-turn-registry.js.map +1 -0
- package/dist/collection-api.d.ts +2 -2
- package/dist/collection-api.d.ts.map +1 -1
- package/dist/collection-api.js +44 -13
- package/dist/collection-api.js.map +1 -1
- package/dist/configured-providers.d.ts +52 -0
- package/dist/configured-providers.d.ts.map +1 -0
- package/dist/configured-providers.js +85 -0
- package/dist/configured-providers.js.map +1 -0
- package/dist/cors.d.ts +22 -0
- package/dist/cors.d.ts.map +1 -0
- package/dist/cors.js +91 -0
- package/dist/cors.js.map +1 -0
- package/dist/credit-gate.d.ts +36 -0
- package/dist/credit-gate.d.ts.map +1 -0
- package/dist/credit-gate.js +76 -0
- package/dist/credit-gate.js.map +1 -0
- package/dist/custom-providers.d.ts +40 -0
- package/dist/custom-providers.d.ts.map +1 -0
- package/dist/custom-providers.js +134 -0
- package/dist/custom-providers.js.map +1 -0
- package/dist/debug-sessions.d.ts +10 -0
- package/dist/debug-sessions.d.ts.map +1 -0
- package/dist/debug-sessions.js +9 -0
- package/dist/debug-sessions.js.map +1 -0
- package/dist/drain.d.ts +34 -0
- package/dist/drain.d.ts.map +1 -0
- package/dist/drain.js +68 -0
- package/dist/drain.js.map +1 -0
- package/dist/e2e-server.d.ts +30 -0
- package/dist/e2e-server.d.ts.map +1 -0
- package/dist/e2e-server.js +111 -0
- package/dist/e2e-server.js.map +1 -0
- package/dist/example-workflows.d.ts +20 -0
- package/dist/example-workflows.d.ts.map +1 -0
- package/dist/example-workflows.js +123 -0
- package/dist/example-workflows.js.map +1 -0
- package/dist/extension-cdp-bridge.d.ts +85 -0
- package/dist/extension-cdp-bridge.d.ts.map +1 -0
- package/dist/extension-cdp-bridge.js +163 -0
- package/dist/extension-cdp-bridge.js.map +1 -0
- package/dist/external-mcp.d.ts +58 -0
- package/dist/external-mcp.d.ts.map +1 -0
- package/dist/external-mcp.js +288 -0
- package/dist/external-mcp.js.map +1 -0
- package/dist/fake-runtime.d.ts +114 -0
- package/dist/fake-runtime.d.ts.map +1 -0
- package/dist/fake-runtime.js +270 -0
- package/dist/fake-runtime.js.map +1 -0
- package/dist/file-api.d.ts +4 -7
- package/dist/file-api.d.ts.map +1 -1
- package/dist/file-api.js +122 -39
- package/dist/file-api.js.map +1 -1
- package/dist/frontend-renderer-registry.d.ts +75 -0
- package/dist/frontend-renderer-registry.d.ts.map +1 -0
- package/dist/frontend-renderer-registry.js +111 -0
- package/dist/frontend-renderer-registry.js.map +1 -0
- package/dist/generation-recovery.d.ts +13 -0
- package/dist/generation-recovery.d.ts.map +1 -0
- package/dist/generation-recovery.js +197 -0
- package/dist/generation-recovery.js.map +1 -0
- package/dist/headless-job-runner.d.ts +71 -0
- package/dist/headless-job-runner.d.ts.map +1 -0
- package/dist/headless-job-runner.js +198 -0
- package/dist/headless-job-runner.js.map +1 -0
- package/dist/http-api.d.ts +157 -24
- package/dist/http-api.d.ts.map +1 -1
- package/dist/http-api.js +613 -646
- package/dist/http-api.js.map +1 -1
- package/dist/http-body-schemas.d.ts +109 -0
- package/dist/http-body-schemas.d.ts.map +1 -0
- package/dist/http-body-schemas.js +146 -0
- package/dist/http-body-schemas.js.map +1 -0
- package/dist/index.d.ts +11 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -3
- package/dist/index.js.map +1 -1
- package/dist/job-control.d.ts +57 -0
- package/dist/job-control.d.ts.map +1 -0
- package/dist/job-control.js +134 -0
- package/dist/job-control.js.map +1 -0
- package/dist/job-queue.d.ts +47 -0
- package/dist/job-queue.d.ts.map +1 -0
- package/dist/job-queue.js +52 -0
- package/dist/job-queue.js.map +1 -0
- package/dist/job-run-registry.d.ts +176 -0
- package/dist/job-run-registry.d.ts.map +1 -0
- package/dist/job-run-registry.js +312 -0
- package/dist/job-run-registry.js.map +1 -0
- package/dist/lib/app-build-service.d.ts +16 -0
- package/dist/lib/app-build-service.d.ts.map +1 -0
- package/dist/lib/app-build-service.js +39 -0
- package/dist/lib/app-build-service.js.map +1 -0
- package/dist/lib/app-debug-service.d.ts +16 -0
- package/dist/lib/app-debug-service.d.ts.map +1 -0
- package/dist/lib/app-debug-service.js +48 -0
- package/dist/lib/app-debug-service.js.map +1 -0
- package/dist/lib/app-deployment-service.d.ts +82 -0
- package/dist/lib/app-deployment-service.d.ts.map +1 -0
- package/dist/lib/app-deployment-service.js +282 -0
- package/dist/lib/app-deployment-service.js.map +1 -0
- package/dist/lib/app-run-server.d.ts +7 -0
- package/dist/lib/app-run-server.d.ts.map +1 -0
- package/dist/lib/app-run-server.js +6 -0
- package/dist/lib/app-run-server.js.map +1 -0
- package/dist/lib/app-session-scope.d.ts +91 -0
- package/dist/lib/app-session-scope.d.ts.map +1 -0
- package/dist/lib/app-session-scope.js +91 -0
- package/dist/lib/app-session-scope.js.map +1 -0
- package/dist/lib/applications-service.d.ts +173 -0
- package/dist/lib/applications-service.d.ts.map +1 -0
- package/dist/lib/applications-service.js +491 -0
- package/dist/lib/applications-service.js.map +1 -0
- package/dist/lib/asset-duration.d.ts +7 -0
- package/dist/lib/asset-duration.d.ts.map +1 -0
- package/dist/lib/asset-duration.js +24 -0
- package/dist/lib/asset-duration.js.map +1 -0
- package/dist/lib/asset-export.d.ts +34 -0
- package/dist/lib/asset-export.d.ts.map +1 -0
- package/dist/lib/asset-export.js +113 -0
- package/dist/lib/asset-export.js.map +1 -0
- package/dist/lib/asset-model-interface.d.ts +41 -0
- package/dist/lib/asset-model-interface.d.ts.map +1 -0
- package/dist/lib/asset-model-interface.js +74 -0
- package/dist/lib/asset-model-interface.js.map +1 -0
- package/dist/lib/asset-paths.d.ts +52 -0
- package/dist/lib/asset-paths.d.ts.map +1 -1
- package/dist/lib/asset-paths.js +145 -1
- package/dist/lib/asset-paths.js.map +1 -1
- package/dist/lib/asset-response.d.ts +12 -0
- package/dist/lib/asset-response.d.ts.map +1 -0
- package/dist/lib/asset-response.js +88 -0
- package/dist/lib/asset-response.js.map +1 -0
- package/dist/lib/audio-peaks.d.ts +23 -0
- package/dist/lib/audio-peaks.d.ts.map +1 -0
- package/dist/lib/audio-peaks.js +331 -0
- package/dist/lib/audio-peaks.js.map +1 -0
- package/dist/lib/bridge.d.ts +9 -1
- package/dist/lib/bridge.d.ts.map +1 -1
- package/dist/lib/bridge.js +27 -4
- package/dist/lib/bridge.js.map +1 -1
- package/dist/lib/compression.d.ts +3 -0
- package/dist/lib/compression.d.ts.map +1 -0
- package/dist/lib/compression.js +3 -0
- package/dist/lib/compression.js.map +1 -0
- package/dist/lib/document-model-interfaces.d.ts +30 -0
- package/dist/lib/document-model-interfaces.d.ts.map +1 -0
- package/dist/lib/document-model-interfaces.js +295 -0
- package/dist/lib/document-model-interfaces.js.map +1 -0
- package/dist/lib/env-config.d.ts +9 -0
- package/dist/lib/env-config.d.ts.map +1 -0
- package/dist/lib/env-config.js +18 -0
- package/dist/lib/env-config.js.map +1 -0
- package/dist/lib/example-apps.d.ts +43 -0
- package/dist/lib/example-apps.d.ts.map +1 -0
- package/dist/lib/example-apps.js +145 -0
- package/dist/lib/example-apps.js.map +1 -0
- package/dist/lib/example-games.d.ts +5 -0
- package/dist/lib/example-games.d.ts.map +1 -0
- package/dist/lib/example-games.js +13 -0
- package/dist/lib/example-games.js.map +1 -0
- package/dist/lib/example-recipes.d.ts +33 -0
- package/dist/lib/example-recipes.d.ts.map +1 -0
- package/dist/lib/example-recipes.js +213 -0
- package/dist/lib/example-recipes.js.map +1 -0
- package/dist/lib/example-storyboards.d.ts +40 -0
- package/dist/lib/example-storyboards.d.ts.map +1 -0
- package/dist/lib/example-storyboards.js +131 -0
- package/dist/lib/example-storyboards.js.map +1 -0
- package/dist/lib/example-thumbnail.d.ts +21 -0
- package/dist/lib/example-thumbnail.d.ts.map +1 -0
- package/dist/lib/example-thumbnail.js +50 -0
- package/dist/lib/example-thumbnail.js.map +1 -0
- package/dist/lib/example-timelines.d.ts +9 -0
- package/dist/lib/example-timelines.d.ts.map +1 -0
- package/dist/lib/example-timelines.js +25 -0
- package/dist/lib/example-timelines.js.map +1 -0
- package/dist/lib/extension-dist.d.ts +24 -0
- package/dist/lib/extension-dist.d.ts.map +1 -0
- package/dist/lib/extension-dist.js +67 -0
- package/dist/lib/extension-dist.js.map +1 -0
- package/dist/lib/external-asset-lookup.d.ts +30 -0
- package/dist/lib/external-asset-lookup.d.ts.map +1 -0
- package/dist/lib/external-asset-lookup.js +99 -0
- package/dist/lib/external-asset-lookup.js.map +1 -0
- package/dist/lib/external-assets.d.ts +13 -0
- package/dist/lib/external-assets.d.ts.map +1 -0
- package/dist/lib/external-assets.js +46 -0
- package/dist/lib/external-assets.js.map +1 -0
- package/dist/lib/fly-replay.d.ts +34 -0
- package/dist/lib/fly-replay.d.ts.map +1 -0
- package/dist/lib/fly-replay.js +111 -0
- package/dist/lib/fly-replay.js.map +1 -0
- package/dist/lib/http-rate-limit.d.ts +32 -0
- package/dist/lib/http-rate-limit.d.ts.map +1 -0
- package/dist/lib/http-rate-limit.js +47 -0
- package/dist/lib/http-rate-limit.js.map +1 -0
- package/dist/lib/instance-id.d.ts +25 -0
- package/dist/lib/instance-id.d.ts.map +1 -0
- package/dist/lib/instance-id.js +44 -0
- package/dist/lib/instance-id.js.map +1 -0
- package/dist/lib/link-codes.d.ts +82 -0
- package/dist/lib/link-codes.d.ts.map +1 -0
- package/dist/lib/link-codes.js +100 -0
- package/dist/lib/link-codes.js.map +1 -0
- package/dist/lib/local-asset-upload.d.ts +5 -0
- package/dist/lib/local-asset-upload.d.ts.map +1 -0
- package/dist/lib/local-asset-upload.js +69 -0
- package/dist/lib/local-asset-upload.js.map +1 -0
- package/dist/lib/local-file-access.d.ts +31 -0
- package/dist/lib/local-file-access.d.ts.map +1 -0
- package/dist/lib/local-file-access.js +169 -0
- package/dist/lib/local-file-access.js.map +1 -0
- package/dist/lib/localhost-trust.d.ts +59 -0
- package/dist/lib/localhost-trust.d.ts.map +1 -0
- package/dist/lib/localhost-trust.js +139 -0
- package/dist/lib/localhost-trust.js.map +1 -0
- package/dist/lib/mcp-mount.d.ts +25 -0
- package/dist/lib/mcp-mount.d.ts.map +1 -0
- package/dist/lib/mcp-mount.js +32 -0
- package/dist/lib/mcp-mount.js.map +1 -0
- package/dist/lib/media-size.d.ts +21 -0
- package/dist/lib/media-size.d.ts.map +1 -0
- package/dist/lib/media-size.js +48 -0
- package/dist/lib/media-size.js.map +1 -0
- package/dist/lib/media.d.ts +38 -0
- package/dist/lib/media.d.ts.map +1 -0
- package/dist/lib/media.js +146 -0
- package/dist/lib/media.js.map +1 -0
- package/dist/lib/package-asset-export.d.ts +83 -0
- package/dist/lib/package-asset-export.d.ts.map +1 -0
- package/dist/lib/package-asset-export.js +212 -0
- package/dist/lib/package-asset-export.js.map +1 -0
- package/dist/lib/package-assets.d.ts +41 -0
- package/dist/lib/package-assets.d.ts.map +1 -0
- package/dist/lib/package-assets.js +155 -0
- package/dist/lib/package-assets.js.map +1 -0
- package/dist/lib/project-delete.d.ts +11 -0
- package/dist/lib/project-delete.d.ts.map +1 -0
- package/dist/lib/project-delete.js +67 -0
- package/dist/lib/project-delete.js.map +1 -0
- package/dist/lib/project-document-copy.d.ts +31 -0
- package/dist/lib/project-document-copy.d.ts.map +1 -0
- package/dist/lib/project-document-copy.js +473 -0
- package/dist/lib/project-document-copy.js.map +1 -0
- package/dist/lib/public-app-assets.d.ts +8 -0
- package/dist/lib/public-app-assets.d.ts.map +1 -0
- package/dist/lib/public-app-assets.js +77 -0
- package/dist/lib/public-app-assets.js.map +1 -0
- package/dist/lib/public-routes.d.ts +95 -0
- package/dist/lib/public-routes.d.ts.map +1 -0
- package/dist/lib/public-routes.js +154 -0
- package/dist/lib/public-routes.js.map +1 -0
- package/dist/lib/runtime-detection.d.ts +17 -0
- package/dist/lib/runtime-detection.d.ts.map +1 -0
- package/dist/lib/runtime-detection.js +90 -0
- package/dist/lib/runtime-detection.js.map +1 -0
- package/dist/lib/storage-access.d.ts +21 -0
- package/dist/lib/storage-access.d.ts.map +1 -0
- package/dist/lib/storage-access.js +74 -0
- package/dist/lib/storage-access.js.map +1 -0
- package/dist/lib/storage.d.ts +9 -0
- package/dist/lib/storage.d.ts.map +1 -0
- package/dist/lib/storage.js +28 -0
- package/dist/lib/storage.js.map +1 -0
- package/dist/lib/storyboard-export.d.ts +64 -0
- package/dist/lib/storyboard-export.d.ts.map +1 -0
- package/dist/lib/storyboard-export.js +183 -0
- package/dist/lib/storyboard-export.js.map +1 -0
- package/dist/lib/style-presets.d.ts +34 -0
- package/dist/lib/style-presets.d.ts.map +1 -0
- package/dist/lib/style-presets.js +62 -0
- package/dist/lib/style-presets.js.map +1 -0
- package/dist/lib/temp-url-resolver.d.ts +35 -0
- package/dist/lib/temp-url-resolver.d.ts.map +1 -0
- package/dist/lib/temp-url-resolver.js +88 -0
- package/dist/lib/temp-url-resolver.js.map +1 -0
- package/dist/lib/thumbnail.d.ts +74 -0
- package/dist/lib/thumbnail.d.ts.map +1 -0
- package/dist/lib/thumbnail.js +302 -0
- package/dist/lib/thumbnail.js.map +1 -0
- package/dist/lib/timeline-bundle.d.ts +1689 -0
- package/dist/lib/timeline-bundle.d.ts.map +1 -0
- package/dist/lib/timeline-bundle.js +330 -0
- package/dist/lib/timeline-bundle.js.map +1 -0
- package/dist/lib/video-proxy.d.ts +114 -0
- package/dist/lib/video-proxy.d.ts.map +1 -0
- package/dist/lib/video-proxy.js +545 -0
- package/dist/lib/video-proxy.js.map +1 -0
- package/dist/lib/wire-values.d.ts +31 -0
- package/dist/lib/wire-values.d.ts.map +1 -0
- package/dist/lib/wire-values.js +49 -0
- package/dist/lib/wire-values.js.map +1 -0
- package/dist/lib/workflow-bundle.d.ts +102 -0
- package/dist/lib/workflow-bundle.d.ts.map +1 -0
- package/dist/lib/workflow-bundle.js +306 -0
- package/dist/lib/workflow-bundle.js.map +1 -0
- package/dist/lib/workflow-service.d.ts +82 -0
- package/dist/lib/workflow-service.d.ts.map +1 -0
- package/dist/lib/workflow-service.js +194 -0
- package/dist/lib/workflow-service.js.map +1 -0
- package/dist/lib/workflow-workspace.d.ts +20 -0
- package/dist/lib/workflow-workspace.d.ts.map +1 -0
- package/dist/lib/workflow-workspace.js +65 -0
- package/dist/lib/workflow-workspace.js.map +1 -0
- package/dist/lib/ws-connection-health.d.ts +10 -0
- package/dist/lib/ws-connection-health.d.ts.map +1 -0
- package/dist/lib/ws-connection-health.js +38 -0
- package/dist/lib/ws-connection-health.js.map +1 -0
- package/dist/lib/ws-rate-limit.d.ts +22 -0
- package/dist/lib/ws-rate-limit.d.ts.map +1 -0
- package/dist/lib/ws-rate-limit.js +45 -0
- package/dist/lib/ws-rate-limit.js.map +1 -0
- package/dist/lib/ws-upgrade.d.ts +40 -0
- package/dist/lib/ws-upgrade.d.ts.map +1 -0
- package/dist/lib/ws-upgrade.js +66 -0
- package/dist/lib/ws-upgrade.js.map +1 -0
- package/dist/lib/zip-stream.d.ts +46 -0
- package/dist/lib/zip-stream.d.ts.map +1 -0
- package/dist/lib/zip-stream.js +140 -0
- package/dist/lib/zip-stream.js.map +1 -0
- package/dist/mcp-agent-tools.d.ts +56 -0
- package/dist/mcp-agent-tools.d.ts.map +1 -0
- package/dist/mcp-agent-tools.js +1216 -0
- package/dist/mcp-agent-tools.js.map +1 -0
- package/dist/mcp-server.d.ts +56 -7
- package/dist/mcp-server.d.ts.map +1 -1
- package/dist/mcp-server.js +255 -762
- package/dist/mcp-server.js.map +1 -1
- package/dist/mcp-tool-deps.d.ts +20 -0
- package/dist/mcp-tool-deps.d.ts.map +1 -0
- package/dist/mcp-tool-deps.js +61 -0
- package/dist/mcp-tool-deps.js.map +1 -0
- package/dist/messagepack.d.ts +7 -0
- package/dist/messagepack.d.ts.map +1 -0
- package/dist/messagepack.js +19 -0
- package/dist/messagepack.js.map +1 -0
- package/dist/model-download-runtime.d.ts +9 -0
- package/dist/model-download-runtime.d.ts.map +1 -0
- package/dist/model-download-runtime.js +77 -0
- package/dist/model-download-runtime.js.map +1 -0
- package/dist/model-execution-availability.d.ts +14 -0
- package/dist/model-execution-availability.d.ts.map +1 -0
- package/dist/model-execution-availability.js +161 -0
- package/dist/model-execution-availability.js.map +1 -0
- package/dist/models-api.d.ts +51 -5
- package/dist/models-api.d.ts.map +1 -1
- package/dist/models-api.js +160 -690
- package/dist/models-api.js.map +1 -1
- package/dist/node-registry-setup.d.ts +84 -0
- package/dist/node-registry-setup.d.ts.map +1 -0
- package/dist/node-registry-setup.js +256 -0
- package/dist/node-registry-setup.js.map +1 -0
- package/dist/oauth/cimd.d.ts +28 -0
- package/dist/oauth/cimd.d.ts.map +1 -0
- package/dist/oauth/cimd.js +177 -0
- package/dist/oauth/cimd.js.map +1 -0
- package/dist/oauth/gate.d.ts +38 -0
- package/dist/oauth/gate.d.ts.map +1 -0
- package/dist/oauth/gate.js +77 -0
- package/dist/oauth/gate.js.map +1 -0
- package/dist/oauth/metadata.d.ts +35 -0
- package/dist/oauth/metadata.d.ts.map +1 -0
- package/dist/oauth/metadata.js +56 -0
- package/dist/oauth/metadata.js.map +1 -0
- package/dist/oauth/pending-store.d.ts +79 -0
- package/dist/oauth/pending-store.d.ts.map +1 -0
- package/dist/oauth/pending-store.js +133 -0
- package/dist/oauth/pending-store.js.map +1 -0
- package/dist/oauth/pkce.d.ts +16 -0
- package/dist/oauth/pkce.d.ts.map +1 -0
- package/dist/oauth/pkce.js +29 -0
- package/dist/oauth/pkce.js.map +1 -0
- package/dist/oauth/validate.d.ts +25 -0
- package/dist/oauth/validate.d.ts.map +1 -0
- package/dist/oauth/validate.js +62 -0
- package/dist/oauth/validate.js.map +1 -0
- package/dist/oauth/www-authenticate.d.ts +9 -0
- package/dist/oauth/www-authenticate.d.ts.map +1 -0
- package/dist/oauth/www-authenticate.js +33 -0
- package/dist/oauth/www-authenticate.js.map +1 -0
- package/dist/oauth-api.d.ts +19 -0
- package/dist/oauth-api.d.ts.map +1 -1
- package/dist/oauth-api.js +531 -12
- package/dist/oauth-api.js.map +1 -1
- package/dist/openai-api.d.ts.map +1 -1
- package/dist/openai-api.js +4 -9
- package/dist/openai-api.js.map +1 -1
- package/dist/pack-snapshot.d.ts +24 -0
- package/dist/pack-snapshot.d.ts.map +1 -0
- package/dist/pack-snapshot.js +35 -0
- package/dist/pack-snapshot.js.map +1 -0
- package/dist/plugins/websocket.d.ts +21 -4
- package/dist/plugins/websocket.d.ts.map +1 -1
- package/dist/plugins/websocket.js +136 -290
- package/dist/plugins/websocket.js.map +1 -1
- package/dist/resolve-media-urls.d.ts +17 -3
- package/dist/resolve-media-urls.d.ts.map +1 -1
- package/dist/resolve-media-urls.js +117 -31
- package/dist/resolve-media-urls.js.map +1 -1
- package/dist/resource-events.d.ts +28 -0
- package/dist/resource-events.d.ts.map +1 -0
- package/dist/resource-events.js +20 -0
- package/dist/resource-events.js.map +1 -0
- package/dist/routes/account.d.ts +100 -0
- package/dist/routes/account.d.ts.map +1 -0
- package/dist/routes/account.js +197 -0
- package/dist/routes/account.js.map +1 -0
- package/dist/routes/applications.d.ts +36 -0
- package/dist/routes/applications.d.ts.map +1 -0
- package/dist/routes/applications.js +183 -0
- package/dist/routes/applications.js.map +1 -0
- package/dist/routes/assets.d.ts +3 -3
- package/dist/routes/assets.d.ts.map +1 -1
- package/dist/routes/assets.js +78 -44
- package/dist/routes/assets.js.map +1 -1
- package/dist/routes/atlascloud-webhook.d.ts +93 -0
- package/dist/routes/atlascloud-webhook.d.ts.map +1 -0
- package/dist/routes/atlascloud-webhook.js +369 -0
- package/dist/routes/atlascloud-webhook.js.map +1 -0
- package/dist/routes/collections.d.ts.map +1 -1
- package/dist/routes/config.d.ts +26 -0
- package/dist/routes/config.d.ts.map +1 -0
- package/dist/routes/config.js +64 -0
- package/dist/routes/config.js.map +1 -0
- package/dist/routes/documents.d.ts +27 -0
- package/dist/routes/documents.d.ts.map +1 -0
- package/dist/routes/documents.js +155 -0
- package/dist/routes/documents.js.map +1 -0
- package/dist/routes/fal-credits.d.ts +4 -0
- package/dist/routes/fal-credits.d.ts.map +1 -0
- package/dist/routes/fal-credits.js +101 -0
- package/dist/routes/fal-credits.js.map +1 -0
- package/dist/routes/fal-pricing-estimate.d.ts +4 -0
- package/dist/routes/fal-pricing-estimate.d.ts.map +1 -0
- package/dist/routes/fal-pricing-estimate.js +114 -0
- package/dist/routes/fal-pricing-estimate.js.map +1 -0
- package/dist/routes/fal-pricing.d.ts +4 -0
- package/dist/routes/fal-pricing.d.ts.map +1 -0
- package/dist/routes/fal-pricing.js +93 -0
- package/dist/routes/fal-pricing.js.map +1 -0
- package/dist/routes/fal-webhook.d.ts +102 -0
- package/dist/routes/fal-webhook.d.ts.map +1 -0
- package/dist/routes/fal-webhook.js +379 -0
- package/dist/routes/fal-webhook.js.map +1 -0
- package/dist/routes/files.d.ts.map +1 -1
- package/dist/routes/files.js +4 -3
- package/dist/routes/files.js.map +1 -1
- package/dist/routes/health.d.ts +1 -0
- package/dist/routes/health.d.ts.map +1 -1
- package/dist/routes/health.js +41 -7
- package/dist/routes/health.js.map +1 -1
- package/dist/routes/integrations.d.ts +39 -0
- package/dist/routes/integrations.d.ts.map +1 -0
- package/dist/routes/integrations.js +211 -0
- package/dist/routes/integrations.js.map +1 -0
- package/dist/routes/js-scripts.d.ts +26 -0
- package/dist/routes/js-scripts.d.ts.map +1 -0
- package/dist/routes/js-scripts.js +116 -0
- package/dist/routes/js-scripts.js.map +1 -0
- package/dist/routes/kie-credits.d.ts +4 -0
- package/dist/routes/kie-credits.d.ts.map +1 -0
- package/dist/routes/kie-credits.js +93 -0
- package/dist/routes/kie-credits.js.map +1 -0
- package/dist/routes/kie-pricing.d.ts +4 -0
- package/dist/routes/kie-pricing.d.ts.map +1 -0
- package/dist/routes/kie-pricing.js +45 -0
- package/dist/routes/kie-pricing.js.map +1 -0
- package/dist/routes/kie-webhook.d.ts +4 -0
- package/dist/routes/kie-webhook.d.ts.map +1 -0
- package/dist/routes/kie-webhook.js +70 -0
- package/dist/routes/kie-webhook.js.map +1 -0
- package/dist/routes/nodes.d.ts.map +1 -1
- package/dist/routes/nodes.js +6 -59
- package/dist/routes/nodes.js.map +1 -1
- package/dist/routes/oauth-as.d.ts +30 -0
- package/dist/routes/oauth-as.d.ts.map +1 -0
- package/dist/routes/oauth-as.js +412 -0
- package/dist/routes/oauth-as.js.map +1 -0
- package/dist/routes/oauth.d.ts.map +1 -1
- package/dist/routes/openai.d.ts.map +1 -1
- package/dist/routes/public-apps.d.ts +27 -0
- package/dist/routes/public-apps.d.ts.map +1 -0
- package/dist/routes/public-apps.js +99 -0
- package/dist/routes/public-apps.js.map +1 -0
- package/dist/routes/sandbox-modules.d.ts +17 -0
- package/dist/routes/sandbox-modules.d.ts.map +1 -0
- package/dist/routes/sandbox-modules.js +85 -0
- package/dist/routes/sandbox-modules.js.map +1 -0
- package/dist/routes/sdk-v1.d.ts +24 -0
- package/dist/routes/sdk-v1.d.ts.map +1 -0
- package/dist/routes/sdk-v1.js +320 -0
- package/dist/routes/sdk-v1.js.map +1 -0
- package/dist/routes/storage.d.ts.map +1 -1
- package/dist/routes/storyboards.d.ts +16 -0
- package/dist/routes/storyboards.d.ts.map +1 -0
- package/dist/routes/storyboards.js +97 -0
- package/dist/routes/storyboards.js.map +1 -0
- package/dist/routes/timeline-animations.d.ts +23 -0
- package/dist/routes/timeline-animations.d.ts.map +1 -0
- package/dist/routes/timeline-animations.js +99 -0
- package/dist/routes/timeline-animations.js.map +1 -0
- package/dist/routes/timeline-audio-bake.d.ts +51 -0
- package/dist/routes/timeline-audio-bake.d.ts.map +1 -0
- package/dist/routes/timeline-audio-bake.js +110 -0
- package/dist/routes/timeline-audio-bake.js.map +1 -0
- package/dist/routes/timeline-fonts.d.ts +29 -0
- package/dist/routes/timeline-fonts.d.ts.map +1 -0
- package/dist/routes/timeline-fonts.js +64 -0
- package/dist/routes/timeline-fonts.js.map +1 -0
- package/dist/routes/timeline-isolate-subject.d.ts +55 -0
- package/dist/routes/timeline-isolate-subject.d.ts.map +1 -0
- package/dist/routes/timeline-isolate-subject.js +128 -0
- package/dist/routes/timeline-isolate-subject.js.map +1 -0
- package/dist/routes/timeline-track-object.d.ts +39 -0
- package/dist/routes/timeline-track-object.d.ts.map +1 -0
- package/dist/routes/timeline-track-object.js +97 -0
- package/dist/routes/timeline-track-object.js.map +1 -0
- package/dist/routes/timelines.d.ts +18 -0
- package/dist/routes/timelines.d.ts.map +1 -0
- package/dist/routes/timelines.js +191 -0
- package/dist/routes/timelines.js.map +1 -0
- package/dist/routes/workflows.d.ts +6 -9
- package/dist/routes/workflows.d.ts.map +1 -1
- package/dist/routes/workflows.js +100 -11
- package/dist/routes/workflows.js.map +1 -1
- package/dist/routes/workspace.d.ts.map +1 -1
- package/dist/run-supervisor.d.ts +30 -0
- package/dist/run-supervisor.d.ts.map +1 -0
- package/dist/run-supervisor.js +86 -0
- package/dist/run-supervisor.js.map +1 -0
- package/dist/sandbox-catalog.d.ts +28 -0
- package/dist/sandbox-catalog.d.ts.map +1 -0
- package/dist/sandbox-catalog.js +74 -0
- package/dist/sandbox-catalog.js.map +1 -0
- package/dist/screenshot-projects.d.ts +27 -0
- package/dist/screenshot-projects.d.ts.map +1 -0
- package/dist/screenshot-projects.js +880 -0
- package/dist/screenshot-projects.js.map +1 -0
- package/dist/screenshot-server.js +947 -43
- package/dist/screenshot-server.js.map +1 -1
- package/dist/sdk/sdk-cached-model-inventory.d.ts +21 -0
- package/dist/sdk/sdk-cached-model-inventory.d.ts.map +1 -0
- package/dist/sdk/sdk-cached-model-inventory.js +51 -0
- package/dist/sdk/sdk-cached-model-inventory.js.map +1 -0
- package/dist/sdk/sdk-capabilities-service.d.ts +32 -0
- package/dist/sdk/sdk-capabilities-service.d.ts.map +1 -0
- package/dist/sdk/sdk-capabilities-service.js +47 -0
- package/dist/sdk/sdk-capabilities-service.js.map +1 -0
- package/dist/sdk/sdk-execution-readiness-probe.d.ts +19 -0
- package/dist/sdk/sdk-execution-readiness-probe.d.ts.map +1 -0
- package/dist/sdk/sdk-execution-readiness-probe.js +90 -0
- package/dist/sdk/sdk-execution-readiness-probe.js.map +1 -0
- package/dist/sdk/sdk-execution-target-readiness.d.ts +15 -0
- package/dist/sdk/sdk-execution-target-readiness.d.ts.map +1 -0
- package/dist/sdk/sdk-execution-target-readiness.js +50 -0
- package/dist/sdk/sdk-execution-target-readiness.js.map +1 -0
- package/dist/sdk/sdk-feature-flags.d.ts +18 -0
- package/dist/sdk/sdk-feature-flags.d.ts.map +1 -0
- package/dist/sdk/sdk-feature-flags.js +30 -0
- package/dist/sdk/sdk-feature-flags.js.map +1 -0
- package/dist/sdk/sdk-huggingface-download-state.d.ts +15 -0
- package/dist/sdk/sdk-huggingface-download-state.d.ts.map +1 -0
- package/dist/sdk/sdk-huggingface-download-state.js +25 -0
- package/dist/sdk/sdk-huggingface-download-state.js.map +1 -0
- package/dist/sdk/sdk-live-runner-registry.d.ts +21 -0
- package/dist/sdk/sdk-live-runner-registry.d.ts.map +1 -0
- package/dist/sdk/sdk-live-runner-registry.js +26 -0
- package/dist/sdk/sdk-live-runner-registry.js.map +1 -0
- package/dist/sdk/sdk-model-catalog-service.d.ts +20 -0
- package/dist/sdk/sdk-model-catalog-service.d.ts.map +1 -0
- package/dist/sdk/sdk-model-catalog-service.js +219 -0
- package/dist/sdk/sdk-model-catalog-service.js.map +1 -0
- package/dist/sdk/sdk-model-download-service.d.ts +39 -0
- package/dist/sdk/sdk-model-download-service.d.ts.map +1 -0
- package/dist/sdk/sdk-model-download-service.js +274 -0
- package/dist/sdk/sdk-model-download-service.js.map +1 -0
- package/dist/sdk/sdk-model-requirement-probe.d.ts +30 -0
- package/dist/sdk/sdk-model-requirement-probe.d.ts.map +1 -0
- package/dist/sdk/sdk-model-requirement-probe.js +120 -0
- package/dist/sdk/sdk-model-requirement-probe.js.map +1 -0
- package/dist/sdk/sdk-node-package-requirement-probe.d.ts +20 -0
- package/dist/sdk/sdk-node-package-requirement-probe.d.ts.map +1 -0
- package/dist/sdk/sdk-node-package-requirement-probe.js +44 -0
- package/dist/sdk/sdk-node-package-requirement-probe.js.map +1 -0
- package/dist/sdk/sdk-node-type-inventory-service.d.ts +11 -0
- package/dist/sdk/sdk-node-type-inventory-service.d.ts.map +1 -0
- package/dist/sdk/sdk-node-type-inventory-service.js +36 -0
- package/dist/sdk/sdk-node-type-inventory-service.js.map +1 -0
- package/dist/sdk/sdk-preflight-orchestrator.d.ts +54 -0
- package/dist/sdk/sdk-preflight-orchestrator.d.ts.map +1 -0
- package/dist/sdk/sdk-preflight-orchestrator.js +63 -0
- package/dist/sdk/sdk-preflight-orchestrator.js.map +1 -0
- package/dist/sdk/sdk-preflight-requirement-resolver.d.ts +35 -0
- package/dist/sdk/sdk-preflight-requirement-resolver.d.ts.map +1 -0
- package/dist/sdk/sdk-preflight-requirement-resolver.js +67 -0
- package/dist/sdk/sdk-preflight-requirement-resolver.js.map +1 -0
- package/dist/sdk/sdk-preflight-service.d.ts +61 -0
- package/dist/sdk/sdk-preflight-service.d.ts.map +1 -0
- package/dist/sdk/sdk-preflight-service.js +104 -0
- package/dist/sdk/sdk-preflight-service.js.map +1 -0
- package/dist/sdk/sdk-preflight-workflow-source.d.ts +18 -0
- package/dist/sdk/sdk-preflight-workflow-source.d.ts.map +1 -0
- package/dist/sdk/sdk-preflight-workflow-source.js +42 -0
- package/dist/sdk/sdk-preflight-workflow-source.js.map +1 -0
- package/dist/sdk/sdk-route-policy.d.ts +3 -0
- package/dist/sdk/sdk-route-policy.d.ts.map +1 -0
- package/dist/sdk/sdk-route-policy.js +18 -0
- package/dist/sdk/sdk-route-policy.js.map +1 -0
- package/dist/sdk/sdk-runtime-capabilities-service.d.ts +60 -0
- package/dist/sdk/sdk-runtime-capabilities-service.d.ts.map +1 -0
- package/dist/sdk/sdk-runtime-capabilities-service.js +30 -0
- package/dist/sdk/sdk-runtime-capabilities-service.js.map +1 -0
- package/dist/sdk/sdk-runtime-requirement-probe.d.ts +13 -0
- package/dist/sdk/sdk-runtime-requirement-probe.d.ts.map +1 -0
- package/dist/sdk/sdk-runtime-requirement-probe.js +55 -0
- package/dist/sdk/sdk-runtime-requirement-probe.js.map +1 -0
- package/dist/sdk/sdk-static-preflight-service.d.ts +66 -0
- package/dist/sdk/sdk-static-preflight-service.d.ts.map +1 -0
- package/dist/sdk/sdk-static-preflight-service.js +492 -0
- package/dist/sdk/sdk-static-preflight-service.js.map +1 -0
- package/dist/sdk/sdk-temporary-asset-service.d.ts +18 -0
- package/dist/sdk/sdk-temporary-asset-service.d.ts.map +1 -0
- package/dist/sdk/sdk-temporary-asset-service.js +33 -0
- package/dist/sdk/sdk-temporary-asset-service.js.map +1 -0
- package/dist/sdk/sdk-temporary-asset-upload-http-handler.d.ts +9 -0
- package/dist/sdk/sdk-temporary-asset-upload-http-handler.d.ts.map +1 -0
- package/dist/sdk/sdk-temporary-asset-upload-http-handler.js +65 -0
- package/dist/sdk/sdk-temporary-asset-upload-http-handler.js.map +1 -0
- package/dist/sdk/sdk-v1-handler-map.d.ts +44 -0
- package/dist/sdk/sdk-v1-handler-map.d.ts.map +1 -0
- package/dist/sdk/sdk-v1-handler-map.js +37 -0
- package/dist/sdk/sdk-v1-handler-map.js.map +1 -0
- package/dist/sdk/sdk-v1-service-error.d.ts +21 -0
- package/dist/sdk/sdk-v1-service-error.d.ts.map +1 -0
- package/dist/sdk/sdk-v1-service-error.js +62 -0
- package/dist/sdk/sdk-v1-service-error.js.map +1 -0
- package/dist/sdk/sdk-v1-service.d.ts +74 -0
- package/dist/sdk/sdk-v1-service.d.ts.map +1 -0
- package/dist/sdk/sdk-v1-service.js +219 -0
- package/dist/sdk/sdk-v1-service.js.map +1 -0
- package/dist/sdk/sdk-worker-readiness-adapter.d.ts +22 -0
- package/dist/sdk/sdk-worker-readiness-adapter.d.ts.map +1 -0
- package/dist/sdk/sdk-worker-readiness-adapter.js +29 -0
- package/dist/sdk/sdk-worker-readiness-adapter.js.map +1 -0
- package/dist/server.js +1019 -201
- package/dist/server.js.map +1 -1
- package/dist/session/asset-autosave.d.ts +80 -0
- package/dist/session/asset-autosave.d.ts.map +1 -0
- package/dist/session/asset-autosave.js +456 -0
- package/dist/session/asset-autosave.js.map +1 -0
- package/dist/session/chat-compaction.d.ts +107 -0
- package/dist/session/chat-compaction.d.ts.map +1 -0
- package/dist/session/chat-compaction.js +191 -0
- package/dist/session/chat-compaction.js.map +1 -0
- package/dist/session/chat-history.d.ts +75 -0
- package/dist/session/chat-history.d.ts.map +1 -0
- package/dist/session/chat-history.js +194 -0
- package/dist/session/chat-history.js.map +1 -0
- package/dist/session/chat-prompt.d.ts +45 -0
- package/dist/session/chat-prompt.d.ts.map +1 -0
- package/dist/session/chat-prompt.js +480 -0
- package/dist/session/chat-prompt.js.map +1 -0
- package/dist/session/chat-turn.d.ts +336 -0
- package/dist/session/chat-turn.d.ts.map +1 -0
- package/dist/session/chat-turn.js +3516 -0
- package/dist/session/chat-turn.js.map +1 -0
- package/dist/session/client-session.d.ts +59 -0
- package/dist/session/client-session.d.ts.map +1 -0
- package/dist/session/client-session.js +2 -0
- package/dist/session/client-session.js.map +1 -0
- package/dist/session/commands.d.ts +133 -0
- package/dist/session/commands.d.ts.map +1 -0
- package/dist/session/commands.js +778 -0
- package/dist/session/commands.js.map +1 -0
- package/dist/session/inference.d.ts +213 -0
- package/dist/session/inference.d.ts.map +1 -0
- package/dist/session/inference.js +1072 -0
- package/dist/session/inference.js.map +1 -0
- package/dist/session/job-execution.d.ts +477 -0
- package/dist/session/job-execution.d.ts.map +1 -0
- package/dist/session/job-execution.js +2089 -0
- package/dist/session/job-execution.js.map +1 -0
- package/dist/session/media-generation.d.ts +94 -0
- package/dist/session/media-generation.d.ts.map +1 -0
- package/dist/session/media-generation.js +234 -0
- package/dist/session/media-generation.js.map +1 -0
- package/dist/session/model-interfaces.d.ts +27 -0
- package/dist/session/model-interfaces.d.ts.map +1 -0
- package/dist/session/model-interfaces.js +132 -0
- package/dist/session/model-interfaces.js.map +1 -0
- package/dist/session/sanitize.d.ts +7 -0
- package/dist/session/sanitize.d.ts.map +1 -0
- package/dist/session/sanitize.js +64 -0
- package/dist/session/sanitize.js.map +1 -0
- package/dist/settings-registry.d.ts +6 -15
- package/dist/settings-registry.d.ts.map +1 -1
- package/dist/settings-registry.js +6 -103
- package/dist/settings-registry.js.map +1 -1
- package/dist/storage-api.d.ts +20 -0
- package/dist/storage-api.d.ts.map +1 -1
- package/dist/storage-api.js +260 -77
- package/dist/storage-api.js.map +1 -1
- package/dist/storage-retention.d.ts +9 -0
- package/dist/storage-retention.d.ts.map +1 -0
- package/dist/storage-retention.js +74 -0
- package/dist/storage-retention.js.map +1 -0
- package/dist/system-stats.d.ts +52 -0
- package/dist/system-stats.d.ts.map +1 -0
- package/dist/system-stats.js +134 -0
- package/dist/system-stats.js.map +1 -0
- package/dist/test-ui-server.d.ts +46 -0
- package/dist/test-ui-server.d.ts.map +1 -1
- package/dist/test-ui-server.js +188 -41
- package/dist/test-ui-server.js.map +1 -1
- package/dist/triggers/boot.d.ts +73 -0
- package/dist/triggers/boot.d.ts.map +1 -0
- package/dist/triggers/boot.js +149 -0
- package/dist/triggers/boot.js.map +1 -0
- package/dist/triggers/dispatcher.d.ts +234 -0
- package/dist/triggers/dispatcher.d.ts.map +1 -0
- package/dist/triggers/dispatcher.js +525 -0
- package/dist/triggers/dispatcher.js.map +1 -0
- package/dist/triggers/file-watch.d.ts +90 -0
- package/dist/triggers/file-watch.d.ts.map +1 -0
- package/dist/triggers/file-watch.js +405 -0
- package/dist/triggers/file-watch.js.map +1 -0
- package/dist/triggers/registration-sync.d.ts +34 -0
- package/dist/triggers/registration-sync.d.ts.map +1 -0
- package/dist/triggers/registration-sync.js +162 -0
- package/dist/triggers/registration-sync.js.map +1 -0
- package/dist/triggers/schedule-timing.d.ts +52 -0
- package/dist/triggers/schedule-timing.d.ts.map +1 -0
- package/dist/triggers/schedule-timing.js +90 -0
- package/dist/triggers/schedule-timing.js.map +1 -0
- package/dist/triggers/scheduler.d.ts +55 -0
- package/dist/triggers/scheduler.d.ts.map +1 -0
- package/dist/triggers/scheduler.js +122 -0
- package/dist/triggers/scheduler.js.map +1 -0
- package/dist/triggers/settle.d.ts +66 -0
- package/dist/triggers/settle.d.ts.map +1 -0
- package/dist/triggers/settle.js +99 -0
- package/dist/triggers/settle.js.map +1 -0
- package/dist/triggers/stores.d.ts +38 -0
- package/dist/triggers/stores.d.ts.map +1 -0
- package/dist/triggers/stores.js +168 -0
- package/dist/triggers/stores.js.map +1 -0
- package/dist/triggers/webhook-route.d.ts +49 -0
- package/dist/triggers/webhook-route.d.ts.map +1 -0
- package/dist/triggers/webhook-route.js +148 -0
- package/dist/triggers/webhook-route.js.map +1 -0
- package/dist/trpc/context.d.ts +52 -5
- package/dist/trpc/context.d.ts.map +1 -1
- package/dist/trpc/context.js +4 -1
- package/dist/trpc/context.js.map +1 -1
- package/dist/trpc/error-formatter.d.ts.map +1 -1
- package/dist/trpc/error-formatter.js +23 -2
- package/dist/trpc/error-formatter.js.map +1 -1
- package/dist/trpc/error-logging.d.ts +14 -0
- package/dist/trpc/error-logging.d.ts.map +1 -0
- package/dist/trpc/error-logging.js +27 -0
- package/dist/trpc/error-logging.js.map +1 -0
- package/dist/trpc/index.d.ts +2 -2
- package/dist/trpc/index.d.ts.map +1 -1
- package/dist/trpc/index.js +0 -2
- package/dist/trpc/index.js.map +1 -1
- package/dist/trpc/middleware.d.ts +6 -3
- package/dist/trpc/middleware.d.ts.map +1 -1
- package/dist/trpc/router.d.ts +29791 -1303
- package/dist/trpc/router.d.ts.map +1 -1
- package/dist/trpc/router.js +46 -3
- package/dist/trpc/router.js.map +1 -1
- package/dist/trpc/routers/agent-access.d.ts +149 -0
- package/dist/trpc/routers/agent-access.d.ts.map +1 -0
- package/dist/trpc/routers/agent-access.js +224 -0
- package/dist/trpc/routers/agent-access.js.map +1 -0
- package/dist/trpc/routers/applications.d.ts +1117 -0
- package/dist/trpc/routers/applications.d.ts.map +1 -0
- package/dist/trpc/routers/applications.js +218 -0
- package/dist/trpc/routers/applications.js.map +1 -0
- package/dist/trpc/routers/assets.d.ts +184 -42
- package/dist/trpc/routers/assets.d.ts.map +1 -1
- package/dist/trpc/routers/assets.js +499 -105
- package/dist/trpc/routers/assets.js.map +1 -1
- package/dist/trpc/routers/code-gen.d.ts +99 -0
- package/dist/trpc/routers/code-gen.d.ts.map +1 -0
- package/dist/trpc/routers/code-gen.js +164 -0
- package/dist/trpc/routers/code-gen.js.map +1 -0
- package/dist/trpc/routers/collections.d.ts +1 -26
- package/dist/trpc/routers/collections.d.ts.map +1 -1
- package/dist/trpc/routers/collections.js +145 -78
- package/dist/trpc/routers/collections.js.map +1 -1
- package/dist/trpc/routers/costs.d.ts +40 -96
- package/dist/trpc/routers/costs.d.ts.map +1 -1
- package/dist/trpc/routers/costs.js +14 -89
- package/dist/trpc/routers/costs.js.map +1 -1
- package/dist/trpc/routers/credits.d.ts +110 -0
- package/dist/trpc/routers/credits.d.ts.map +1 -0
- package/dist/trpc/routers/credits.js +69 -0
- package/dist/trpc/routers/credits.js.map +1 -0
- package/dist/trpc/routers/custom-providers.d.ts +88 -0
- package/dist/trpc/routers/custom-providers.d.ts.map +1 -0
- package/dist/trpc/routers/custom-providers.js +140 -0
- package/dist/trpc/routers/custom-providers.js.map +1 -0
- package/dist/trpc/routers/documents.d.ts +61 -0
- package/dist/trpc/routers/documents.d.ts.map +1 -0
- package/dist/trpc/routers/documents.js +23 -0
- package/dist/trpc/routers/documents.js.map +1 -0
- package/dist/trpc/routers/extension.d.ts +24 -0
- package/dist/trpc/routers/extension.d.ts.map +1 -0
- package/dist/trpc/routers/extension.js +28 -0
- package/dist/trpc/routers/extension.js.map +1 -0
- package/dist/trpc/routers/external-mcp.d.ts +108 -0
- package/dist/trpc/routers/external-mcp.d.ts.map +1 -0
- package/dist/trpc/routers/external-mcp.js +70 -0
- package/dist/trpc/routers/external-mcp.js.map +1 -0
- package/dist/trpc/routers/files.d.ts +9 -2
- package/dist/trpc/routers/files.d.ts.map +1 -1
- package/dist/trpc/routers/files.js +67 -37
- package/dist/trpc/routers/files.js.map +1 -1
- package/dist/trpc/routers/fonts.d.ts +30 -0
- package/dist/trpc/routers/fonts.d.ts.map +1 -0
- package/dist/trpc/routers/fonts.js +112 -0
- package/dist/trpc/routers/fonts.js.map +1 -0
- package/dist/trpc/routers/games.d.ts +7610 -0
- package/dist/trpc/routers/games.d.ts.map +1 -0
- package/dist/trpc/routers/games.js +456 -0
- package/dist/trpc/routers/games.js.map +1 -0
- package/dist/trpc/routers/integrations.d.ts +86 -0
- package/dist/trpc/routers/integrations.d.ts.map +1 -0
- package/dist/trpc/routers/integrations.js +129 -0
- package/dist/trpc/routers/integrations.js.map +1 -0
- package/dist/trpc/routers/jobs.d.ts +88 -27
- package/dist/trpc/routers/jobs.d.ts.map +1 -1
- package/dist/trpc/routers/jobs.js +113 -35
- package/dist/trpc/routers/jobs.js.map +1 -1
- package/dist/trpc/routers/js-scripts.d.ts +410 -0
- package/dist/trpc/routers/js-scripts.d.ts.map +1 -0
- package/dist/trpc/routers/js-scripts.js +303 -0
- package/dist/trpc/routers/js-scripts.js.map +1 -0
- package/dist/trpc/routers/mcp-config.d.ts +1 -1
- package/dist/trpc/routers/mcp-config.js +5 -3
- package/dist/trpc/routers/mcp-config.js.map +1 -1
- package/dist/trpc/routers/memories.d.ts +79 -0
- package/dist/trpc/routers/memories.d.ts.map +1 -0
- package/dist/trpc/routers/memories.js +66 -0
- package/dist/trpc/routers/memories.js.map +1 -0
- package/dist/trpc/routers/messages.d.ts +4 -77
- package/dist/trpc/routers/messages.d.ts.map +1 -1
- package/dist/trpc/routers/messages.js +9 -58
- package/dist/trpc/routers/messages.js.map +1 -1
- package/dist/trpc/routers/models.d.ts +1403 -238
- package/dist/trpc/routers/models.d.ts.map +1 -1
- package/dist/trpc/routers/models.js +532 -294
- package/dist/trpc/routers/models.js.map +1 -1
- package/dist/trpc/routers/nodes.d.ts +32 -16
- package/dist/trpc/routers/nodes.d.ts.map +1 -1
- package/dist/trpc/routers/nodes.js +70 -31
- package/dist/trpc/routers/nodes.js.map +1 -1
- package/dist/trpc/routers/packs.d.ts +184 -0
- package/dist/trpc/routers/packs.d.ts.map +1 -0
- package/dist/trpc/routers/packs.js +259 -0
- package/dist/trpc/routers/packs.js.map +1 -0
- package/dist/trpc/routers/projects.d.ts +422 -0
- package/dist/trpc/routers/projects.d.ts.map +1 -0
- package/dist/trpc/routers/projects.js +373 -0
- package/dist/trpc/routers/projects.js.map +1 -0
- package/dist/trpc/routers/resources.d.ts +120 -0
- package/dist/trpc/routers/resources.d.ts.map +1 -0
- package/dist/trpc/routers/resources.js +212 -0
- package/dist/trpc/routers/resources.js.map +1 -0
- package/dist/trpc/routers/scripts.d.ts +1116 -0
- package/dist/trpc/routers/scripts.d.ts.map +1 -0
- package/dist/trpc/routers/scripts.js +131 -0
- package/dist/trpc/routers/scripts.js.map +1 -0
- package/dist/trpc/routers/segmentation.d.ts +62 -0
- package/dist/trpc/routers/segmentation.d.ts.map +1 -0
- package/dist/trpc/routers/segmentation.js +146 -0
- package/dist/trpc/routers/segmentation.js.map +1 -0
- package/dist/trpc/routers/settings.d.ts +177 -10
- package/dist/trpc/routers/settings.d.ts.map +1 -1
- package/dist/trpc/routers/settings.js +123 -33
- package/dist/trpc/routers/settings.js.map +1 -1
- package/dist/trpc/routers/sketch.d.ts +854 -0
- package/dist/trpc/routers/sketch.d.ts.map +1 -0
- package/dist/trpc/routers/sketch.js +617 -0
- package/dist/trpc/routers/sketch.js.map +1 -0
- package/dist/trpc/routers/skills.d.ts +76 -22
- package/dist/trpc/routers/skills.d.ts.map +1 -1
- package/dist/trpc/routers/skills.js +195 -170
- package/dist/trpc/routers/skills.js.map +1 -1
- package/dist/trpc/routers/storage.d.ts +3 -30
- package/dist/trpc/routers/storage.d.ts.map +1 -1
- package/dist/trpc/routers/storage.js +60 -117
- package/dist/trpc/routers/storage.js.map +1 -1
- package/dist/trpc/routers/storyboards.d.ts +5907 -0
- package/dist/trpc/routers/storyboards.d.ts.map +1 -0
- package/dist/trpc/routers/storyboards.js +156 -0
- package/dist/trpc/routers/storyboards.js.map +1 -0
- package/dist/trpc/routers/threads.d.ts +33 -21
- package/dist/trpc/routers/threads.d.ts.map +1 -1
- package/dist/trpc/routers/threads.js +36 -30
- package/dist/trpc/routers/threads.js.map +1 -1
- package/dist/trpc/routers/timeline.d.ts +8516 -0
- package/dist/trpc/routers/timeline.d.ts.map +1 -0
- package/dist/trpc/routers/timeline.js +493 -0
- package/dist/trpc/routers/timeline.js.map +1 -0
- package/dist/trpc/routers/triggers.d.ts +46 -0
- package/dist/trpc/routers/triggers.d.ts.map +1 -0
- package/dist/trpc/routers/triggers.js +117 -0
- package/dist/trpc/routers/triggers.js.map +1 -0
- package/dist/trpc/routers/users.d.ts +1 -14
- package/dist/trpc/routers/users.d.ts.map +1 -1
- package/dist/trpc/routers/users.js +1 -12
- package/dist/trpc/routers/users.js.map +1 -1
- package/dist/trpc/routers/worker.d.ts +242 -0
- package/dist/trpc/routers/worker.d.ts.map +1 -0
- package/dist/trpc/routers/worker.js +220 -0
- package/dist/trpc/routers/worker.js.map +1 -0
- package/dist/trpc/routers/workflows.d.ts +385 -197
- package/dist/trpc/routers/workflows.d.ts.map +1 -1
- package/dist/trpc/routers/workflows.js +326 -345
- package/dist/trpc/routers/workflows.js.map +1 -1
- package/dist/trpc/routers/workspace.d.ts +59 -33
- package/dist/trpc/routers/workspace.d.ts.map +1 -1
- package/dist/trpc/routers/workspace.js +230 -93
- package/dist/trpc/routers/workspace.js.map +1 -1
- package/dist/trpc/sandbox-coverage.d.ts +64 -0
- package/dist/trpc/sandbox-coverage.d.ts.map +1 -0
- package/dist/trpc/sandbox-coverage.js +1049 -0
- package/dist/trpc/sandbox-coverage.js.map +1 -0
- package/dist/websocket-client-session.d.ts +320 -0
- package/dist/websocket-client-session.d.ts.map +1 -0
- package/dist/websocket-client-session.js +1253 -0
- package/dist/websocket-client-session.js.map +1 -0
- package/dist/workflow-interface-service.d.ts +87 -0
- package/dist/workflow-interface-service.d.ts.map +1 -0
- package/dist/workflow-interface-service.js +131 -0
- package/dist/workflow-interface-service.js.map +1 -0
- package/dist/workspace-api.d.ts.map +1 -1
- package/dist/workspace-api.js +33 -23
- package/dist/workspace-api.js.map +1 -1
- package/dist/ws-adapter.d.ts +46 -3
- package/dist/ws-adapter.d.ts.map +1 -1
- package/dist/ws-adapter.js +164 -26
- package/dist/ws-adapter.js.map +1 -1
- package/package.json +81 -35
- package/dist/agent/agent-runtime.d.ts +0 -47
- package/dist/agent/agent-runtime.d.ts.map +0 -1
- package/dist/agent/agent-runtime.js +0 -579
- package/dist/agent/agent-runtime.js.map +0 -1
- package/dist/agent/codex-agent.d.ts +0 -41
- package/dist/agent/codex-agent.d.ts.map +0 -1
- package/dist/agent/codex-agent.js +0 -334
- package/dist/agent/codex-agent.js.map +0 -1
- package/dist/agent/index.d.ts +0 -6
- package/dist/agent/index.d.ts.map +0 -1
- package/dist/agent/index.js +0 -4
- package/dist/agent/index.js.map +0 -1
- package/dist/agent/llm-agent.d.ts +0 -51
- package/dist/agent/llm-agent.d.ts.map +0 -1
- package/dist/agent/llm-agent.js +0 -526
- package/dist/agent/llm-agent.js.map +0 -1
- package/dist/agent/mcp-tool-server.d.ts +0 -33
- package/dist/agent/mcp-tool-server.d.ts.map +0 -1
- package/dist/agent/mcp-tool-server.js +0 -292
- package/dist/agent/mcp-tool-server.js.map +0 -1
- package/dist/agent/opencode-agent.d.ts +0 -32
- package/dist/agent/opencode-agent.d.ts.map +0 -1
- package/dist/agent/opencode-agent.js +0 -332
- package/dist/agent/opencode-agent.js.map +0 -1
- package/dist/agent/pi-agent.d.ts +0 -42
- package/dist/agent/pi-agent.d.ts.map +0 -1
- package/dist/agent/pi-agent.js +0 -429
- package/dist/agent/pi-agent.js.map +0 -1
- package/dist/agent/sdk-provider.d.ts +0 -46
- package/dist/agent/sdk-provider.d.ts.map +0 -1
- package/dist/agent/sdk-provider.js +0 -50
- package/dist/agent/sdk-provider.js.map +0 -1
- package/dist/agent/socket-route.d.ts +0 -11
- package/dist/agent/socket-route.d.ts.map +0 -1
- package/dist/agent/socket-route.js +0 -262
- package/dist/agent/socket-route.js.map +0 -1
- package/dist/agent/transport.d.ts +0 -39
- package/dist/agent/transport.d.ts.map +0 -1
- package/dist/agent/transport.js +0 -17
- package/dist/agent/transport.js.map +0 -1
- package/dist/agent/types.d.ts +0 -11
- package/dist/agent/types.d.ts.map +0 -1
- package/dist/agent/types.js +0 -10
- package/dist/agent/types.js.map +0 -1
- package/dist/routes/jobs.d.ts +0 -16
- package/dist/routes/jobs.d.ts.map +0 -1
- package/dist/routes/jobs.js +0 -25
- package/dist/routes/jobs.js.map +0 -1
- package/dist/trpc/routers/sandboxes.d.ts +0 -64
- package/dist/trpc/routers/sandboxes.d.ts.map +0 -1
- package/dist/trpc/routers/sandboxes.js +0 -327
- package/dist/trpc/routers/sandboxes.js.map +0 -1
- package/dist/unified-websocket-runner.d.ts +0 -280
- package/dist/unified-websocket-runner.d.ts.map +0 -1
- package/dist/unified-websocket-runner.js +0 -3840
- package/dist/unified-websocket-runner.js.map +0 -1
|
@@ -0,0 +1,2089 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Everything between "run_job arrived" and "terminal status persisted": the
|
|
3
|
+
* admission gates, the concurrency queue, run start-up, the message stream,
|
|
4
|
+
* reconnect, cancel, and terminal-status persistence.
|
|
5
|
+
*
|
|
6
|
+
* Extracted from `WebSocketClientSession` (T3 of
|
|
7
|
+
* docs/websocket-runner-refactor-plan.md). It knows the connection only
|
|
8
|
+
* through {@link ClientSession}; what the seam does not carry arrives as
|
|
9
|
+
* {@link JobExecutionDeps}.
|
|
10
|
+
*/
|
|
11
|
+
import { randomUUID } from "node:crypto";
|
|
12
|
+
import { ApiErrorCode } from "../error-codes.js";
|
|
13
|
+
import { admitSpend, releaseSpend, reserveSpend } from "../credit-gate.js";
|
|
14
|
+
import { JobConcurrencyQueue } from "../job-queue.js";
|
|
15
|
+
import { isBoolean, isFiniteNumber, isFunctionValue, isNonEmptyString, isNumber, isObjectLike, isRecord, isString } from "../lib/wire-values.js";
|
|
16
|
+
import { Graph, withExplicitNodeFlags } from "@nodetool-ai/kernel";
|
|
17
|
+
import { ExecutionSession, isExecutionPreflightError, isUnitBilledCapability, priceGeneration, toRawGraphInput } from "@nodetool-ai/execution";
|
|
18
|
+
import { persistableOutputs, withoutInlineAssetBytes } from "@nodetool-ai/execution/service";
|
|
19
|
+
import { createRunSupervisor } from "../run-supervisor.js";
|
|
20
|
+
import { jobRunRegistry } from "../job-run-registry.js";
|
|
21
|
+
import { Application, Asset, Job, listApplicationVersions, invocationIdInUse, releasedApplicationRelease, releasedApplicationVersion, reserveInvocation, settleInvocation, Workflow } from "@nodetool-ai/models";
|
|
22
|
+
import { getInstanceId } from "../lib/instance-id.js";
|
|
23
|
+
import { requestRemoteJobCancel } from "../job-control.js";
|
|
24
|
+
import { estimateWorkflowCost, nodeExpectedQuantity } from "@nodetool-ai/node-sdk/cost-estimate";
|
|
25
|
+
import { extractPricingParams } from "@nodetool-ai/node-sdk/pricing-params";
|
|
26
|
+
import { getModelUnitPrice } from "@nodetool-ai/model-pricing";
|
|
27
|
+
import { applicationReleaseResponse } from "@nodetool-ai/protocol/api-schemas/applications.js";
|
|
28
|
+
import { confineRunRequest, isRunRefusal } from "../lib/app-session-scope.js";
|
|
29
|
+
import { releaseBlockedReason } from "../lib/app-deployment-service.js";
|
|
30
|
+
import { autoSaveAssets, primaryTextOutputName } from "./asset-autosave.js";
|
|
31
|
+
import { createRuntimeContext } from "./model-interfaces.js";
|
|
32
|
+
import { formatSanitizedError, sanitizeLargeText } from "./sanitize.js";
|
|
33
|
+
import { createLogger } from "@nodetool-ai/config";
|
|
34
|
+
const log = createLogger("nodetool.websocket.runner");
|
|
35
|
+
const TERMINAL_JOB_STATUSES = ["completed", "failed", "cancelled", "error"];
|
|
36
|
+
export const DEFAULT_RUN_JOB_EXECUTION_OPTIONS = Object.freeze({
|
|
37
|
+
persistence: "job",
|
|
38
|
+
eventDetail: "full",
|
|
39
|
+
assetPersistence: "auto"
|
|
40
|
+
});
|
|
41
|
+
export function resolveRunJobExecutionOptions(value, sdkDefaults = false) {
|
|
42
|
+
return {
|
|
43
|
+
persistence: value?.persistence === "session" ? "session" : "job",
|
|
44
|
+
eventDetail: value?.event_detail === "outputs" || value?.event_detail === "terminal"
|
|
45
|
+
? value.event_detail
|
|
46
|
+
: "full",
|
|
47
|
+
assetPersistence: value?.asset_persistence === "temporary" ||
|
|
48
|
+
(value?.asset_persistence == null && sdkDefaults)
|
|
49
|
+
? "temporary"
|
|
50
|
+
: "auto"
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
export function resolveRunJobUserId(requestUserId, connectionUserId) {
|
|
54
|
+
return requestUserId?.trim() || connectionUserId;
|
|
55
|
+
}
|
|
56
|
+
export function createRelayActivityWaiter(context, executionSettled, abortSignal) {
|
|
57
|
+
let pending = context.hasMessages();
|
|
58
|
+
let resolveWaiter = null;
|
|
59
|
+
let disposed = false;
|
|
60
|
+
const notify = () => {
|
|
61
|
+
pending = true;
|
|
62
|
+
const resolve = resolveWaiter;
|
|
63
|
+
resolveWaiter = null;
|
|
64
|
+
resolve?.();
|
|
65
|
+
};
|
|
66
|
+
const removeMessageListener = context.addMessageListener(notify);
|
|
67
|
+
const dispose = () => {
|
|
68
|
+
if (disposed)
|
|
69
|
+
return;
|
|
70
|
+
disposed = true;
|
|
71
|
+
removeMessageListener();
|
|
72
|
+
abortSignal?.removeEventListener("abort", onAbort);
|
|
73
|
+
};
|
|
74
|
+
const settle = () => {
|
|
75
|
+
notify();
|
|
76
|
+
dispose();
|
|
77
|
+
};
|
|
78
|
+
const onAbort = () => settle();
|
|
79
|
+
if (abortSignal?.aborted) {
|
|
80
|
+
settle();
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
abortSignal?.addEventListener("abort", onAbort, { once: true });
|
|
84
|
+
}
|
|
85
|
+
void executionSettled.then(settle, settle);
|
|
86
|
+
return async () => {
|
|
87
|
+
if (pending) {
|
|
88
|
+
pending = false;
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
await new Promise((resolve) => {
|
|
92
|
+
resolveWaiter = resolve;
|
|
93
|
+
});
|
|
94
|
+
pending = false;
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
/** The shape `crypto.randomUUID()` produces, on either side of the wire. */
|
|
98
|
+
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
99
|
+
export class JobExecutionManager {
|
|
100
|
+
session;
|
|
101
|
+
deps;
|
|
102
|
+
constructor(session, deps) {
|
|
103
|
+
this.session = session;
|
|
104
|
+
this.deps = deps;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* The runs in flight on this connection. Private since T8's D2: chat's runs
|
|
108
|
+
* and the suites that stage one go through the methods below, so every
|
|
109
|
+
* mutation of the connection's slot accounting happens in this file.
|
|
110
|
+
*/
|
|
111
|
+
activeJobs = new Map();
|
|
112
|
+
/**
|
|
113
|
+
* Runs that arrived while {@link MAX_CONCURRENT_JOBS} runs were already in
|
|
114
|
+
* flight. They start automatically (FIFO) as active jobs finish.
|
|
115
|
+
*/
|
|
116
|
+
jobQueue = new JobConcurrencyQueue();
|
|
117
|
+
dequeuedJobs = new Set();
|
|
118
|
+
/**
|
|
119
|
+
* Count of jobs that have passed the concurrency gate but haven't been added
|
|
120
|
+
* to {@link activeJobs} yet (startJob awaits graph hydration first). Counted
|
|
121
|
+
* toward the cap synchronously so two run_job commands arriving back-to-back
|
|
122
|
+
* can't both slip past `activeJobs.size` and exceed MAX_CONCURRENT_JOBS.
|
|
123
|
+
*/
|
|
124
|
+
startingJobs = 0;
|
|
125
|
+
/**
|
|
126
|
+
* WS slot accounting, for leak accounting: after every run finishes both
|
|
127
|
+
* must be back to zero. Read by the reliability harness's ws-server driver,
|
|
128
|
+
* whose `cleanup-leaks` invariant can only assert what it can measure.
|
|
129
|
+
*/
|
|
130
|
+
get slotCounters() {
|
|
131
|
+
return {
|
|
132
|
+
activeJobs: this.activeJobs.size,
|
|
133
|
+
startingJobs: this.startingJobs
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
/** Ids of the runs in flight on this connection. Read for diagnostics. */
|
|
137
|
+
activeJobIds() {
|
|
138
|
+
return [...this.activeJobs.keys()];
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Register a run against this connection's slots. A chat turn that starts a
|
|
142
|
+
* workflow shares the connection's concurrency accounting, so its run is
|
|
143
|
+
* registered here rather than in a map of chat's own.
|
|
144
|
+
*/
|
|
145
|
+
registerJob(jobId, active) {
|
|
146
|
+
this.activeJobs.set(jobId, active);
|
|
147
|
+
}
|
|
148
|
+
/** Drop a run without draining the queue — the caller's `finally` does that. */
|
|
149
|
+
dropJob(jobId) {
|
|
150
|
+
this.activeJobs.delete(jobId);
|
|
151
|
+
}
|
|
152
|
+
/** Drop a run and start whatever was queued behind it. */
|
|
153
|
+
releaseJob(jobId) {
|
|
154
|
+
this.activeJobs.delete(jobId);
|
|
155
|
+
this.drainQueue();
|
|
156
|
+
}
|
|
157
|
+
/** The run registered under `jobId`, if it is still in flight. */
|
|
158
|
+
getActiveJob(jobId) {
|
|
159
|
+
return this.activeJobs.get(jobId);
|
|
160
|
+
}
|
|
161
|
+
/** Whether a run is registered under `jobId`. */
|
|
162
|
+
hasActiveJob(jobId) {
|
|
163
|
+
return this.activeJobs.has(jobId);
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Job ids of runs still executing on a previous connection's runner that
|
|
167
|
+
* THIS connection reattached to via `reconnect_job`. Client commands for
|
|
168
|
+
* them (`cancel_job`, `stop`, `stream_input`, `end_input_stream`,
|
|
169
|
+
* `update_node_properties`) are forwarded to the executing runner through
|
|
170
|
+
* the session's hooks.
|
|
171
|
+
*
|
|
172
|
+
* Ids, not session objects: holding the session would pin its frame buffer
|
|
173
|
+
* for this connection's whole life, defeating the registry's retention
|
|
174
|
+
* drop. Resolved through the registry at every use, so a dropped session
|
|
175
|
+
* simply resolves to null and its memory goes with it.
|
|
176
|
+
*/
|
|
177
|
+
adoptedJobIds = new Set();
|
|
178
|
+
/**
|
|
179
|
+
* This connection's identity as a job-run delivery target. Separate object
|
|
180
|
+
* from the host's chat delivery target so a job session's `detach(target)`
|
|
181
|
+
* can never be confused with a chat session's — both guard on identity.
|
|
182
|
+
*/
|
|
183
|
+
jobDeliveryTarget = {
|
|
184
|
+
deliver: (message) => this.deps.sendToSocket(message)
|
|
185
|
+
};
|
|
186
|
+
inferOutputType(value) {
|
|
187
|
+
if (value === null || value === undefined)
|
|
188
|
+
return "any";
|
|
189
|
+
if (isString(value))
|
|
190
|
+
return "str";
|
|
191
|
+
if (isNumber(value))
|
|
192
|
+
return Number.isInteger(value) ? "int" : "float";
|
|
193
|
+
if (isBoolean(value))
|
|
194
|
+
return "bool";
|
|
195
|
+
if (Array.isArray(value))
|
|
196
|
+
return "list";
|
|
197
|
+
if (isObjectLike(value))
|
|
198
|
+
return "dict";
|
|
199
|
+
return "any";
|
|
200
|
+
}
|
|
201
|
+
resolveOutputNodeForKey(active, outputKey) {
|
|
202
|
+
let fallback = null;
|
|
203
|
+
for (const raw of active.graph.nodes) {
|
|
204
|
+
const node = raw;
|
|
205
|
+
const id = isString(node.id) ? node.id : null;
|
|
206
|
+
if (!id)
|
|
207
|
+
continue;
|
|
208
|
+
const name = isString(node.name) ? node.name : id;
|
|
209
|
+
const type = isString(node.type) ? node.type : "";
|
|
210
|
+
if (name === outputKey || id === outputKey)
|
|
211
|
+
return { id, name };
|
|
212
|
+
if (type === "nodetool.output.Output" && !fallback)
|
|
213
|
+
fallback = { id, name };
|
|
214
|
+
}
|
|
215
|
+
return fallback;
|
|
216
|
+
}
|
|
217
|
+
async sendOutputUpdates(active, outputs) {
|
|
218
|
+
for (const [outputKey, values] of Object.entries(outputs)) {
|
|
219
|
+
const nodeRef = this.resolveOutputNodeForKey(active, outputKey) ?? {
|
|
220
|
+
id: outputKey,
|
|
221
|
+
name: outputKey
|
|
222
|
+
};
|
|
223
|
+
const seq = Array.isArray(values) ? values : [];
|
|
224
|
+
for (const rawValue of seq) {
|
|
225
|
+
const value = await active.context.normalizeOutputValue(rawValue);
|
|
226
|
+
await this.session.send({
|
|
227
|
+
type: "output_update",
|
|
228
|
+
node_id: nodeRef.id,
|
|
229
|
+
node_name: nodeRef.name,
|
|
230
|
+
output_name: "output",
|
|
231
|
+
value,
|
|
232
|
+
output_type: this.inferOutputType(value),
|
|
233
|
+
metadata: {},
|
|
234
|
+
workflow_id: active.workflowId,
|
|
235
|
+
job_id: active.jobId
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
async normalizeFinalOutputs(active, outputs) {
|
|
241
|
+
const normalized = {};
|
|
242
|
+
for (const [outputKey, values] of Object.entries(outputs)) {
|
|
243
|
+
normalized[outputKey] = [];
|
|
244
|
+
for (const value of Array.isArray(values) ? values : []) {
|
|
245
|
+
normalized[outputKey].push(await active.context.normalizeOutputValue(value));
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return normalized;
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* The resilient session a frame's `job_id` belongs to: one this connection
|
|
252
|
+
* started, or one it adopted via `reconnect_job`.
|
|
253
|
+
*/
|
|
254
|
+
resolveJobSession(jobId) {
|
|
255
|
+
if (!isNonEmptyString(jobId))
|
|
256
|
+
return null;
|
|
257
|
+
const active = this.activeJobs.get(jobId);
|
|
258
|
+
if (active?.runSession)
|
|
259
|
+
return active.runSession;
|
|
260
|
+
if (!this.adoptedJobIds.has(jobId))
|
|
261
|
+
return null;
|
|
262
|
+
// No user means no connect(), so this session can own no registered run.
|
|
263
|
+
// "Nothing running" is the honest answer; main looked the job up under a
|
|
264
|
+
// hardcoded "1", which could return another user's run.
|
|
265
|
+
const userId = this.session.userId;
|
|
266
|
+
if (userId === null)
|
|
267
|
+
return null;
|
|
268
|
+
return jobRunRegistry.get(userId, jobId);
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Where a client command for `jobId` should act: this connection's own
|
|
272
|
+
* ExecutionSession, or — for a run this client reconnected to — the hooks
|
|
273
|
+
* of the session whose runner still owns it. Null when nothing is running.
|
|
274
|
+
*/
|
|
275
|
+
resolveJobControl(jobId) {
|
|
276
|
+
const active = this.activeJobs.get(jobId);
|
|
277
|
+
if (active) {
|
|
278
|
+
const session = active.session;
|
|
279
|
+
return {
|
|
280
|
+
workflowId: active.workflowId,
|
|
281
|
+
hooks: {
|
|
282
|
+
cancel: () => session.cancel(),
|
|
283
|
+
pushInput: (input, value, handle) => session.pushInput(input, value, handle),
|
|
284
|
+
finishInputStream: (input, handle) => session.finishInputStream(input, handle),
|
|
285
|
+
updateNodeProperties: (nodeId, properties) => session.updateNodeProperties(nodeId, properties)
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
const lookupUserId = this.session.userId;
|
|
290
|
+
if (lookupUserId === null)
|
|
291
|
+
return null;
|
|
292
|
+
const registered = jobRunRegistry.get(lookupUserId, jobId);
|
|
293
|
+
if (registered && registered.status === "running") {
|
|
294
|
+
return { hooks: registered.hooks, workflowId: registered.workflowId };
|
|
295
|
+
}
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Normalize a raw graph so that the kernel's NodeDescriptor contract is met.
|
|
300
|
+
* The web-UI / Python serialisation stores node properties under `data`;
|
|
301
|
+
* the kernel expects them under `properties`.
|
|
302
|
+
*/
|
|
303
|
+
normalizeGraph(graph) {
|
|
304
|
+
const nodes = graph.nodes.map((n) => {
|
|
305
|
+
if (n.properties === undefined && n.data !== undefined) {
|
|
306
|
+
const { data, ...rest } = n;
|
|
307
|
+
return { ...rest, properties: data };
|
|
308
|
+
}
|
|
309
|
+
return n;
|
|
310
|
+
});
|
|
311
|
+
const edges = graph.edges.map((edge) => {
|
|
312
|
+
const rawEdgeType = edge.edge_type ?? edge.type;
|
|
313
|
+
const edge_type = rawEdgeType === "control" ? "control" : "data";
|
|
314
|
+
const { type, ...rest } = edge;
|
|
315
|
+
return { ...rest, edge_type };
|
|
316
|
+
});
|
|
317
|
+
return { nodes, edges };
|
|
318
|
+
}
|
|
319
|
+
async hydrateGraph(graph) {
|
|
320
|
+
const normalized = this.normalizeGraph(graph);
|
|
321
|
+
if (!this.session.resolveNodeType) {
|
|
322
|
+
// No registry resolver configured — behavior flags can only come from
|
|
323
|
+
// the saved graph itself; absent ones are explicitly defaulted off.
|
|
324
|
+
// `normalizeGraph` above moved a saved node's `data` to `properties`
|
|
325
|
+
// and settled `edge_type`; what a saved record still lacks is the
|
|
326
|
+
// declared string type of the four identity fields, so read them out
|
|
327
|
+
// rather than assert them.
|
|
328
|
+
const asGraphData = {
|
|
329
|
+
nodes: normalized.nodes.map((n) => ({
|
|
330
|
+
...n,
|
|
331
|
+
id: String(n.id ?? ""),
|
|
332
|
+
type: String(n.type ?? "")
|
|
333
|
+
})),
|
|
334
|
+
edges: normalized.edges.map((e) => ({
|
|
335
|
+
...e,
|
|
336
|
+
source: String(e.source ?? ""),
|
|
337
|
+
sourceHandle: String(e.sourceHandle ?? ""),
|
|
338
|
+
target: String(e.target ?? ""),
|
|
339
|
+
targetHandle: String(e.targetHandle ?? "")
|
|
340
|
+
}))
|
|
341
|
+
};
|
|
342
|
+
return withExplicitNodeFlags(asGraphData);
|
|
343
|
+
}
|
|
344
|
+
const hydrated = await Graph.loadFromDict(normalized, {
|
|
345
|
+
resolver: this.session.resolveNodeType
|
|
346
|
+
});
|
|
347
|
+
return {
|
|
348
|
+
nodes: [...hydrated.nodes],
|
|
349
|
+
edges: [...hydrated.edges]
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
getRawGraph(req) {
|
|
353
|
+
if (req.graph) {
|
|
354
|
+
return this.normalizeGraph(req.graph);
|
|
355
|
+
}
|
|
356
|
+
if (req.workflow_id) {
|
|
357
|
+
const userId = this.session.requireUserId();
|
|
358
|
+
const workflowId = req.workflow_id;
|
|
359
|
+
return (async () => {
|
|
360
|
+
const workflow = await Workflow.find(userId, workflowId);
|
|
361
|
+
if (!workflow)
|
|
362
|
+
throw new Error(`Workflow not found: ${workflowId}`);
|
|
363
|
+
return this.normalizeGraph(workflow.graph);
|
|
364
|
+
})();
|
|
365
|
+
}
|
|
366
|
+
throw new Error("workflow_id or graph is required");
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* Surface a clean terminal job_update when pre-run setup fails (typically
|
|
370
|
+
* because the Python bridge could not start). Without this the error would
|
|
371
|
+
* bubble up to handleCommand and be sent as a generic `invalid_command`
|
|
372
|
+
* envelope, which the UI does not associate with the job — the workflow
|
|
373
|
+
* appears to spin forever instead of failing.
|
|
374
|
+
*/
|
|
375
|
+
async emitBeforeRunFailure(jobId, workflowId, err, persistJob) {
|
|
376
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
377
|
+
this.session.logError("beforeRunJob failed", err);
|
|
378
|
+
await this.session.send({
|
|
379
|
+
type: "job_update",
|
|
380
|
+
status: "failed",
|
|
381
|
+
job_id: jobId,
|
|
382
|
+
workflow_id: workflowId,
|
|
383
|
+
error: errorMessage
|
|
384
|
+
});
|
|
385
|
+
if (!persistJob)
|
|
386
|
+
return;
|
|
387
|
+
try {
|
|
388
|
+
const job = (await Job.get(jobId));
|
|
389
|
+
if (job) {
|
|
390
|
+
job.markFailed(errorMessage);
|
|
391
|
+
await job.save();
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
catch (persistErr) {
|
|
395
|
+
this.session.logError("beforeRunJob failure persistence failed", persistErr);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* Jobs occupying a concurrency slot: live + reserved-but-not-yet-registered.
|
|
400
|
+
*
|
|
401
|
+
* The cap counts a *user's* runs, not a socket's. A run detached from a
|
|
402
|
+
* dropped connection keeps executing, so counting only `activeJobs` would
|
|
403
|
+
* let a client reconnect its way past the cap — four abandoned sockets and
|
|
404
|
+
* a fresh one is eight concurrent runs. The registry is the cross-
|
|
405
|
+
* connection count; `activeJobs` contributes only the entries it does not
|
|
406
|
+
* already cover (a chat-triggered workflow run, which registers no
|
|
407
|
+
* session), or the same run would be counted twice.
|
|
408
|
+
*/
|
|
409
|
+
get inFlightJobCount() {
|
|
410
|
+
let sessionless = 0;
|
|
411
|
+
for (const job of this.activeJobs.values()) {
|
|
412
|
+
if (!job.runSession)
|
|
413
|
+
sessionless += 1;
|
|
414
|
+
}
|
|
415
|
+
// A session with no user has no registered runs to count; the local
|
|
416
|
+
// counters are still the honest answer. Same reasoning as the registry
|
|
417
|
+
// lookups above.
|
|
418
|
+
const countUserId = this.session.userId;
|
|
419
|
+
return (this.startingJobs +
|
|
420
|
+
(countUserId === null ? 0 : jobRunRegistry.countRunning(countUserId)) +
|
|
421
|
+
sessionless);
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* Number of live (unfinished) runs currently executing for a workflow. Used
|
|
425
|
+
* to enforce the per-workflow concurrency limit so non-concurrent runs stay
|
|
426
|
+
* sequential and their live node updates don't clobber each other in the
|
|
427
|
+
* editor. Counted the same way as {@link inFlightJobCount}: the registry
|
|
428
|
+
* across connections, plus this connection's sessionless entries.
|
|
429
|
+
*/
|
|
430
|
+
countActiveJobsForWorkflow(workflowId) {
|
|
431
|
+
if (!workflowId) {
|
|
432
|
+
return 0;
|
|
433
|
+
}
|
|
434
|
+
const workflowUserId = this.session.userId;
|
|
435
|
+
let count = workflowUserId === null
|
|
436
|
+
? 0
|
|
437
|
+
: jobRunRegistry.countRunningForWorkflow(workflowUserId, workflowId);
|
|
438
|
+
for (const job of this.activeJobs.values()) {
|
|
439
|
+
if (!job.runSession && job.workflowId === workflowId && !job.finished) {
|
|
440
|
+
count++;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
return count;
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* The per-workflow concurrency limit a given run is subject to: concurrency
|
|
447
|
+
* opt-in runs share the configurable {@link getMaxConcurrentRunsPerWorkflow}
|
|
448
|
+
* cap; everything else stays strictly sequential (one run per workflow) so
|
|
449
|
+
* live node updates don't clobber the editor.
|
|
450
|
+
*/
|
|
451
|
+
perWorkflowLimitFor(req) {
|
|
452
|
+
return req.concurrent
|
|
453
|
+
? this.deps.getMaxConcurrentRunsPerWorkflow()
|
|
454
|
+
: Promise.resolve(1);
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* Read-only view of the same admission counters used by runJob. This does
|
|
458
|
+
* not reserve a slot or mutate the queue; lifecycle preflight can therefore
|
|
459
|
+
* report likely queueing without changing current execution behavior.
|
|
460
|
+
*/
|
|
461
|
+
async getSdkExecutionCapacitySnapshot(input) {
|
|
462
|
+
const [maxConcurrentJobs, maxConcurrentRunsForWorkflow] = await Promise.all([
|
|
463
|
+
this.deps.getMaxConcurrentJobs(),
|
|
464
|
+
this.perWorkflowLimitFor({ concurrent: input.concurrent })
|
|
465
|
+
]);
|
|
466
|
+
const inFlightJobs = this.inFlightJobCount;
|
|
467
|
+
const workflowInFlightJobs = this.countActiveJobsForWorkflow(input.workflowId);
|
|
468
|
+
return {
|
|
469
|
+
inFlightJobs,
|
|
470
|
+
maxConcurrentJobs,
|
|
471
|
+
queuedJobs: this.jobQueue.size,
|
|
472
|
+
workflowInFlightJobs,
|
|
473
|
+
maxConcurrentRunsForWorkflow,
|
|
474
|
+
likelyQueued: inFlightJobs >= maxConcurrentJobs ||
|
|
475
|
+
workflowInFlightJobs >= maxConcurrentRunsForWorkflow
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
/**
|
|
479
|
+
* Best-effort pre-run cost estimate for a graph, in USD. Nodes the estimator
|
|
480
|
+
* cannot price contribute nothing, so the figure is a floor — good enough to
|
|
481
|
+
* stop a run that would obviously blow the budget, and never a reason to
|
|
482
|
+
* refuse one it cannot price.
|
|
483
|
+
*/
|
|
484
|
+
estimateRunCost(req) {
|
|
485
|
+
try {
|
|
486
|
+
const estimate = this.estimateGraphCost(req);
|
|
487
|
+
if (!estimate)
|
|
488
|
+
return 0;
|
|
489
|
+
return Number.isFinite(estimate.total) ? estimate.total : 0;
|
|
490
|
+
}
|
|
491
|
+
catch (err) {
|
|
492
|
+
this.session.logError("run cost estimate failed", err);
|
|
493
|
+
return 0;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
/**
|
|
497
|
+
* The graph estimate both gates read. One implementation so the budget gate,
|
|
498
|
+
* the credit gate and the editor's cost panel price a run the same way —
|
|
499
|
+
* including fan-out: a node asked for ten images is priced at ten, through
|
|
500
|
+
* the same `nodeExpectedQuantity` the panel uses.
|
|
501
|
+
*/
|
|
502
|
+
estimateGraphCost(req) {
|
|
503
|
+
const nodes = req.graph?.nodes;
|
|
504
|
+
if (!nodes || !this.session.getNodeMetadata)
|
|
505
|
+
return null;
|
|
506
|
+
const priced = nodes.map((node) => ({
|
|
507
|
+
id: String(node.id),
|
|
508
|
+
type: String(node.type),
|
|
509
|
+
data: (node.data ?? {})
|
|
510
|
+
}));
|
|
511
|
+
return estimateWorkflowCost({
|
|
512
|
+
nodes: priced,
|
|
513
|
+
getMetadata: (nodeType) => this.session.getNodeMetadata?.(nodeType),
|
|
514
|
+
// Prices the model picked on a generic node (e.g. a FAL or kie model on
|
|
515
|
+
// nodetool.image.TextToImage), which node-type metadata alone cannot.
|
|
516
|
+
// Same lookup the editor's cost preview uses.
|
|
517
|
+
getModelPrice: getModelUnitPrice,
|
|
518
|
+
// A per-second model bills the clip it is asked for, so the duration
|
|
519
|
+
// and resolution the node states have to reach the price lookup.
|
|
520
|
+
getParams: (node) => extractPricingParams(node.data),
|
|
521
|
+
quantities: Object.fromEntries(priced.map((node) => [
|
|
522
|
+
node.id,
|
|
523
|
+
nodeExpectedQuantity(node.data)
|
|
524
|
+
]))
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
/**
|
|
528
|
+
* The slice of a run that spends through NodeTool's managed provider —
|
|
529
|
+
* the only spend the credit balance meters. BYOK nodes are excluded on
|
|
530
|
+
* purpose: their cost rides the user's own keys.
|
|
531
|
+
*/
|
|
532
|
+
estimateNodetoolSpend(req) {
|
|
533
|
+
try {
|
|
534
|
+
const estimate = this.estimateGraphCost(req);
|
|
535
|
+
if (!estimate) {
|
|
536
|
+
return { usesNodetool: false, estimatedUsd: 0, models: [] };
|
|
537
|
+
}
|
|
538
|
+
const items = estimate.items.filter((item) => item.provider === "nodetool");
|
|
539
|
+
const total = items.reduce((sum, item) => sum +
|
|
540
|
+
(Number.isFinite(item.estimated_cost) ? item.estimated_cost : 0), 0);
|
|
541
|
+
const models = [
|
|
542
|
+
...new Set(items
|
|
543
|
+
.map((item) => item.model)
|
|
544
|
+
.filter((model) => Boolean(model)))
|
|
545
|
+
];
|
|
546
|
+
return { usesNodetool: items.length > 0, estimatedUsd: total, models };
|
|
547
|
+
}
|
|
548
|
+
catch (err) {
|
|
549
|
+
this.session.logError("nodetool spend estimate failed", err);
|
|
550
|
+
return { usesNodetool: false, estimatedUsd: 0, models: [] };
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
/** Tell the client a run was refused, in the shape a failed job takes. */
|
|
554
|
+
refuseRun(req, jobId, code, error) {
|
|
555
|
+
this.session.sendDetached({
|
|
556
|
+
type: "job_update",
|
|
557
|
+
status: "failed",
|
|
558
|
+
job_id: jobId,
|
|
559
|
+
workflow_id: req.workflow_id ?? null,
|
|
560
|
+
error,
|
|
561
|
+
error_code: code
|
|
562
|
+
});
|
|
563
|
+
return false;
|
|
564
|
+
}
|
|
565
|
+
/**
|
|
566
|
+
* The version number a stale client claimed, if the app really has it —
|
|
567
|
+
* otherwise null, and the claim is unsupportable.
|
|
568
|
+
*
|
|
569
|
+
* Version history is per app and short (one row per publish), so listing it
|
|
570
|
+
* is cheap; the ceiling exists only because the model helper takes a limit,
|
|
571
|
+
* and it has to be high enough that an old claim is never truncated away by
|
|
572
|
+
* the newest releases.
|
|
573
|
+
*/
|
|
574
|
+
async claimedApplicationVersion(applicationId, claimed, userId) {
|
|
575
|
+
if (claimed == null)
|
|
576
|
+
return null;
|
|
577
|
+
const versions = await listApplicationVersions(applicationId, 10_000, userId);
|
|
578
|
+
return versions.some((v) => v.version === claimed) ? claimed : null;
|
|
579
|
+
}
|
|
580
|
+
/**
|
|
581
|
+
* Gate an app's run on its spend budget. Runs of a published app execute with
|
|
582
|
+
* the creator's secrets, so this refuses before the job exists rather than
|
|
583
|
+
* reporting an overspend afterwards. Returns false when the run was refused
|
|
584
|
+
* (the client has already been told why).
|
|
585
|
+
*
|
|
586
|
+
* `application_id` arrives on the wire, so before any of that the app has to
|
|
587
|
+
* be one this connection's user owns. Honouring the id as sent let a client
|
|
588
|
+
* name a stranger's app and spend their budget — and pollute their release
|
|
589
|
+
* telemetry, which is the same ledger.
|
|
590
|
+
*/
|
|
591
|
+
async admitApplicationRun(req) {
|
|
592
|
+
const applicationId = req.application_id;
|
|
593
|
+
if (!applicationId)
|
|
594
|
+
return true;
|
|
595
|
+
const jobId = req.job_id ?? randomUUID();
|
|
596
|
+
req.job_id = jobId;
|
|
597
|
+
// The connection's authenticated user, not `req.user_id`: the request body
|
|
598
|
+
// is the thing being authorized, so it cannot supply the identity that
|
|
599
|
+
// authorizes it.
|
|
600
|
+
const userId = this.session.requireUserId();
|
|
601
|
+
// Authorization sits outside the try below, which swallows a ledger outage
|
|
602
|
+
// on purpose. Metering fails open; ownership fails closed — a lookup this
|
|
603
|
+
// never completed is not permission to bill the app it names.
|
|
604
|
+
let owned = false;
|
|
605
|
+
try {
|
|
606
|
+
const application = await Application.findById(applicationId);
|
|
607
|
+
owned = application?.user_id === userId;
|
|
608
|
+
}
|
|
609
|
+
catch (err) {
|
|
610
|
+
this.session.logError("application ownership check failed", err);
|
|
611
|
+
}
|
|
612
|
+
if (!owned) {
|
|
613
|
+
log.warn("Run refused: application not owned by this user", {
|
|
614
|
+
applicationId,
|
|
615
|
+
jobId,
|
|
616
|
+
userId
|
|
617
|
+
});
|
|
618
|
+
// Applications are owned by one user and there is no path today that
|
|
619
|
+
// serves someone else's app to run — `releasedApplicationDocument`
|
|
620
|
+
// itself requires ownership — so refusing cannot break a legitimate
|
|
621
|
+
// run, and it is the only answer that keeps the budget a hard stop.
|
|
622
|
+
return this.refuseRun(req, jobId, ApiErrorCode.NOT_FOUND, "Application not found");
|
|
623
|
+
}
|
|
624
|
+
try {
|
|
625
|
+
const estimatedUsd = this.estimateRunCost(req);
|
|
626
|
+
// The client says whether this is a release run or a draft run; the
|
|
627
|
+
// server decides which release the ledger records. A number taken on
|
|
628
|
+
// faith would let a run bill itself to a version it never executed, and
|
|
629
|
+
// the ledger is also the release telemetry — so a claim is only honoured
|
|
630
|
+
// below once the server has found that version in the app's history.
|
|
631
|
+
const released = req.application_version == null
|
|
632
|
+
? null
|
|
633
|
+
: await releasedApplicationVersion(applicationId, userId);
|
|
634
|
+
if (req.application_version != null && !released) {
|
|
635
|
+
// A release run of an app that has released nothing. The claim is
|
|
636
|
+
// unsupportable rather than merely stale, and letting it through would
|
|
637
|
+
// file the run in the telemetry ledger as a release that never shipped.
|
|
638
|
+
return this.refuseRun(req, jobId, ApiErrorCode.INVALID_INPUT, "This app has no released version to run");
|
|
639
|
+
}
|
|
640
|
+
// Which version the ledger row belongs to. The release is the default,
|
|
641
|
+
// because that is what a current client runs.
|
|
642
|
+
let version = released?.version ?? null;
|
|
643
|
+
if (released && released.version !== req.application_version) {
|
|
644
|
+
log.warn("Run claimed a version other than the released one", {
|
|
645
|
+
applicationId,
|
|
646
|
+
jobId,
|
|
647
|
+
claimed: req.application_version,
|
|
648
|
+
released: released.version
|
|
649
|
+
});
|
|
650
|
+
// A client that loaded the app before the newest release still holds
|
|
651
|
+
// that older snapshot and is about to execute it. Filing the run under
|
|
652
|
+
// the current release would credit v2's metrics and budget with a run
|
|
653
|
+
// of v1, so the row follows what actually ran — but only once the
|
|
654
|
+
// server has confirmed the claimed version is a real version of this
|
|
655
|
+
// app. That check is what keeps the client from picking its own
|
|
656
|
+
// attribution: it can name a version it once had, not one it invents.
|
|
657
|
+
const claimed = await this.claimedApplicationVersion(applicationId, req.application_version, userId);
|
|
658
|
+
if (!claimed) {
|
|
659
|
+
return this.refuseRun(req, jobId, ApiErrorCode.INVALID_INPUT, `This app has no version ${req.application_version} to run`);
|
|
660
|
+
}
|
|
661
|
+
version = claimed;
|
|
662
|
+
// The run proceeds, but the client is stale and has no other way to
|
|
663
|
+
// learn it: nothing in a job's updates mentions releases.
|
|
664
|
+
this.session.sendDetached({
|
|
665
|
+
type: "notification",
|
|
666
|
+
node_id: "",
|
|
667
|
+
severity: "warning",
|
|
668
|
+
workflow_id: req.workflow_id ?? null,
|
|
669
|
+
content: `Running version ${claimed} of this app; version ${released.version} has since been released. Reload to get the latest.`
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
// Reserving claims the run against the budget in the same transaction
|
|
673
|
+
// that checks it, so concurrent runs of one app cannot each read a total
|
|
674
|
+
// that excludes the others and all be admitted.
|
|
675
|
+
// The ledger holds one row per (application, invocation), so two runs
|
|
676
|
+
// racing on one id lose the insert rather than double-spend. The loser
|
|
677
|
+
// is refused like any other rejected run instead of surfacing a driver
|
|
678
|
+
// error to the visitor.
|
|
679
|
+
let decision;
|
|
680
|
+
try {
|
|
681
|
+
decision = await reserveInvocation({
|
|
682
|
+
applicationId,
|
|
683
|
+
version,
|
|
684
|
+
invocationId: jobId,
|
|
685
|
+
operationId: req.operation_id ?? undefined,
|
|
686
|
+
estimatedUsd,
|
|
687
|
+
requireFiniteBudget: this.session.appSession !== null
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
catch (error) {
|
|
691
|
+
log.warn("Application run ledger rejected the reservation", {
|
|
692
|
+
applicationId,
|
|
693
|
+
jobId,
|
|
694
|
+
error: String(error)
|
|
695
|
+
});
|
|
696
|
+
return this.refuseRun(req, jobId, ApiErrorCode.INVALID_INPUT, "This app run named a run id that is already in use");
|
|
697
|
+
}
|
|
698
|
+
if (!decision.allowed) {
|
|
699
|
+
log.warn("Run refused by application budget", {
|
|
700
|
+
applicationId,
|
|
701
|
+
jobId,
|
|
702
|
+
reason: decision.reason
|
|
703
|
+
});
|
|
704
|
+
return this.refuseRun(req, jobId, ApiErrorCode.BUDGET_EXCEEDED, decision.reason);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
catch (err) {
|
|
708
|
+
// A ledger that is unavailable must not take runs down with it; the
|
|
709
|
+
// refusals above are the only paths that block, and they return rather
|
|
710
|
+
// than throw so an outage can never swallow one.
|
|
711
|
+
this.session.logError("application budget check failed", err);
|
|
712
|
+
}
|
|
713
|
+
return true;
|
|
714
|
+
}
|
|
715
|
+
/**
|
|
716
|
+
* Gate a run on the user's credit balance — but only the part of the run
|
|
717
|
+
* that spends through NodeTool's managed provider. A graph with no
|
|
718
|
+
* `nodetool` models passes untouched (BYOK stays unmetered); one with them
|
|
719
|
+
* is refused when it names a model this server does not sell, or when the
|
|
720
|
+
* balance is empty or can't cover the estimate. Estimates are floors, so an
|
|
721
|
+
* empty balance refuses even a 0-estimate nodetool call. Fails open on gate
|
|
722
|
+
* errors, like the application-budget gate above.
|
|
723
|
+
*/
|
|
724
|
+
async admitCreditRun(req) {
|
|
725
|
+
const { usesNodetool, estimatedUsd, models } = this.estimateNodetoolSpend(req);
|
|
726
|
+
if (!usesNodetool)
|
|
727
|
+
return true;
|
|
728
|
+
// Pin the job id now so the reservation taken here can be released at the
|
|
729
|
+
// run's terminal state (and on cancel-while-queued) under the same key.
|
|
730
|
+
req.job_id ??= randomUUID();
|
|
731
|
+
const decision = await admitSpend(this.session.requireUserId(), estimatedUsd, models);
|
|
732
|
+
if (!decision.allowed) {
|
|
733
|
+
return this.refuseRun(req, req.job_id, decision.refusal === "model_not_allowed"
|
|
734
|
+
? ApiErrorCode.MODEL_NOT_AVAILABLE
|
|
735
|
+
: ApiErrorCode.BUDGET_EXCEEDED, decision.reason);
|
|
736
|
+
}
|
|
737
|
+
reserveSpend(this.session.requireUserId(), req.job_id, estimatedUsd);
|
|
738
|
+
return true;
|
|
739
|
+
}
|
|
740
|
+
/**
|
|
741
|
+
* Narrow a run started by a deployed app's visitor to the one run the
|
|
742
|
+
* session allows, or refuse it.
|
|
743
|
+
*
|
|
744
|
+
* The connection authenticates as the app's owner, so `admitApplicationRun`
|
|
745
|
+
* below would happily approve anything it names — ownership is what that
|
|
746
|
+
* check tests, and a visitor's session passes it. This is the check that
|
|
747
|
+
* actually confines them, and it runs first: what comes out is built from
|
|
748
|
+
* the signed session and the release, not from the request.
|
|
749
|
+
*
|
|
750
|
+
* The run executes only the release version the signed session names. A
|
|
751
|
+
* later publish invalidates the session for execution, so the visitor must
|
|
752
|
+
* reload and mint a session for the new release rather than run its graphs.
|
|
753
|
+
*/
|
|
754
|
+
async confineAppSessionRun(req) {
|
|
755
|
+
const scope = this.session.appSession;
|
|
756
|
+
if (!scope)
|
|
757
|
+
return req;
|
|
758
|
+
// The client names the run so it can follow its own frames, but the id it
|
|
759
|
+
// names is also the ledger key a job command is authorized against. An id
|
|
760
|
+
// that is already a job or already in a ledger would let a visitor file a
|
|
761
|
+
// row under someone else's run and then command it, so a taken id — and a
|
|
762
|
+
// shape that is not a generated id at all — is refused rather than
|
|
763
|
+
// silently replaced with one the client would never recognize.
|
|
764
|
+
const requested = req.job_id ?? "";
|
|
765
|
+
const jobId = requested === "" ? randomUUID() : requested;
|
|
766
|
+
const deny = (code, error) => {
|
|
767
|
+
this.refuseRun(req, jobId, code, error);
|
|
768
|
+
return null;
|
|
769
|
+
};
|
|
770
|
+
if (requested !== "") {
|
|
771
|
+
if (!UUID_PATTERN.test(requested)) {
|
|
772
|
+
return deny(ApiErrorCode.INVALID_INPUT, "This app run named an invalid run id");
|
|
773
|
+
}
|
|
774
|
+
if ((await Job.get(requested)) !== null ||
|
|
775
|
+
(await invocationIdInUse(requested))) {
|
|
776
|
+
return deny(ApiErrorCode.INVALID_INPUT, "This app run named a run id that is already in use");
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
const release = await releasedApplicationRelease(scope.applicationId, this.session.requireUserId());
|
|
780
|
+
if (!release) {
|
|
781
|
+
return deny(ApiErrorCode.NOT_FOUND, "This app is not available");
|
|
782
|
+
}
|
|
783
|
+
// A document that no longer parses and one a public link may not serve get
|
|
784
|
+
// the same answer, so the visitor learns nothing about which it hit.
|
|
785
|
+
const parsedRelease = applicationReleaseResponse.safeParse(release);
|
|
786
|
+
if (!parsedRelease.success || releaseBlockedReason(parsedRelease.data)) {
|
|
787
|
+
return deny(ApiErrorCode.NOT_FOUND, "This app is not available");
|
|
788
|
+
}
|
|
789
|
+
if (release.version !== scope.version) {
|
|
790
|
+
return deny(ApiErrorCode.INVALID_INPUT, "This app has been updated. Reload the page before running it.");
|
|
791
|
+
}
|
|
792
|
+
const confined = confineRunRequest(req, scope, release);
|
|
793
|
+
if (isRunRefusal(confined)) {
|
|
794
|
+
return deny(ApiErrorCode.INVALID_INPUT, confined.refused);
|
|
795
|
+
}
|
|
796
|
+
confined.job_id = jobId;
|
|
797
|
+
return confined;
|
|
798
|
+
}
|
|
799
|
+
/**
|
|
800
|
+
* Entry point for the "run_job" command. Starts the run immediately when the
|
|
801
|
+
* client is under its concurrency cap, otherwise queues it (FIFO) and emits a
|
|
802
|
+
* `queued` job update. Queued runs start automatically as active jobs finish.
|
|
803
|
+
*/
|
|
804
|
+
async runJob(incoming) {
|
|
805
|
+
const req = await this.confineAppSessionRun(incoming);
|
|
806
|
+
if (!req)
|
|
807
|
+
return;
|
|
808
|
+
req._accepted_at_ms ??= performance.now();
|
|
809
|
+
if (!(await this.admitApplicationRun(req)))
|
|
810
|
+
return;
|
|
811
|
+
if (!(await this.admitCreditRun(req)))
|
|
812
|
+
return;
|
|
813
|
+
const max = await this.deps.getMaxConcurrentJobs();
|
|
814
|
+
const perWorkflowMax = await this.perWorkflowLimitFor(req);
|
|
815
|
+
// Queue the run when over the global cap, or when this workflow already has
|
|
816
|
+
// its per-workflow limit of runs in flight — 1 for normal runs, or the
|
|
817
|
+
// configurable MAX_CONCURRENT_RUNS_PER_WORKFLOW for runs that opt into
|
|
818
|
+
// concurrency. Reserve the slot synchronously (after the awaits above) so
|
|
819
|
+
// two run_job commands can't both observe a free slot before either registers.
|
|
820
|
+
if (this.inFlightJobCount >= max ||
|
|
821
|
+
this.countActiveJobsForWorkflow(req.workflow_id) >= perWorkflowMax) {
|
|
822
|
+
await this.enqueueJob(req);
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
this.startingJobs++;
|
|
826
|
+
await this.startJob(req);
|
|
827
|
+
}
|
|
828
|
+
/** Queue a run that can't start yet, persist it, and notify the client. */
|
|
829
|
+
async enqueueJob(req) {
|
|
830
|
+
const jobId = req.job_id ?? randomUUID();
|
|
831
|
+
req.job_id = jobId;
|
|
832
|
+
const position = this.jobQueue.enqueue(req);
|
|
833
|
+
log.info("Job queued", { jobId, position });
|
|
834
|
+
// Persist the queued run so it shows in jobs.list (Queue panel, reload,
|
|
835
|
+
// other tabs). Best-effort, mirroring startJobInner's persistence. It flips
|
|
836
|
+
// to "running" in startJobInner when a slot frees.
|
|
837
|
+
if (resolveRunJobExecutionOptions(req.execution_options, req.require_terminal_result === true).persistence === "job") {
|
|
838
|
+
try {
|
|
839
|
+
const existing = await Job.get(jobId);
|
|
840
|
+
if (!existing) {
|
|
841
|
+
await Job.create({
|
|
842
|
+
id: jobId,
|
|
843
|
+
workflow_id: req.workflow_id ?? "",
|
|
844
|
+
user_id: resolveRunJobUserId(req.user_id, this.session.requireUserId()),
|
|
845
|
+
status: "queued",
|
|
846
|
+
name: req.job_name ?? "",
|
|
847
|
+
params: req.params ?? {},
|
|
848
|
+
graph: req.graph ?? { nodes: [], edges: [] }
|
|
849
|
+
});
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
catch (err) {
|
|
853
|
+
this.session.logError("enqueue persistence failed", err);
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
this.session.sendDetached({
|
|
857
|
+
type: "job_update",
|
|
858
|
+
status: "queued",
|
|
859
|
+
job_id: jobId,
|
|
860
|
+
workflow_id: req.workflow_id ?? null,
|
|
861
|
+
queue_position: position,
|
|
862
|
+
message: `Queued (#${position})`
|
|
863
|
+
});
|
|
864
|
+
}
|
|
865
|
+
/**
|
|
866
|
+
* Start the next queued run (if any) after a job slot frees up, and refresh
|
|
867
|
+
* the reported positions of the runs still waiting.
|
|
868
|
+
*/
|
|
869
|
+
drainQueue() {
|
|
870
|
+
void (async () => {
|
|
871
|
+
const max = await this.deps
|
|
872
|
+
.getMaxConcurrentJobs()
|
|
873
|
+
.catch(() => this.deps.defaultMaxConcurrentJobs);
|
|
874
|
+
const perWorkflowMax = await this.deps
|
|
875
|
+
.getMaxConcurrentRunsPerWorkflow()
|
|
876
|
+
.catch(() => this.deps.defaultMaxConcurrentRunsPerWorkflow);
|
|
877
|
+
// Fill free slots with the first queued run whose workflow is still under
|
|
878
|
+
// its per-workflow limit (1 for normal runs, perWorkflowMax for opted-in
|
|
879
|
+
// concurrent runs). startJob registers the job before it returns, so the
|
|
880
|
+
// next iteration sees it as in-flight.
|
|
881
|
+
while (this.inFlightJobCount < max) {
|
|
882
|
+
const candidate = this.jobQueue
|
|
883
|
+
.positions()
|
|
884
|
+
.find((p) => this.countActiveJobsForWorkflow(p.workflowId) <
|
|
885
|
+
(p.concurrent ? perWorkflowMax : 1));
|
|
886
|
+
if (!candidate) {
|
|
887
|
+
break;
|
|
888
|
+
}
|
|
889
|
+
const next = this.jobQueue.remove(candidate.jobId);
|
|
890
|
+
if (!next) {
|
|
891
|
+
break;
|
|
892
|
+
}
|
|
893
|
+
// Reserve the slot synchronously, mirroring runJob, so a concurrent
|
|
894
|
+
// run_job/drain can't also claim it before startJob registers.
|
|
895
|
+
this.startingJobs++;
|
|
896
|
+
const nextId = next.job_id;
|
|
897
|
+
if (nextId) {
|
|
898
|
+
this.dequeuedJobs.add(nextId);
|
|
899
|
+
}
|
|
900
|
+
try {
|
|
901
|
+
await this.startJob(next);
|
|
902
|
+
}
|
|
903
|
+
catch (err) {
|
|
904
|
+
// The dequeued job threw before it could register/stream. Don't
|
|
905
|
+
// silently lose it: tell the client this run failed, then keep
|
|
906
|
+
// draining so the rest of the queue still progresses.
|
|
907
|
+
this.session.logError("startJob (from queue) failed", err);
|
|
908
|
+
await this.session.send({
|
|
909
|
+
type: "job_update",
|
|
910
|
+
status: "failed",
|
|
911
|
+
job_id: next.job_id ?? null,
|
|
912
|
+
workflow_id: next.workflow_id ?? null,
|
|
913
|
+
error: formatSanitizedError(err)
|
|
914
|
+
});
|
|
915
|
+
}
|
|
916
|
+
finally {
|
|
917
|
+
if (nextId) {
|
|
918
|
+
this.dequeuedJobs.delete(nextId);
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
this.broadcastQueuePositions();
|
|
923
|
+
})();
|
|
924
|
+
}
|
|
925
|
+
/** Push updated queue positions to every still-waiting run. */
|
|
926
|
+
broadcastQueuePositions() {
|
|
927
|
+
for (const { jobId, workflowId, position } of this.jobQueue.positions()) {
|
|
928
|
+
this.session.sendDetached({
|
|
929
|
+
type: "job_update",
|
|
930
|
+
status: "queued",
|
|
931
|
+
job_id: jobId,
|
|
932
|
+
workflow_id: workflowId,
|
|
933
|
+
queue_position: position,
|
|
934
|
+
message: `Queued (#${position})`
|
|
935
|
+
});
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
async startJob(req) {
|
|
939
|
+
// The caller (runJob/drainQueue) reserved a concurrency slot via
|
|
940
|
+
// startingJobs++. Release it exactly once here: the slot is handed off to
|
|
941
|
+
// activeJobs on successful registration, or freed on early return/throw.
|
|
942
|
+
let slotReleased = false;
|
|
943
|
+
const releaseSlot = () => {
|
|
944
|
+
if (!slotReleased) {
|
|
945
|
+
slotReleased = true;
|
|
946
|
+
this.startingJobs = Math.max(0, this.startingJobs - 1);
|
|
947
|
+
}
|
|
948
|
+
};
|
|
949
|
+
try {
|
|
950
|
+
await this.startJobInner(req, releaseSlot);
|
|
951
|
+
}
|
|
952
|
+
finally {
|
|
953
|
+
// Safety net: if startJobInner returned/threw without registering, the
|
|
954
|
+
// slot is freed so it doesn't leak and permanently shrink the cap.
|
|
955
|
+
releaseSlot();
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
async startJobInner(req, releaseSlot) {
|
|
959
|
+
const userId = resolveRunJobUserId(req.user_id, this.session.requireUserId());
|
|
960
|
+
const workflowId = req.workflow_id ?? null;
|
|
961
|
+
const jobId = req.job_id ?? randomUUID();
|
|
962
|
+
let projectId = req.project_id ?? null;
|
|
963
|
+
// Inline graphs do not require a database lookup. This matters for
|
|
964
|
+
// hermetic callers that provide a workflow_id only as a correlation id.
|
|
965
|
+
// Saved-workflow requests still resolve the persisted project owner here.
|
|
966
|
+
if (workflowId && !req.graph) {
|
|
967
|
+
const workflow = await Workflow.find(userId, workflowId);
|
|
968
|
+
if (workflow) {
|
|
969
|
+
if (projectId !== null && projectId !== workflow.project_id) {
|
|
970
|
+
await this.emitBeforeRunFailure(jobId, workflowId, new Error("Workflow is owned by another project"), true);
|
|
971
|
+
return;
|
|
972
|
+
}
|
|
973
|
+
projectId = workflow.project_id;
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
const executionOptions = resolveRunJobExecutionOptions(req.execution_options, req.require_terminal_result === true);
|
|
977
|
+
const acceptedAt = req._accepted_at_ms ?? performance.now();
|
|
978
|
+
const preparationStartedAt = performance.now();
|
|
979
|
+
let phaseStartedAt = preparationStartedAt;
|
|
980
|
+
const rawGraph = await this.getRawGraph(req);
|
|
981
|
+
const graphLoadedMs = performance.now() - phaseStartedAt;
|
|
982
|
+
// Hydrate the graph (resolves node types from the registry)
|
|
983
|
+
phaseStartedAt = performance.now();
|
|
984
|
+
const graph = await this.hydrateGraph(rawGraph);
|
|
985
|
+
const graphHydratedMs = performance.now() - phaseStartedAt;
|
|
986
|
+
// The kernel keys terminal outputs by node.name. For SDK runs, align output
|
|
987
|
+
// node names with their public interface names before execution so the
|
|
988
|
+
// authoritative terminal snapshot addresses the same pins as output_update.
|
|
989
|
+
if (req.require_terminal_result) {
|
|
990
|
+
for (const node of graph.nodes) {
|
|
991
|
+
if (!node.type.startsWith("nodetool.output."))
|
|
992
|
+
continue;
|
|
993
|
+
const properties = node.properties;
|
|
994
|
+
const publicName = properties?.name;
|
|
995
|
+
if (isString(publicName) && publicName.trim().length > 0) {
|
|
996
|
+
node.name = publicName;
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
phaseStartedAt = performance.now();
|
|
1001
|
+
if (this.deps.beforeRunJob) {
|
|
1002
|
+
try {
|
|
1003
|
+
await this.deps.beforeRunJob(graph);
|
|
1004
|
+
}
|
|
1005
|
+
catch (err) {
|
|
1006
|
+
await this.emitBeforeRunFailure(jobId, workflowId, err, executionOptions.persistence === "job");
|
|
1007
|
+
return;
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
const preRunMs = performance.now() - phaseStartedAt;
|
|
1011
|
+
const workspace = this.session.workspaceResolver
|
|
1012
|
+
? await this.session.workspaceResolver(workflowId ?? null, userId)
|
|
1013
|
+
: null;
|
|
1014
|
+
const context = createRuntimeContext({
|
|
1015
|
+
jobId,
|
|
1016
|
+
workflowId,
|
|
1017
|
+
projectId,
|
|
1018
|
+
userId,
|
|
1019
|
+
workspace,
|
|
1020
|
+
assetOutputMode: this.session.mode === "text" ? "data_uri" : "temp_url",
|
|
1021
|
+
persistOutputAssets: executionOptions.assetPersistence === "auto"
|
|
1022
|
+
});
|
|
1023
|
+
// Expose executor/node-type resolution on the context so that
|
|
1024
|
+
// sub-workflow nodes (WorkflowNode) can create child runners.
|
|
1025
|
+
context.setResolveExecutor((node) => this.session.resolveExecutor(node));
|
|
1026
|
+
if (this.session.resolveNodeType) {
|
|
1027
|
+
const resolverObj = isFunctionValue(this.session.resolveNodeType)
|
|
1028
|
+
? { resolveNodeType: this.session.resolveNodeType }
|
|
1029
|
+
: this.session.resolveNodeType;
|
|
1030
|
+
context.setResolveNodeType((nodeType) => resolverObj.resolveNodeType(nodeType));
|
|
1031
|
+
}
|
|
1032
|
+
// Persistence runs BEFORE the session exists, because
|
|
1033
|
+
// `ExecutionSession.create()` starts the kernel: a job cancelled while it
|
|
1034
|
+
// sat in the queue must never execute, and the only way to guarantee that
|
|
1035
|
+
// is to check before anything can run. (The DB-only cancel path — tRPC
|
|
1036
|
+
// `jobs.cancel` — doesn't remove the job from `jobQueue`, so drainQueue
|
|
1037
|
+
// can still hand us a cancelled job.)
|
|
1038
|
+
phaseStartedAt = performance.now();
|
|
1039
|
+
// Which machine holds this run's session, for owner-aware reconnects and
|
|
1040
|
+
// cross-instance cancel. Null on a single-machine deployment.
|
|
1041
|
+
const instanceId = getInstanceId();
|
|
1042
|
+
if (executionOptions.persistence === "job") {
|
|
1043
|
+
try {
|
|
1044
|
+
const existing = await Job.get(jobId);
|
|
1045
|
+
if (existing) {
|
|
1046
|
+
if (existing.status === "cancelled") {
|
|
1047
|
+
log.info("Skipping start of cancelled job", { jobId });
|
|
1048
|
+
// Nothing was registered in activeJobs yet — free the reserved
|
|
1049
|
+
// slot and promote any queued run, matching every other slot
|
|
1050
|
+
// release (streamJobMessages finally, the chat-run finally).
|
|
1051
|
+
// Without it a cancelled-while-queued job leaves its slot idle
|
|
1052
|
+
// and the next queued run stalls.
|
|
1053
|
+
releaseSlot();
|
|
1054
|
+
this.drainQueue();
|
|
1055
|
+
this.session.sendDetached({
|
|
1056
|
+
type: "job_update",
|
|
1057
|
+
status: "cancelled",
|
|
1058
|
+
job_id: jobId,
|
|
1059
|
+
workflow_id: workflowId
|
|
1060
|
+
});
|
|
1061
|
+
return;
|
|
1062
|
+
}
|
|
1063
|
+
// Was persisted as "queued" while waiting for a slot — flip it to
|
|
1064
|
+
// running now that it's actually starting. The stamp goes on here
|
|
1065
|
+
// too: the queued row may have been written by another instance.
|
|
1066
|
+
if (existing.status !== "running" ||
|
|
1067
|
+
existing.runner_instance !== instanceId) {
|
|
1068
|
+
existing.markRunning();
|
|
1069
|
+
existing.runner_instance = instanceId;
|
|
1070
|
+
await existing.save();
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
else {
|
|
1074
|
+
await Job.create({
|
|
1075
|
+
id: jobId,
|
|
1076
|
+
workflow_id: workflowId ?? "",
|
|
1077
|
+
user_id: userId,
|
|
1078
|
+
status: "running",
|
|
1079
|
+
name: req.job_name ?? "",
|
|
1080
|
+
started_at: new Date().toISOString(),
|
|
1081
|
+
params: req.params ?? {},
|
|
1082
|
+
graph,
|
|
1083
|
+
project_id: projectId ?? "default",
|
|
1084
|
+
runner_instance: instanceId
|
|
1085
|
+
});
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
catch (error) {
|
|
1089
|
+
this.session.logError("runJob persistence failed", error);
|
|
1090
|
+
// Persistence is best-effort in TS runtime mode.
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
const persistenceMs = performance.now() - phaseStartedAt;
|
|
1094
|
+
// A5 (docs/RELIABILITY_TASKS.md Track A): the facade replaces the direct
|
|
1095
|
+
// `new WorkflowRunner` + later `runner.run()` pair — `graph` is already
|
|
1096
|
+
// hydrated/output-name-rewritten above, so this call re-hydrates it
|
|
1097
|
+
// (idempotent: `withExplicitNodeFlags`, used because this class has no
|
|
1098
|
+
// `NodeRegistry` of its own, passes every already-resolved field through
|
|
1099
|
+
// unchanged via `...node`) and starts the run immediately. `resolveExecutor`
|
|
1100
|
+
// is passed through as-is (not rebuilt from a registry+bridge) because
|
|
1101
|
+
// this class only ever holds the bootstrap-injected closure, never a
|
|
1102
|
+
// `NodeRegistry` instance.
|
|
1103
|
+
// Opt-in per request; a run that asked for no supervisor gets none, and a
|
|
1104
|
+
// supervisor that cannot be built leaves the run unsupervised rather than
|
|
1105
|
+
// failing it.
|
|
1106
|
+
const supervisor = await createRunSupervisor({
|
|
1107
|
+
supervise: req.supervise,
|
|
1108
|
+
supervisor: req.supervisor,
|
|
1109
|
+
context,
|
|
1110
|
+
defaultProvider: this.deps.defaults.provider,
|
|
1111
|
+
defaultModel: this.deps.defaults.model
|
|
1112
|
+
});
|
|
1113
|
+
const sessionOptions = {
|
|
1114
|
+
graph: toRawGraphInput(graph),
|
|
1115
|
+
resolveExecutor: (node) => this.session.resolveExecutor(node),
|
|
1116
|
+
bridgeFactory: async () => null,
|
|
1117
|
+
// This runner owns a long-lived shared bridge, so `bridgeFactory` hands
|
|
1118
|
+
// the session nothing to close. The run boundary still has to reach that
|
|
1119
|
+
// bridge — pass it explicitly.
|
|
1120
|
+
jobLifecycleBridge: this.session.pythonBridge ?? null,
|
|
1121
|
+
jobId,
|
|
1122
|
+
workflowId,
|
|
1123
|
+
context,
|
|
1124
|
+
params: req.params ?? {},
|
|
1125
|
+
projectId,
|
|
1126
|
+
validateNode: this.session.validateNode
|
|
1127
|
+
};
|
|
1128
|
+
if (supervisor) {
|
|
1129
|
+
sessionOptions.supervisor = supervisor;
|
|
1130
|
+
}
|
|
1131
|
+
// A graph this runtime cannot honour (unknown model, unregistered
|
|
1132
|
+
// provider, missing credential) is refused before the kernel starts.
|
|
1133
|
+
// Route it through the same terminal `job_update` a failed pre-run hook
|
|
1134
|
+
// uses: a bare throw reaches handleCommand as a generic `invalid_command`
|
|
1135
|
+
// the UI never associates with the job, so the run appears to spin
|
|
1136
|
+
// forever instead of failing with the reason.
|
|
1137
|
+
let session;
|
|
1138
|
+
try {
|
|
1139
|
+
session = await ExecutionSession.create(sessionOptions);
|
|
1140
|
+
}
|
|
1141
|
+
catch (err) {
|
|
1142
|
+
if (!isExecutionPreflightError(err))
|
|
1143
|
+
throw err;
|
|
1144
|
+
await this.emitBeforeRunFailure(jobId, workflowId, err, executionOptions.persistence === "job");
|
|
1145
|
+
return;
|
|
1146
|
+
}
|
|
1147
|
+
const active = {
|
|
1148
|
+
jobId,
|
|
1149
|
+
workflowId,
|
|
1150
|
+
context,
|
|
1151
|
+
session,
|
|
1152
|
+
graph,
|
|
1153
|
+
finished: false,
|
|
1154
|
+
status: "running",
|
|
1155
|
+
requireTerminalResult: req.require_terminal_result === true,
|
|
1156
|
+
executionOptions,
|
|
1157
|
+
timings: {
|
|
1158
|
+
acceptedAt,
|
|
1159
|
+
queueMs: Math.max(0, preparationStartedAt - acceptedAt),
|
|
1160
|
+
graphLoadedMs,
|
|
1161
|
+
graphHydratedMs,
|
|
1162
|
+
preRunMs,
|
|
1163
|
+
persistenceMs,
|
|
1164
|
+
kernelStartedAt: performance.now()
|
|
1165
|
+
},
|
|
1166
|
+
applicationId: req.application_id ?? null
|
|
1167
|
+
};
|
|
1168
|
+
// Decouple the run from this socket: from here on every frame carrying
|
|
1169
|
+
// this job_id is stamped with `job_seq` and buffered, so a client that
|
|
1170
|
+
// drops mid-run can `reconnect_job` from a fresh connection and replay
|
|
1171
|
+
// the tail — including the terminal job_update.
|
|
1172
|
+
//
|
|
1173
|
+
// Keyed on the connection's identity, not `userId`: every lookup
|
|
1174
|
+
// (`reconnect_job`, `cancel_job`, the slot counts) reads
|
|
1175
|
+
// `this.session.requireUserId()`, and a run opened under an explicit differing
|
|
1176
|
+
// `req.user_id` would be unreachable — no replay, and a cancel that
|
|
1177
|
+
// reports the job as not found.
|
|
1178
|
+
const runSession = jobRunRegistry.open(this.session.requireUserId(), jobId, workflowId, {
|
|
1179
|
+
cancel: () => session.cancel(),
|
|
1180
|
+
pushInput: (input, value, handle) => session.pushInput(input, value, handle),
|
|
1181
|
+
finishInputStream: (input, handle) => session.finishInputStream(input, handle),
|
|
1182
|
+
updateNodeProperties: (nodeId, properties) => session.updateNodeProperties(nodeId, properties)
|
|
1183
|
+
});
|
|
1184
|
+
runSession.attach(this.jobDeliveryTarget, runSession.lastSeq);
|
|
1185
|
+
active.runSession = runSession;
|
|
1186
|
+
this.activeJobs.set(jobId, active);
|
|
1187
|
+
// Slot ownership transfers from startingJobs to the registry's running
|
|
1188
|
+
// count now that the job is registered. Released before the DB check
|
|
1189
|
+
// below so the reservation and the registry entry never both count.
|
|
1190
|
+
releaseSlot();
|
|
1191
|
+
log.info("Job started", { jobId, workflowId });
|
|
1192
|
+
await this.settleRunAgainstLostConnection(runSession, jobId, executionOptions);
|
|
1193
|
+
// The run itself already started inside `ExecutionSession.create()`
|
|
1194
|
+
// above — `session.result` is the same never-rejecting terminal-result
|
|
1195
|
+
// promise `runner.run()` used to return.
|
|
1196
|
+
const executePromise = session.result;
|
|
1197
|
+
// `streamJobMessages` handles its own failures, but nothing awaits this
|
|
1198
|
+
// promise — attach a terminal handler so a bug there can never surface as
|
|
1199
|
+
// an unhandled rejection (which Node 22 turns into a process exit).
|
|
1200
|
+
active.streamTask = this.streamJobMessages(active, executePromise).catch((error) => {
|
|
1201
|
+
this.session.logError("job stream task failed", error);
|
|
1202
|
+
});
|
|
1203
|
+
}
|
|
1204
|
+
/**
|
|
1205
|
+
* A run whose start was mid-flight when `disconnect()` fired registers
|
|
1206
|
+
* AFTER that walk of `activeJobs`, so nothing detached it and nothing
|
|
1207
|
+
* armed its grace timer — it would execute unattended forever, delivering
|
|
1208
|
+
* into a dead target. Two fixes, both after registration:
|
|
1209
|
+
* - no live socket: detach, which starts the detach-grace countdown;
|
|
1210
|
+
* - the row already reads cancelled: `disconnect()`'s queue drain (or a
|
|
1211
|
+
* DB-only cancel) settled it while this run was starting, so cancel
|
|
1212
|
+
* the run rather than let it execute to completion under a row that
|
|
1213
|
+
* says it never did.
|
|
1214
|
+
*/
|
|
1215
|
+
async settleRunAgainstLostConnection(runSession, jobId, executionOptions) {
|
|
1216
|
+
const socketGone = !this.deps.isSocketConnected();
|
|
1217
|
+
if (socketGone) {
|
|
1218
|
+
runSession.detach(this.jobDeliveryTarget);
|
|
1219
|
+
}
|
|
1220
|
+
if (executionOptions.persistence !== "job")
|
|
1221
|
+
return;
|
|
1222
|
+
try {
|
|
1223
|
+
const job = await Job.get(jobId);
|
|
1224
|
+
if (job?.status === "cancelled") {
|
|
1225
|
+
log.info("Job was cancelled while starting, cancelling the run", {
|
|
1226
|
+
jobId
|
|
1227
|
+
});
|
|
1228
|
+
runSession.cancel();
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
catch (error) {
|
|
1232
|
+
this.session.logError("post-start cancellation check failed", error);
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
async streamJobMessages(active, executePromise) {
|
|
1236
|
+
// The drain loop has awaited operations (Asset.paginate, normalizeOutputValue,
|
|
1237
|
+
// sendMessage) that can throw. Guarantee the job slot is released and the
|
|
1238
|
+
// queue drains no matter what — otherwise one throw permanently leaks a
|
|
1239
|
+
// MAX_CONCURRENT_JOBS slot and stalls every queued run.
|
|
1240
|
+
try {
|
|
1241
|
+
await this._streamJobMessagesInner(active, executePromise);
|
|
1242
|
+
}
|
|
1243
|
+
catch (error) {
|
|
1244
|
+
// Without this catch the rejection escaped entirely: the caller only
|
|
1245
|
+
// assigns `active.streamTask` and never awaits it, so Node's default
|
|
1246
|
+
// unhandledRejection behaviour terminated the process — and none of the
|
|
1247
|
+
// terminal bookkeeping below ran, leaving the DB row stuck at "running",
|
|
1248
|
+
// the client UI spinning, and the app ledger holding the estimate.
|
|
1249
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1250
|
+
this.session.logError("job message streaming failed", error);
|
|
1251
|
+
active.finished = true;
|
|
1252
|
+
active.status = "failed";
|
|
1253
|
+
active.error = message;
|
|
1254
|
+
try {
|
|
1255
|
+
await this.session.send({
|
|
1256
|
+
type: "job_update",
|
|
1257
|
+
status: "failed",
|
|
1258
|
+
job_id: active.jobId,
|
|
1259
|
+
workflow_id: active.workflowId,
|
|
1260
|
+
error: message
|
|
1261
|
+
});
|
|
1262
|
+
}
|
|
1263
|
+
catch (sendError) {
|
|
1264
|
+
this.session.logError("terminal job_update send failed", sendError);
|
|
1265
|
+
}
|
|
1266
|
+
await this.persistTerminalJobStatus(active);
|
|
1267
|
+
await this.settleApplicationInvocation(active);
|
|
1268
|
+
releaseSpend(this.session.requireUserId(), active.jobId);
|
|
1269
|
+
}
|
|
1270
|
+
finally {
|
|
1271
|
+
// Terminal: the session stops buffering and starts its retention
|
|
1272
|
+
// window, so a client reconnecting shortly after still gets the tail
|
|
1273
|
+
// (and the outcome) before the persisted row becomes the only source.
|
|
1274
|
+
active.runSession?.finish(active.status);
|
|
1275
|
+
this.activeJobs.delete(active.jobId);
|
|
1276
|
+
this.drainQueue();
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
/**
|
|
1280
|
+
* Write the run's terminal status onto the persisted Job row. Skipped for
|
|
1281
|
+
* explicitly session-scoped runs, which own no row. Never throws —
|
|
1282
|
+
* persistence is best-effort and must not mask the run's own outcome.
|
|
1283
|
+
*/
|
|
1284
|
+
async persistTerminalJobStatus(active, outputs) {
|
|
1285
|
+
if ((active.executionOptions?.persistence ??
|
|
1286
|
+
DEFAULT_RUN_JOB_EXECUTION_OPTIONS.persistence) !== "job") {
|
|
1287
|
+
return;
|
|
1288
|
+
}
|
|
1289
|
+
try {
|
|
1290
|
+
const job = (await Job.get(active.jobId));
|
|
1291
|
+
// A DB-only cancel (tRPC `jobs.cancel`) can finalize the row as cancelled
|
|
1292
|
+
// while the job is still executing in memory. Don't overwrite that with a
|
|
1293
|
+
// completed/failed status when the in-flight run finishes.
|
|
1294
|
+
if (job) {
|
|
1295
|
+
if (job.status !== "cancelled") {
|
|
1296
|
+
if (active.status === "completed") {
|
|
1297
|
+
job.markCompleted();
|
|
1298
|
+
if (outputs !== undefined) {
|
|
1299
|
+
job.metadata_json = {
|
|
1300
|
+
...(job.metadata_json ?? {}),
|
|
1301
|
+
outputs: persistableOutputs(withoutInlineAssetBytes(outputs))
|
|
1302
|
+
};
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
else if (active.status === "failed") {
|
|
1306
|
+
job.markFailed(active.error ?? "Unknown error");
|
|
1307
|
+
}
|
|
1308
|
+
else if (active.status === "cancelled") {
|
|
1309
|
+
job.markCancelled();
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
job.cost = this.runMeasuredCost(active);
|
|
1313
|
+
await job.save();
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
catch (error) {
|
|
1317
|
+
this.session.logError("job persistence (final status) failed", error);
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
/**
|
|
1321
|
+
* Close the app's ledger row at what the run actually cost. Until this lands
|
|
1322
|
+
* the run keeps counting against the budget at its estimate, which is the
|
|
1323
|
+
* conservative direction: a crash cannot free spend it may have incurred.
|
|
1324
|
+
* The total is what anything measured: charges a node reported for itself
|
|
1325
|
+
* plus generation the cost ledger priced. An absent total still means
|
|
1326
|
+
* "nothing measured this run", not "this run was free" — passing null keeps
|
|
1327
|
+
* the estimate standing rather than handing the spend back. Never throws.
|
|
1328
|
+
*/
|
|
1329
|
+
async settleApplicationInvocation(active) {
|
|
1330
|
+
if (!active.applicationId)
|
|
1331
|
+
return;
|
|
1332
|
+
try {
|
|
1333
|
+
await settleInvocation(active.applicationId, active.jobId, this.runMeasuredCost(active), active.status === "failed"
|
|
1334
|
+
? "failed"
|
|
1335
|
+
: active.status === "cancelled"
|
|
1336
|
+
? "cancelled"
|
|
1337
|
+
: "completed");
|
|
1338
|
+
}
|
|
1339
|
+
catch (error) {
|
|
1340
|
+
this.session.logError("application invocation settlement failed", error);
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
async _streamJobMessagesInner(active, executePromise) {
|
|
1344
|
+
let terminalSeen = false;
|
|
1345
|
+
let terminalWithResultSeen = false;
|
|
1346
|
+
let outputUpdateSeen = false;
|
|
1347
|
+
let finalOutputs = {};
|
|
1348
|
+
// Per-node arrival counter for `generation_complete.index` within this job.
|
|
1349
|
+
// The function is scoped to one job, so keying by node_id alone yields a
|
|
1350
|
+
// per-(job_id, node_id) monotonic index. DB-ordering reconciliation is a
|
|
1351
|
+
// later step (RFC Decision 8); this is the in-memory arrival order.
|
|
1352
|
+
const generationIndexByNode = new Map();
|
|
1353
|
+
// Arrival positions already autosaved in THIS run, keyed `${nodeId} ${index}`.
|
|
1354
|
+
// A single generation_complete can persist several assets (a `list[image]`
|
|
1355
|
+
// output, or media + text), so dedupe by the event's arrival index — NOT by
|
|
1356
|
+
// a total asset count, which would under-save the next event (RFC D8).
|
|
1357
|
+
const autosavedSlots = new Set();
|
|
1358
|
+
// Cross-run replay dedupe: the `generation_index` values already persisted
|
|
1359
|
+
// for a node by a PRIOR run. Warmed with ONE `Asset.paginate` on a node's
|
|
1360
|
+
// first generation_complete, then reused for every later variant — so an
|
|
1361
|
+
// N-variant run does one query per node, not one per variant (RFC D8).
|
|
1362
|
+
const persistedIndexByNode = new Map();
|
|
1363
|
+
// The kernel opens every run with the same running update. Authoritative
|
|
1364
|
+
// SDK runs relay that one and avoid an otherwise duplicate WebSocket
|
|
1365
|
+
// frame. Legacy clients keep the eager acknowledgement.
|
|
1366
|
+
let runningSeen = false;
|
|
1367
|
+
if (!active.requireTerminalResult) {
|
|
1368
|
+
await this.session.send({
|
|
1369
|
+
type: "job_update",
|
|
1370
|
+
status: "running",
|
|
1371
|
+
job_id: active.jobId,
|
|
1372
|
+
workflow_id: active.workflowId
|
|
1373
|
+
});
|
|
1374
|
+
runningSeen = true;
|
|
1375
|
+
}
|
|
1376
|
+
// Guard the framing contract for authoritative runs: a terminal update
|
|
1377
|
+
// must never be the first job_update a client sees. The kernel emits
|
|
1378
|
+
// running before it can fail, but a run that dies before the kernel
|
|
1379
|
+
// starts — or a future reordering there — must not silently drop the
|
|
1380
|
+
// acknowledgement.
|
|
1381
|
+
const ensureRunningFrame = async () => {
|
|
1382
|
+
if (runningSeen)
|
|
1383
|
+
return;
|
|
1384
|
+
runningSeen = true;
|
|
1385
|
+
await this.session.send({
|
|
1386
|
+
type: "job_update",
|
|
1387
|
+
status: "running",
|
|
1388
|
+
job_id: active.jobId,
|
|
1389
|
+
workflow_id: active.workflowId
|
|
1390
|
+
});
|
|
1391
|
+
};
|
|
1392
|
+
const executionSettled = executePromise
|
|
1393
|
+
.then((result) => {
|
|
1394
|
+
active.status = result.status;
|
|
1395
|
+
active.error = result.error;
|
|
1396
|
+
finalOutputs = result.outputs ?? {};
|
|
1397
|
+
})
|
|
1398
|
+
.catch((err) => {
|
|
1399
|
+
this.session.logError("job execution failed", err);
|
|
1400
|
+
active.status = "failed";
|
|
1401
|
+
active.error = formatSanitizedError(err);
|
|
1402
|
+
})
|
|
1403
|
+
.finally(() => {
|
|
1404
|
+
active.finished = true;
|
|
1405
|
+
});
|
|
1406
|
+
const waitForActivity = createRelayActivityWaiter(active.context, executionSettled);
|
|
1407
|
+
const graphNodes = active.graph.nodes ?? [];
|
|
1408
|
+
const graphNodeMap = new Map();
|
|
1409
|
+
for (const n of graphNodes) {
|
|
1410
|
+
if (isString(n.id)) {
|
|
1411
|
+
graphNodeMap.set(n.id, n);
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
while (!active.finished || active.context.hasMessages()) {
|
|
1415
|
+
while (active.context.hasMessages()) {
|
|
1416
|
+
const msg = active.context.popMessage();
|
|
1417
|
+
if (!msg)
|
|
1418
|
+
break;
|
|
1419
|
+
const outbound = { ...msg };
|
|
1420
|
+
outbound.job_id ??= active.jobId;
|
|
1421
|
+
outbound.workflow_id ??= active.workflowId;
|
|
1422
|
+
// Leave a nullish error untouched (the kernel stamps `error: null` on
|
|
1423
|
+
// every update) — only sanitize a real error value. Formatting null here
|
|
1424
|
+
// would ship the literal string "null" to clients.
|
|
1425
|
+
if (outbound.error != null) {
|
|
1426
|
+
outbound.error = formatSanitizedError(outbound.error);
|
|
1427
|
+
}
|
|
1428
|
+
if (outbound.type === "notification" && isString(outbound.content)) {
|
|
1429
|
+
outbound.content = sanitizeLargeText(outbound.content);
|
|
1430
|
+
}
|
|
1431
|
+
// Every message, not just node updates: a `prediction` is where
|
|
1432
|
+
// ledger-priced generation spend (Replicate, Gemini, OpenAI, …)
|
|
1433
|
+
// reports itself, and this is the path whose terminal status and app
|
|
1434
|
+
// settlement read the run's measured cost.
|
|
1435
|
+
this._handleNodeProviderCost(active, outbound);
|
|
1436
|
+
if (outbound.type === "node_update" && outbound.status === "error") {
|
|
1437
|
+
log.error("Node error", {
|
|
1438
|
+
jobId: active.jobId,
|
|
1439
|
+
nodeId: outbound.node_id,
|
|
1440
|
+
error: outbound.error
|
|
1441
|
+
});
|
|
1442
|
+
}
|
|
1443
|
+
else if (outbound.type === "job_update" &&
|
|
1444
|
+
outbound.status === "failed") {
|
|
1445
|
+
log.error("Job failed", {
|
|
1446
|
+
jobId: active.jobId,
|
|
1447
|
+
error: outbound.error
|
|
1448
|
+
});
|
|
1449
|
+
}
|
|
1450
|
+
// Skip messages for constant/input nodes — they produce trivial
|
|
1451
|
+
// outputs that don't need to be relayed to the frontend.
|
|
1452
|
+
if (outbound.type === "output_update" ||
|
|
1453
|
+
outbound.type === "node_update" ||
|
|
1454
|
+
outbound.type === "generation_complete") {
|
|
1455
|
+
const nodeId = String(outbound.node_id ?? "");
|
|
1456
|
+
const node = graphNodeMap.get(nodeId);
|
|
1457
|
+
const nodeType = isString(node?.type) ? node.type : "";
|
|
1458
|
+
// Skip constant and input nodes entirely
|
|
1459
|
+
if (nodeType.startsWith("nodetool.constant.") ||
|
|
1460
|
+
nodeType.startsWith("nodetool.input.")) {
|
|
1461
|
+
continue;
|
|
1462
|
+
}
|
|
1463
|
+
const meta = this.session.getNodeMetadata?.(nodeType);
|
|
1464
|
+
// Stamp an arrival-order `index` on generation_complete, keyed per
|
|
1465
|
+
// (job_id, node_id) (the function is job-scoped, so node_id alone
|
|
1466
|
+
// suffices). job_id/workflow_id were already backfilled by the
|
|
1467
|
+
// outbound spread above.
|
|
1468
|
+
if (outbound.type === "generation_complete") {
|
|
1469
|
+
const arrivalIndex = generationIndexByNode.get(nodeId) ?? 0;
|
|
1470
|
+
outbound.index = arrivalIndex;
|
|
1471
|
+
generationIndexByNode.set(nodeId, arrivalIndex + 1);
|
|
1472
|
+
// Autosave one generation per generation_complete on the RAW outputs
|
|
1473
|
+
// (before the normalize at the bottom of this block strips inline
|
|
1474
|
+
// bytes), tagged { jobId, nodeId, index }. This is the autosave
|
|
1475
|
+
// cutover (RFC §7, D3): persistence is driven per generation event,
|
|
1476
|
+
// not by the terminal node_update{completed} — so an N-execution
|
|
1477
|
+
// run persists N distinct generations.
|
|
1478
|
+
//
|
|
1479
|
+
// Replay dedupe (D8) is keyed on the event's arrival `index`, NOT on
|
|
1480
|
+
// a total asset count: a single generation_complete can persist
|
|
1481
|
+
// several assets (a `list[image]` output, or media + a text asset),
|
|
1482
|
+
// so a count-vs-index gate would under-save the very first run. Two
|
|
1483
|
+
// guards, both keyed by (nodeId, index):
|
|
1484
|
+
// - in-run: skip if this arrival slot was already saved this run;
|
|
1485
|
+
// - cross-run: skip if an asset for (jobId, nodeId) already carries
|
|
1486
|
+
// metadata.generation_index === arrivalIndex (a reconnect replay
|
|
1487
|
+
// re-streams the same events with arrivalIndex back at 0..N-1).
|
|
1488
|
+
// Server-only (D9): this is the websocket runner; the browser never
|
|
1489
|
+
// reaches runJob, so no browser autosave is introduced here.
|
|
1490
|
+
if (active.executionOptions.assetPersistence === "auto" &&
|
|
1491
|
+
meta?.auto_save_asset &&
|
|
1492
|
+
outbound.outputs != null) {
|
|
1493
|
+
const userId = this.session.requireUserId();
|
|
1494
|
+
const slotKey = `${nodeId} ${arrivalIndex}`;
|
|
1495
|
+
// Warm the cross-run replay set once per node (on its first
|
|
1496
|
+
// generation_complete), then reconcile every later variant
|
|
1497
|
+
// against the in-memory set — one DB read per node, not per slot.
|
|
1498
|
+
let persistedIndices = persistedIndexByNode.get(nodeId);
|
|
1499
|
+
if (persistedIndices === undefined) {
|
|
1500
|
+
persistedIndices = new Set();
|
|
1501
|
+
// Best-effort like every other persistence on this path: a
|
|
1502
|
+
// DB-free run (session persistence in the reliability harness,
|
|
1503
|
+
// a misconfigured deployment) must degrade to skipping the
|
|
1504
|
+
// replay dedupe, not kill the drain loop and fail the job.
|
|
1505
|
+
try {
|
|
1506
|
+
const [persisted] = await Asset.paginate(userId, {
|
|
1507
|
+
jobId: active.jobId,
|
|
1508
|
+
nodeId,
|
|
1509
|
+
limit: 1000
|
|
1510
|
+
});
|
|
1511
|
+
for (const a of persisted) {
|
|
1512
|
+
const gi = a.metadata?.generation_index;
|
|
1513
|
+
if (isNumber(gi))
|
|
1514
|
+
persistedIndices.add(gi);
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
catch (err) {
|
|
1518
|
+
log.warn("generation replay-dedupe read failed", {
|
|
1519
|
+
nodeId,
|
|
1520
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1521
|
+
});
|
|
1522
|
+
}
|
|
1523
|
+
persistedIndexByNode.set(nodeId, persistedIndices);
|
|
1524
|
+
}
|
|
1525
|
+
if (!autosavedSlots.has(slotKey) &&
|
|
1526
|
+
!persistedIndices.has(arrivalIndex)) {
|
|
1527
|
+
autosavedSlots.add(slotKey);
|
|
1528
|
+
try {
|
|
1529
|
+
await autoSaveAssets(outbound.outputs, {
|
|
1530
|
+
userId,
|
|
1531
|
+
workflowId: active.workflowId,
|
|
1532
|
+
jobId: active.jobId,
|
|
1533
|
+
nodeId,
|
|
1534
|
+
textOutputName: primaryTextOutputName(meta),
|
|
1535
|
+
generationIndex: arrivalIndex,
|
|
1536
|
+
properties: outbound.properties ?? undefined,
|
|
1537
|
+
nodeType: isString(outbound.node_type)
|
|
1538
|
+
? outbound.node_type
|
|
1539
|
+
: undefined
|
|
1540
|
+
});
|
|
1541
|
+
}
|
|
1542
|
+
catch (err) {
|
|
1543
|
+
log.warn("autoSaveAssets error", { error: String(err) });
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
// Relay output_update for display-sink nodes (Output, Preview) and
|
|
1549
|
+
// for streaming or auto-saving generative nodes (FAL / Replicate /
|
|
1550
|
+
// Kie / …) so the client receives one event per yielded item — the
|
|
1551
|
+
// UI accumulates and renders each generation as it arrives. The
|
|
1552
|
+
// Preview node re-emits each chunk it receives on its own terminal
|
|
1553
|
+
// `output` handle; relaying those is what lets the preview stream
|
|
1554
|
+
// incrementally instead of collapsing to the final value.
|
|
1555
|
+
if (outbound.type === "output_update") {
|
|
1556
|
+
const isDisplaySink = nodeType.includes("Output") || nodeType.endsWith(".Preview");
|
|
1557
|
+
const isStreamingLeaf = Boolean(meta?.is_streaming_output) ||
|
|
1558
|
+
Boolean(meta?.auto_save_asset);
|
|
1559
|
+
if (!isDisplaySink && !isStreamingLeaf)
|
|
1560
|
+
continue;
|
|
1561
|
+
outputUpdateSeen = true;
|
|
1562
|
+
}
|
|
1563
|
+
const isNodeError = outbound.type === "node_update" && outbound.status === "error";
|
|
1564
|
+
if (!isNodeError &&
|
|
1565
|
+
(active.executionOptions.eventDetail === "terminal" ||
|
|
1566
|
+
(active.executionOptions.eventDetail === "outputs" &&
|
|
1567
|
+
(outbound.type === "node_update" ||
|
|
1568
|
+
outbound.type === "generation_complete")))) {
|
|
1569
|
+
continue;
|
|
1570
|
+
}
|
|
1571
|
+
// Materialize binary assets to temp URLs before sending over WebSocket
|
|
1572
|
+
if (outbound.type === "node_update" && outbound.result != null) {
|
|
1573
|
+
outbound.result = await active.context.normalizeOutputValue(outbound.result);
|
|
1574
|
+
}
|
|
1575
|
+
if (outbound.type === "output_update" && outbound.value != null) {
|
|
1576
|
+
outbound.value = await active.context.normalizeOutputValue(outbound.value);
|
|
1577
|
+
}
|
|
1578
|
+
// Normalize generation_complete.outputs the same way node_update.result
|
|
1579
|
+
// is treated (raw bytes → temp URLs) before sending over the wire.
|
|
1580
|
+
if (outbound.type === "generation_complete" &&
|
|
1581
|
+
outbound.outputs != null) {
|
|
1582
|
+
outbound.outputs = await active.context.normalizeOutputValue(outbound.outputs);
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
if (outbound.type === "edge_update" &&
|
|
1586
|
+
active.executionOptions.eventDetail !== "full") {
|
|
1587
|
+
continue;
|
|
1588
|
+
}
|
|
1589
|
+
const status = outbound.type === "job_update" ? String(outbound.status ?? "") : "";
|
|
1590
|
+
const suppressProvisionalCompletion = active.requireTerminalResult &&
|
|
1591
|
+
status === "completed" &&
|
|
1592
|
+
outbound.result === undefined;
|
|
1593
|
+
if (!suppressProvisionalCompletion) {
|
|
1594
|
+
if (outbound.type === "job_update") {
|
|
1595
|
+
if (status === "running") {
|
|
1596
|
+
runningSeen = true;
|
|
1597
|
+
}
|
|
1598
|
+
else if (TERMINAL_JOB_STATUSES.includes(status)) {
|
|
1599
|
+
await ensureRunningFrame();
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
await this.session.send(outbound);
|
|
1603
|
+
}
|
|
1604
|
+
if (outbound.type === "job_update" && !suppressProvisionalCompletion) {
|
|
1605
|
+
if (TERMINAL_JOB_STATUSES.includes(status)) {
|
|
1606
|
+
terminalSeen = true;
|
|
1607
|
+
if (outbound.result !== undefined) {
|
|
1608
|
+
terminalWithResultSeen = true;
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
if (!active.finished) {
|
|
1614
|
+
await waitForActivity();
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
// The authoritative terminal snapshot is consumed in every event-detail
|
|
1618
|
+
// mode. Keep it just as client-safe as streamed output_update values;
|
|
1619
|
+
// otherwise Outputs/Full can replace a working temp URL with raw media.
|
|
1620
|
+
if (Object.keys(finalOutputs).length > 0) {
|
|
1621
|
+
finalOutputs = await this.normalizeFinalOutputs(active, finalOutputs);
|
|
1622
|
+
}
|
|
1623
|
+
if (active.executionOptions.eventDetail !== "terminal" &&
|
|
1624
|
+
!outputUpdateSeen &&
|
|
1625
|
+
Object.keys(finalOutputs).length > 0) {
|
|
1626
|
+
await this.sendOutputUpdates(active, finalOutputs);
|
|
1627
|
+
}
|
|
1628
|
+
const relayCompletedAt = performance.now();
|
|
1629
|
+
if (!terminalSeen ||
|
|
1630
|
+
(!terminalWithResultSeen && Object.keys(finalOutputs).length > 0)) {
|
|
1631
|
+
await ensureRunningFrame();
|
|
1632
|
+
await this.session.send({
|
|
1633
|
+
type: "job_update",
|
|
1634
|
+
status: active.status,
|
|
1635
|
+
job_id: active.jobId,
|
|
1636
|
+
workflow_id: active.workflowId,
|
|
1637
|
+
error: active.error,
|
|
1638
|
+
result: { outputs: finalOutputs }
|
|
1639
|
+
});
|
|
1640
|
+
}
|
|
1641
|
+
const terminalDeliveredAt = performance.now();
|
|
1642
|
+
log.info("Job completed", {
|
|
1643
|
+
jobId: active.jobId,
|
|
1644
|
+
status: active.status,
|
|
1645
|
+
executionOptions: active.executionOptions,
|
|
1646
|
+
timings: {
|
|
1647
|
+
queueMs: active.timings.queueMs,
|
|
1648
|
+
graphLoadedMs: active.timings.graphLoadedMs,
|
|
1649
|
+
graphHydratedMs: active.timings.graphHydratedMs,
|
|
1650
|
+
preRunMs: active.timings.preRunMs,
|
|
1651
|
+
persistenceMs: active.timings.persistenceMs,
|
|
1652
|
+
executionAndRelayMs: Math.max(0, relayCompletedAt - active.timings.kernelStartedAt),
|
|
1653
|
+
terminalDeliveryMs: Math.max(0, terminalDeliveredAt - relayCompletedAt),
|
|
1654
|
+
totalMs: Math.max(0, terminalDeliveredAt - active.timings.acceptedAt)
|
|
1655
|
+
}
|
|
1656
|
+
});
|
|
1657
|
+
await this.persistTerminalJobStatus(active, finalOutputs);
|
|
1658
|
+
await this.settleApplicationInvocation(active);
|
|
1659
|
+
releaseSpend(this.session.requireUserId(), active.jobId);
|
|
1660
|
+
// Slot release + queue drain happen in the streamJobMessages wrapper's
|
|
1661
|
+
// finally, so they run even if the drain loop above throws.
|
|
1662
|
+
}
|
|
1663
|
+
async reconnectJob(jobId, workflowId, lastSeq = 0) {
|
|
1664
|
+
// A resilient session is the authoritative answer: the run may be
|
|
1665
|
+
// executing on another connection's runner right now, or have finished
|
|
1666
|
+
// while this client was away — either way the seq-stamped buffer holds
|
|
1667
|
+
// exactly what was missed. Adopt it so this connection's `cancel_job` /
|
|
1668
|
+
// `stream_input` / `stop` reach the runner that owns the ExecutionSession.
|
|
1669
|
+
const registered = jobRunRegistry.get(this.session.requireUserId(), jobId);
|
|
1670
|
+
if (registered) {
|
|
1671
|
+
const { replay, incomplete } = registered.attach(this.jobDeliveryTarget, lastSeq);
|
|
1672
|
+
if (registered.status === "running") {
|
|
1673
|
+
this.adoptedJobIds.add(jobId);
|
|
1674
|
+
}
|
|
1675
|
+
// Header first, then the missed tail; live frames queue behind them on
|
|
1676
|
+
// the session's ordered delivery chain.
|
|
1677
|
+
await registered.deliverReplay(this.jobDeliveryTarget, [
|
|
1678
|
+
{
|
|
1679
|
+
type: "job_resumed",
|
|
1680
|
+
job_id: jobId,
|
|
1681
|
+
workflow_id: workflowId ?? registered.workflowId ?? null,
|
|
1682
|
+
status: registered.status,
|
|
1683
|
+
last_seq: registered.lastSeq,
|
|
1684
|
+
replay_count: replay.length,
|
|
1685
|
+
replay_incomplete: incomplete
|
|
1686
|
+
},
|
|
1687
|
+
...replay
|
|
1688
|
+
]);
|
|
1689
|
+
return;
|
|
1690
|
+
}
|
|
1691
|
+
const active = this.activeJobs.get(jobId);
|
|
1692
|
+
if (!active) {
|
|
1693
|
+
// No session and no in-memory job: the run ended long enough ago that
|
|
1694
|
+
// retention elapsed, or this process never had it. A row that already
|
|
1695
|
+
// reached a settled outcome is echoed verbatim — a completed run stays
|
|
1696
|
+
// completed, and the replay-unavailable note rides alongside as an
|
|
1697
|
+
// `error` string explaining only the missing events.
|
|
1698
|
+
//
|
|
1699
|
+
// Every other row status (queued, scheduled, running) is reported as
|
|
1700
|
+
// failed: nothing is left that could
|
|
1701
|
+
// ever send this client another frame, and reporting the row as-is
|
|
1702
|
+
// parks the UI in a state that never settles — a `queued` row from a
|
|
1703
|
+
// dead connection's drained queue reads as "running" with a live Stop
|
|
1704
|
+
// button forever.
|
|
1705
|
+
// Ownership rule as in the jobs router: another user's row is
|
|
1706
|
+
// indistinguishable from a missing one — it must be neither reported
|
|
1707
|
+
// nor settled below.
|
|
1708
|
+
const row = (await Job.get(jobId));
|
|
1709
|
+
const job = row && row.user_id === (this.session.requireUserId()) ? row : null;
|
|
1710
|
+
const settled = job != null &&
|
|
1711
|
+
(job.status === "completed" ||
|
|
1712
|
+
job.status === "failed" ||
|
|
1713
|
+
job.status === "cancelled");
|
|
1714
|
+
const replayUnavailable = job != null && job.status !== "failed" && job.status !== "cancelled";
|
|
1715
|
+
// A non-settled row with no session and no in-memory job is a zombie:
|
|
1716
|
+
// nothing is left that could ever finish it. Persist the failure when
|
|
1717
|
+
// this instance owns the row (or nothing claims it), so the row stops
|
|
1718
|
+
// advertising an in-flight run — otherwise every reload rediscovers it,
|
|
1719
|
+
// reattaches, and re-reports the same loss forever. A row claimed by
|
|
1720
|
+
// another instance is left alone: on a multi-instance deployment this
|
|
1721
|
+
// connection may simply have been balanced away from a run that is
|
|
1722
|
+
// still executing.
|
|
1723
|
+
if (job && !settled) {
|
|
1724
|
+
const instanceId = getInstanceId();
|
|
1725
|
+
const ownedHere = !job.runner_instance ||
|
|
1726
|
+
!instanceId ||
|
|
1727
|
+
job.runner_instance === instanceId;
|
|
1728
|
+
if (ownedHere) {
|
|
1729
|
+
try {
|
|
1730
|
+
job.markFailed("Run was lost after the execution connection went away.");
|
|
1731
|
+
await job.save();
|
|
1732
|
+
}
|
|
1733
|
+
catch (error) {
|
|
1734
|
+
this.session.logError("stale job row cleanup failed", error);
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
await this.session.send({
|
|
1739
|
+
type: "job_update",
|
|
1740
|
+
status: settled ? job.status : "failed",
|
|
1741
|
+
job_id: jobId,
|
|
1742
|
+
workflow_id: workflowId ?? job?.workflow_id ?? null,
|
|
1743
|
+
...(job
|
|
1744
|
+
? replayUnavailable
|
|
1745
|
+
? {
|
|
1746
|
+
error: "Job event replay is unavailable after the execution connection was lost."
|
|
1747
|
+
}
|
|
1748
|
+
: job.error
|
|
1749
|
+
? { error: job.error }
|
|
1750
|
+
: {}
|
|
1751
|
+
: { error: `Job ${jobId} not found` })
|
|
1752
|
+
});
|
|
1753
|
+
return;
|
|
1754
|
+
}
|
|
1755
|
+
await this.session.send({
|
|
1756
|
+
type: "job_update",
|
|
1757
|
+
status: active.status,
|
|
1758
|
+
job_id: jobId,
|
|
1759
|
+
workflow_id: workflowId ?? active.workflowId
|
|
1760
|
+
});
|
|
1761
|
+
for (const status of Object.values(active.context.getNodeStatuses())) {
|
|
1762
|
+
await this.session.send({
|
|
1763
|
+
...status,
|
|
1764
|
+
job_id: jobId,
|
|
1765
|
+
workflow_id: workflowId ?? active.workflowId
|
|
1766
|
+
});
|
|
1767
|
+
}
|
|
1768
|
+
for (const status of Object.values(active.context.getEdgeStatuses())) {
|
|
1769
|
+
await this.session.send({
|
|
1770
|
+
...status,
|
|
1771
|
+
job_id: jobId,
|
|
1772
|
+
workflow_id: workflowId ?? active.workflowId
|
|
1773
|
+
});
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
async cancelJob(jobId, workflowId) {
|
|
1777
|
+
if (!jobId) {
|
|
1778
|
+
return { error: "No job_id provided" };
|
|
1779
|
+
}
|
|
1780
|
+
// A run that's still queued has no ActiveJob yet — drop it from the queue
|
|
1781
|
+
// and tell the client it's cancelled before it ever starts.
|
|
1782
|
+
const queued = this.jobQueue.remove(jobId);
|
|
1783
|
+
if (queued) {
|
|
1784
|
+
releaseSpend(this.session.requireUserId(), jobId);
|
|
1785
|
+
const cancelledWorkflowId = queued.workflow_id ?? workflowId ?? null;
|
|
1786
|
+
// Mark the persisted queued row cancelled so it leaves the queue in
|
|
1787
|
+
// jobs.list too (not just the in-memory queue).
|
|
1788
|
+
if (resolveRunJobExecutionOptions(queued.execution_options, queued.require_terminal_result === true).persistence === "job") {
|
|
1789
|
+
try {
|
|
1790
|
+
const job = await Job.get(jobId);
|
|
1791
|
+
if (job) {
|
|
1792
|
+
job.markCancelled();
|
|
1793
|
+
await job.save();
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
catch (err) {
|
|
1797
|
+
this.session.logError("cancel persistence failed", err);
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1800
|
+
await this.session.send({
|
|
1801
|
+
type: "job_update",
|
|
1802
|
+
status: "cancelled",
|
|
1803
|
+
job_id: jobId,
|
|
1804
|
+
workflow_id: cancelledWorkflowId
|
|
1805
|
+
});
|
|
1806
|
+
this.broadcastQueuePositions();
|
|
1807
|
+
return {
|
|
1808
|
+
message: "Queued job cancelled",
|
|
1809
|
+
job_id: jobId,
|
|
1810
|
+
workflow_id: cancelledWorkflowId
|
|
1811
|
+
};
|
|
1812
|
+
}
|
|
1813
|
+
const active = this.activeJobs.get(jobId);
|
|
1814
|
+
if (!active) {
|
|
1815
|
+
// Not ours, but possibly still running on the connection that started
|
|
1816
|
+
// it (this client reconnected after a drop). Cancel through the
|
|
1817
|
+
// session's hooks and persist the row here — the owning runner's own
|
|
1818
|
+
// terminal bookkeeping still runs, and its `job_update` reaches this
|
|
1819
|
+
// client over the session it just adopted.
|
|
1820
|
+
const registered = jobRunRegistry.get(this.session.requireUserId(), jobId);
|
|
1821
|
+
if (registered && registered.status === "running") {
|
|
1822
|
+
registered.cancel();
|
|
1823
|
+
try {
|
|
1824
|
+
const job = await Job.get(jobId);
|
|
1825
|
+
if (job && job.status !== "cancelled") {
|
|
1826
|
+
job.markCancelled();
|
|
1827
|
+
await job.save();
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
catch (err) {
|
|
1831
|
+
this.session.logError("cancel persistence failed", err);
|
|
1832
|
+
}
|
|
1833
|
+
return {
|
|
1834
|
+
message: "Job cancellation requested",
|
|
1835
|
+
job_id: jobId,
|
|
1836
|
+
workflow_id: workflowId ?? registered.workflowId ?? ""
|
|
1837
|
+
};
|
|
1838
|
+
}
|
|
1839
|
+
// Nothing local holds it. With more than one instance the run may be
|
|
1840
|
+
// executing on another machine: write the cancellation to its row, which
|
|
1841
|
+
// the owning instance's poller picks up.
|
|
1842
|
+
const remote = await requestRemoteJobCancel(this.session.requireUserId(), jobId);
|
|
1843
|
+
if (remote.cancelled) {
|
|
1844
|
+
return {
|
|
1845
|
+
message: "Job cancellation requested",
|
|
1846
|
+
job_id: jobId,
|
|
1847
|
+
workflow_id: workflowId ?? remote.workflowId ?? ""
|
|
1848
|
+
};
|
|
1849
|
+
}
|
|
1850
|
+
return {
|
|
1851
|
+
error: "Job not found or already completed",
|
|
1852
|
+
job_id: jobId,
|
|
1853
|
+
workflow_id: workflowId ?? ""
|
|
1854
|
+
};
|
|
1855
|
+
}
|
|
1856
|
+
if (active.session) {
|
|
1857
|
+
active.session.cancel();
|
|
1858
|
+
}
|
|
1859
|
+
active.status = "cancelled";
|
|
1860
|
+
// Persist the cancellation to the DB right away, mirroring the queued
|
|
1861
|
+
// branch and the tRPC jobs.cancel path. The runner's own cleanup can lag
|
|
1862
|
+
// (it drains in-flight messages before its .finally() persists), so
|
|
1863
|
+
// without this the persisted row stays "running" and jobs.list — which the
|
|
1864
|
+
// Queue panel reads from — keeps reporting the job as running even though
|
|
1865
|
+
// the toolbar Stop already fired.
|
|
1866
|
+
//
|
|
1867
|
+
// Deliberately NO eager `job_update cancelled` frame here: the kernel's
|
|
1868
|
+
// own terminal frame relays through the drain loop AFTER the node-level
|
|
1869
|
+
// terminal updates, and an out-of-band frame ahead of them tells the
|
|
1870
|
+
// client the job is over while nodes still read "running" — the exact
|
|
1871
|
+
// lifecycle violation the reliability harness's mid-run-cancel journeys
|
|
1872
|
+
// pin (`lifecycle.running-after-job-terminal`), and what left canvas
|
|
1873
|
+
// nodes stuck spinning after a Stop. The `cancel_job` RPC response is the
|
|
1874
|
+
// immediate acknowledgement; the ordered terminal arrives a beat later.
|
|
1875
|
+
if ((active.executionOptions?.persistence ??
|
|
1876
|
+
DEFAULT_RUN_JOB_EXECUTION_OPTIONS.persistence) === "job") {
|
|
1877
|
+
try {
|
|
1878
|
+
const job = await Job.get(jobId);
|
|
1879
|
+
if (job && job.status !== "cancelled") {
|
|
1880
|
+
job.markCancelled();
|
|
1881
|
+
await job.save();
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
catch (err) {
|
|
1885
|
+
this.session.logError("cancel persistence failed", err);
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1888
|
+
const cancelledWorkflowId = workflowId ?? active.workflowId ?? null;
|
|
1889
|
+
// Do NOT set active.finished = true here. Let the runner's cancellation
|
|
1890
|
+
// propagate through executePromise's .finally() callback so that
|
|
1891
|
+
// streamJobMessages can drain remaining messages and persist job state.
|
|
1892
|
+
return {
|
|
1893
|
+
message: "Job cancellation requested",
|
|
1894
|
+
job_id: jobId,
|
|
1895
|
+
workflow_id: cancelledWorkflowId ?? ""
|
|
1896
|
+
};
|
|
1897
|
+
}
|
|
1898
|
+
/**
|
|
1899
|
+
* `stop` for one run. The run is cancelled here when this connection owns
|
|
1900
|
+
* it; otherwise it is executing on the connection that started it — this
|
|
1901
|
+
* client only reconnected to it — so it is cancelled through its registered
|
|
1902
|
+
* session, or, when nothing local holds it, through its row.
|
|
1903
|
+
*/
|
|
1904
|
+
async stopJob(jobId) {
|
|
1905
|
+
const active = this.activeJobs.get(jobId);
|
|
1906
|
+
if (active) {
|
|
1907
|
+
active.session.cancel();
|
|
1908
|
+
active.status = "cancelled";
|
|
1909
|
+
return;
|
|
1910
|
+
}
|
|
1911
|
+
const registered = jobRunRegistry.get(this.session.requireUserId(), jobId);
|
|
1912
|
+
if (registered && registered.status === "running") {
|
|
1913
|
+
registered.cancel();
|
|
1914
|
+
return;
|
|
1915
|
+
}
|
|
1916
|
+
await requestRemoteJobCancel(this.session.requireUserId(), jobId);
|
|
1917
|
+
}
|
|
1918
|
+
getStatus(jobId) {
|
|
1919
|
+
if (jobId) {
|
|
1920
|
+
const active = this.activeJobs.get(jobId);
|
|
1921
|
+
if (!active) {
|
|
1922
|
+
return { status: "not_found", job_id: jobId };
|
|
1923
|
+
}
|
|
1924
|
+
return {
|
|
1925
|
+
status: active.status,
|
|
1926
|
+
job_id: active.jobId,
|
|
1927
|
+
workflow_id: active.workflowId
|
|
1928
|
+
};
|
|
1929
|
+
}
|
|
1930
|
+
return {
|
|
1931
|
+
active_jobs: Array.from(this.activeJobs.values()).map((job) => ({
|
|
1932
|
+
job_id: job.jobId,
|
|
1933
|
+
workflow_id: job.workflowId,
|
|
1934
|
+
status: job.status
|
|
1935
|
+
}))
|
|
1936
|
+
};
|
|
1937
|
+
}
|
|
1938
|
+
/**
|
|
1939
|
+
* Accumulate provider cost from a completed node_update into the job total.
|
|
1940
|
+
*
|
|
1941
|
+
* The ledger row is *not* written here. `ExecutionSession` attaches
|
|
1942
|
+
* `attachRunCostLedger` to the same context, so every surface — this server,
|
|
1943
|
+
* the CLI, the debug harness — records the charge once, from one
|
|
1944
|
+
* implementation. Writing it again here would double-count every FAL and kie
|
|
1945
|
+
* generation.
|
|
1946
|
+
*/
|
|
1947
|
+
_handleNodeProviderCost(active, outbound) {
|
|
1948
|
+
if (outbound.type === "prediction") {
|
|
1949
|
+
this._handlePredictionCost(active, outbound);
|
|
1950
|
+
return;
|
|
1951
|
+
}
|
|
1952
|
+
if (outbound.type !== "node_update" ||
|
|
1953
|
+
outbound.status !== "completed" ||
|
|
1954
|
+
outbound.provider_cost == null) {
|
|
1955
|
+
return;
|
|
1956
|
+
}
|
|
1957
|
+
const providerCost = outbound.provider_cost;
|
|
1958
|
+
const amount = providerCost.amount;
|
|
1959
|
+
if (isFiniteNumber(amount)) {
|
|
1960
|
+
(active.selfReportedCostNodeIds ??= new Set()).add(String(outbound.node_id ?? ""));
|
|
1961
|
+
active.providerCostTotal = (active.providerCostTotal ?? 0) + amount;
|
|
1962
|
+
}
|
|
1963
|
+
else {
|
|
1964
|
+
// A non-finite amount (NaN/Infinity from a buggy provider call) can't
|
|
1965
|
+
// be accumulated above, and JSON can't even represent it faithfully
|
|
1966
|
+
// (`JSON.stringify(NaN)` silently becomes `null`). Rather than ship a
|
|
1967
|
+
// `provider_cost` the wire contract calls a real number, drop it — the
|
|
1968
|
+
// rest of the `node_update` still reports normally.
|
|
1969
|
+
delete outbound.provider_cost;
|
|
1970
|
+
}
|
|
1971
|
+
}
|
|
1972
|
+
/**
|
|
1973
|
+
* Accumulate a completed unit-billed `prediction` into the run total, priced
|
|
1974
|
+
* the way the cost ledger prices it. Replicate, Gemini, OpenAI, MiniMax and
|
|
1975
|
+
* ElevenLabs generation reports no `provider_cost` of its own, so without
|
|
1976
|
+
* this a run on those providers settled as "nothing measured" and stayed
|
|
1977
|
+
* booked at its estimate.
|
|
1978
|
+
*/
|
|
1979
|
+
_handlePredictionCost(active, outbound) {
|
|
1980
|
+
if (outbound.status !== "completed")
|
|
1981
|
+
return;
|
|
1982
|
+
const capability = isString(outbound.capability)
|
|
1983
|
+
? outbound.capability
|
|
1984
|
+
: null;
|
|
1985
|
+
if (!isUnitBilledCapability(capability))
|
|
1986
|
+
return;
|
|
1987
|
+
const provider = isString(outbound.provider) ? outbound.provider : "";
|
|
1988
|
+
const model = isString(outbound.model) ? outbound.model : "";
|
|
1989
|
+
if (!provider || !model)
|
|
1990
|
+
return;
|
|
1991
|
+
const priced = priceGeneration({
|
|
1992
|
+
// Pricing is a pure function of provider, model, quantity and params —
|
|
1993
|
+
// `priceGeneration` never reads `userId`, so this must not demand one.
|
|
1994
|
+
// `requireUserId()` here turned cost accounting into a throw on any
|
|
1995
|
+
// session that has not connected. The guard stays on the paths that do
|
|
1996
|
+
// key on identity: registry lookups and spend reservation.
|
|
1997
|
+
userId: this.session.userId ?? "",
|
|
1998
|
+
provider,
|
|
1999
|
+
model,
|
|
2000
|
+
capability,
|
|
2001
|
+
quantity: 1,
|
|
2002
|
+
params: isRecord(outbound.params) ? outbound.params : {}
|
|
2003
|
+
});
|
|
2004
|
+
if (!priced || !isFiniteNumber(priced.cost))
|
|
2005
|
+
return;
|
|
2006
|
+
const nodeId = String(outbound.node_id ?? "");
|
|
2007
|
+
const byNode = (active.predictionCostByNode ??= new Map());
|
|
2008
|
+
byNode.set(nodeId, (byNode.get(nodeId) ?? 0) + priced.cost);
|
|
2009
|
+
}
|
|
2010
|
+
/**
|
|
2011
|
+
* What this run cost, as far as anything measured it: node-reported provider
|
|
2012
|
+
* charges plus ledger-priced generation on nodes that reported none. Null
|
|
2013
|
+
* when nothing measured — which keeps an app invocation standing at its
|
|
2014
|
+
* estimate rather than handing the spend back.
|
|
2015
|
+
*/
|
|
2016
|
+
runMeasuredCost(active) {
|
|
2017
|
+
const selfReported = active.selfReportedCostNodeIds;
|
|
2018
|
+
let total = active.providerCostTotal ?? 0;
|
|
2019
|
+
let measured = active.providerCostTotal != null;
|
|
2020
|
+
for (const [nodeId, cost] of active.predictionCostByNode ?? []) {
|
|
2021
|
+
// The provider's own number wins over the catalog's estimate for the
|
|
2022
|
+
// same node — counting both would double-bill a FAL or kie generation.
|
|
2023
|
+
if (selfReported?.has(nodeId))
|
|
2024
|
+
continue;
|
|
2025
|
+
total += cost;
|
|
2026
|
+
measured = true;
|
|
2027
|
+
}
|
|
2028
|
+
return measured && total > 0 ? total : null;
|
|
2029
|
+
}
|
|
2030
|
+
/**
|
|
2031
|
+
* Release everything this connection still holds on the job side: detach the
|
|
2032
|
+
* resilient runs (they keep executing and buffering), cancel the ones nobody
|
|
2033
|
+
* is left to receive, and mark the queued rows cancelled. Called from the
|
|
2034
|
+
* host's `disconnect`.
|
|
2035
|
+
*/
|
|
2036
|
+
async cancelAll() {
|
|
2037
|
+
// A run with a resilient session survives the socket: detach it (frames
|
|
2038
|
+
// keep buffering for replay, and `streamJobMessages` keeps draining on
|
|
2039
|
+
// this now-socketless runner) instead of cancelling. The session's
|
|
2040
|
+
// detach-grace timer bounds how long it may run unattended. A run without
|
|
2041
|
+
// one — a chat-triggered workflow, whose owning turn is cancelled anyway
|
|
2042
|
+
// — is cancelled as before: nobody is left to receive its output.
|
|
2043
|
+
for (const [jobId, job] of this.activeJobs) {
|
|
2044
|
+
if (job.runSession) {
|
|
2045
|
+
job.runSession.detach(this.jobDeliveryTarget);
|
|
2046
|
+
continue;
|
|
2047
|
+
}
|
|
2048
|
+
job.session?.cancel();
|
|
2049
|
+
this.activeJobs.delete(jobId);
|
|
2050
|
+
}
|
|
2051
|
+
for (const adoptedId of this.adoptedJobIds) {
|
|
2052
|
+
jobRunRegistry
|
|
2053
|
+
.get(this.session.requireUserId(), adoptedId)
|
|
2054
|
+
?.detach(this.jobDeliveryTarget);
|
|
2055
|
+
}
|
|
2056
|
+
this.adoptedJobIds.clear();
|
|
2057
|
+
// Drain runs that were still queued (never started): the client is gone,
|
|
2058
|
+
// so they will never run. Mark their persisted rows cancelled instead of
|
|
2059
|
+
// leaving them as orphaned "scheduled" jobs in jobs.list.
|
|
2060
|
+
for (let queued = this.jobQueue.dequeue(); queued; queued = this.jobQueue.dequeue()) {
|
|
2061
|
+
const queuedId = queued.job_id;
|
|
2062
|
+
if (!queuedId)
|
|
2063
|
+
continue;
|
|
2064
|
+
try {
|
|
2065
|
+
const job = await Job.get(queuedId);
|
|
2066
|
+
if (job) {
|
|
2067
|
+
job.markCancelled();
|
|
2068
|
+
await job.save();
|
|
2069
|
+
}
|
|
2070
|
+
}
|
|
2071
|
+
catch (err) {
|
|
2072
|
+
this.session.logError("disconnect queue cancellation failed", err);
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
2075
|
+
for (const dequeuedId of this.dequeuedJobs) {
|
|
2076
|
+
try {
|
|
2077
|
+
const job = await Job.get(dequeuedId);
|
|
2078
|
+
if (job) {
|
|
2079
|
+
job.markCancelled();
|
|
2080
|
+
await job.save();
|
|
2081
|
+
}
|
|
2082
|
+
}
|
|
2083
|
+
catch (err) {
|
|
2084
|
+
this.session.logError("disconnect dequeued-job cancellation failed", err);
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
}
|
|
2088
|
+
}
|
|
2089
|
+
//# sourceMappingURL=job-execution.js.map
|