deepline 0.3.460 → 0.3.462
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/dist/bundling-sources/sdk/src/client-core.ts +7593 -0
- package/dist/bundling-sources/sdk/src/client.ts +8 -7366
- package/dist/bundling-sources/sdk/src/http-core.ts +1239 -0
- package/dist/bundling-sources/sdk/src/http.ts +6 -1231
- package/dist/bundling-sources/sdk/src/index.ts +4 -1
- package/dist/bundling-sources/sdk/src/play.ts +5 -5
- package/dist/bundling-sources/sdk/src/plays/client-core.ts +437 -0
- package/dist/bundling-sources/sdk/src/plays/client.ts +4 -421
- package/dist/bundling-sources/sdk/src/plays/input-example.ts +3 -4
- package/dist/bundling-sources/sdk/src/plays/play-description.ts +1 -1
- package/dist/bundling-sources/sdk/src/plays/run-request.ts +1 -1
- package/dist/bundling-sources/sdk/src/release.ts +1 -1
- package/dist/bundling-sources/sdk/src/server.ts +43 -0
- package/dist/bundling-sources/sdk/src/types.ts +15 -5
- package/dist/bundling-sources/shared_libs/integration-contracts/billing-entitlements.ts +15 -0
- package/dist/bundling-sources/shared_libs/monitors/billing/state.ts +28 -0
- package/dist/bundling-sources/shared_libs/monitors/callback-security.ts +0 -307
- package/dist/bundling-sources/shared_libs/monitors/http-error-response.ts +3 -0
- package/dist/bundling-sources/shared_libs/monitors/lifecycle-notifications.ts +3 -2
- package/dist/bundling-sources/shared_libs/monitors/package.json +2 -0
- package/dist/bundling-sources/shared_libs/monitors/provider-callback-verification.ts +34 -0
- package/dist/bundling-sources/shared_libs/monitors/provider-monitor-batch-operation.ts +59 -0
- package/dist/bundling-sources/shared_libs/monitors/provider-monitor-upstream-identity.ts +94 -1
- package/dist/bundling-sources/shared_libs/monitors/signal-radars/inbound-dead-letter.ts +35 -1
- package/dist/bundling-sources/shared_libs/monitors/signal-radars/inbound-failure-codes.ts +24 -0
- package/dist/bundling-sources/shared_libs/observability/dlq.ts +32 -0
- package/dist/bundling-sources/shared_libs/observability/scheduled-jobs.json +57 -3
- package/dist/bundling-sources/shared_libs/observability/scheduled-work.ts +1 -0
- package/dist/bundling-sources/shared_libs/play-data-plane/r2.ts +66 -1
- package/dist/bundling-sources/shared_libs/play-data-plane/sheet-column-storage.ts +36 -7
- package/dist/bundling-sources/shared_libs/play-runtime/app-runtime-api.ts +250 -11
- package/dist/bundling-sources/shared_libs/play-runtime/array-input-store.ts +319 -0
- package/dist/bundling-sources/shared_libs/play-runtime/context.ts +991 -359
- package/dist/bundling-sources/shared_libs/play-runtime/ctx-types.ts +17 -0
- package/dist/bundling-sources/shared_libs/play-runtime/daytona-sandbox-janitor.ts +13 -0
- package/dist/bundling-sources/shared_libs/play-runtime/daytona-sandbox-reaper.ts +110 -0
- package/dist/bundling-sources/shared_libs/play-runtime/deployment/fly-machines.ts +155 -6
- package/dist/bundling-sources/shared_libs/play-runtime/deployment/fly-registry.ts +110 -6
- package/dist/bundling-sources/shared_libs/play-runtime/durable-receipt-execution.ts +127 -13
- package/dist/bundling-sources/shared_libs/play-runtime/execution-capabilities.ts +14 -1
- package/dist/bundling-sources/shared_libs/play-runtime/fixture-behavior.ts +57 -1
- package/dist/bundling-sources/shared_libs/play-runtime/free-company-lookup-batching.ts +218 -0
- package/dist/bundling-sources/shared_libs/play-runtime/gateway-postgres-admission.ts +42 -5
- package/dist/bundling-sources/shared_libs/play-runtime/governor/app-runtime-budget-state-backend.ts +6 -1
- package/dist/bundling-sources/shared_libs/play-runtime/governor/app-runtime-rate-state-backend.ts +92 -31
- package/dist/bundling-sources/shared_libs/play-runtime/governor/block-reserving-budget-state-backend.ts +69 -14
- package/dist/bundling-sources/shared_libs/play-runtime/governor/governor.ts +93 -23
- package/dist/bundling-sources/shared_libs/play-runtime/governor/in-memory-rate-state-backend.ts +15 -4
- package/dist/bundling-sources/shared_libs/play-runtime/governor/policy.ts +22 -18
- package/dist/bundling-sources/shared_libs/play-runtime/governor/rate-state-backend.ts +5 -0
- package/dist/bundling-sources/shared_libs/play-runtime/launch-identity.ts +2 -0
- package/dist/bundling-sources/shared_libs/play-runtime/map-memory-limits.ts +190 -14
- package/dist/bundling-sources/shared_libs/play-runtime/memory-ledger.ts +131 -0
- package/dist/bundling-sources/shared_libs/play-runtime/modal-runtime-config.ts +77 -2
- package/dist/bundling-sources/shared_libs/play-runtime/output-size-limits.ts +271 -2
- package/dist/bundling-sources/shared_libs/play-runtime/play-run-recovery-policy.ts +51 -0
- package/dist/bundling-sources/shared_libs/play-runtime/play-runs/sandbox-cleanup/contracts.ts +2 -0
- package/dist/bundling-sources/shared_libs/play-runtime/play-runs/sandbox-cleanup/job.ts +27 -1
- package/dist/bundling-sources/shared_libs/play-runtime/play-runtime-batching-registry.ts +2 -0
- package/dist/bundling-sources/shared_libs/play-runtime/protocol.ts +3 -0
- package/dist/bundling-sources/shared_libs/play-runtime/provider-text-for-log.ts +63 -0
- package/dist/bundling-sources/shared_libs/play-runtime/resource-governor.ts +19 -0
- package/dist/bundling-sources/shared_libs/play-runtime/run-ledger-event-transport.ts +112 -16
- package/dist/bundling-sources/shared_libs/play-runtime/run-ledger.ts +104 -2
- package/dist/bundling-sources/shared_libs/play-runtime/runner-app/index.ts +265 -151
- package/dist/bundling-sources/shared_libs/play-runtime/runner-app/runtime-gateway-transport.ts +17 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-app/runtime-progress-push.ts +35 -14
- package/dist/bundling-sources/shared_libs/play-runtime/runner-app/runtime-sheet-persistence-retry.ts +12 -1
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-control-transport.ts +4 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-labels.ts +36 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-launch-transport.ts +136 -70
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-lifecycle.ts +237 -36
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-runtime-watchdog.ts +148 -4
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-session-execution.ts +6 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona.ts +285 -32
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/modal.ts +251 -56
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/runtime-sandbox-reconciliation.ts +29 -4
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/sandbox-create-outcome.ts +27 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/types.ts +22 -1
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-api.ts +1397 -382
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-array-input-receipts.ts +920 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-constants.ts +16 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-environment.ts +38 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-flow.ts +6 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-incident-drills.ts +26 -1
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-operation-contract.ts +30 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-receipt-store-adapter.ts +14 -3
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-receipt-writer.ts +31 -4
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-sheet-row-transition.ts +47 -0
- package/dist/bundling-sources/shared_libs/play-runtime/sandbox-compute-usage.ts +20 -7
- package/dist/bundling-sources/shared_libs/play-runtime/sandbox-runs/contract.ts +12 -0
- package/dist/bundling-sources/shared_libs/play-runtime/sandbox-runs/index.ts +9 -0
- package/dist/bundling-sources/shared_libs/play-runtime/sandbox-runs/runner-backend-adapter.ts +10 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backend.ts +4 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-runtime-worker.ts +1496 -320
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-worker-required-env.ts +31 -1
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres.ts +1389 -129
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/receipt-gateway.ts +36 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/run-ledger-dispatch-authority.ts +4 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/run-ledger-outbox-recovery.ts +162 -23
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/runtime-event-search.ts +499 -115
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/runtime-sandbox-capacity.ts +377 -9
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/runtime-sandbox-ledger-reconciliation.ts +117 -8
- package/dist/bundling-sources/shared_libs/play-runtime/secret-resolution-retry-policy.ts +25 -0
- package/dist/bundling-sources/shared_libs/play-runtime/terminal-runtime-performance.ts +391 -0
- package/dist/bundling-sources/shared_libs/play-runtime/test-runtime-seams.ts +40 -0
- package/dist/bundling-sources/shared_libs/play-runtime/tool-call/index.ts +6 -0
- package/dist/bundling-sources/shared_libs/play-runtime/tool-call/mapped-calls.ts +171 -0
- package/dist/bundling-sources/shared_libs/play-runtime/tool-call/receipt-cohort.ts +16 -1
- package/dist/bundling-sources/shared_libs/play-runtime/tool-execute-retry-policy.ts +26 -3
- package/dist/bundling-sources/shared_libs/play-runtime/tool-result.ts +16 -2
- package/dist/bundling-sources/shared_libs/play-runtime/transport-error-diagnostics.ts +28 -0
- package/dist/bundling-sources/shared_libs/play-runtime/work-receipt-storage-contract.ts +55 -5
- package/dist/bundling-sources/shared_libs/plays/authoring-contract.ts +41 -1
- package/dist/bundling-sources/shared_libs/plays/contracts.ts +4 -0
- package/dist/bundling-sources/shared_libs/plays/dataset.ts +9 -3
- package/dist/bundling-sources/shared_libs/plays/deploy/manifest-contract.ts +2 -0
- package/dist/bundling-sources/shared_libs/plays/deploy/manifest.ts +3 -2
- package/dist/bundling-sources/shared_libs/plays/deploy/project-serializer.ts +2 -2
- package/dist/bundling-sources/shared_libs/plays/run-output-summary.ts +66 -0
- package/dist/bundling-sources/shared_libs/plays/script-entry.ts +1 -1
- package/dist/bundling-sources/shared_libs/product-notifications/catalog.ts +43 -1
- package/dist/bundling-sources/shared_libs/product-notifications/content.ts +9 -0
- package/dist/bundling-sources/shared_libs/product-notifications/email.ts +91 -13
- package/dist/bundling-sources/shared_libs/product-notifications/events.ts +3 -0
- package/dist/bundling-sources/shared_libs/product-notifications/slack-delivery.ts +41 -24
- package/dist/bundling-sources/shared_libs/security/attested-client-ip.ts +91 -0
- package/dist/bundling-sources/shared_libs/security/internal-actor.ts +0 -11
- package/dist/cli/{auth-HKA6ZVHQ.mjs → auth-JDLEY7VU.mjs} +5 -4
- package/dist/cli/auth-UXKY6TCC.js +41 -0
- package/dist/cli/{billing-MTO2ZZ3G.js → billing-HXAXB3XZ.js} +152 -78
- package/dist/cli/{billing-26G44S5N.mjs → billing-SDHLBLMV.mjs} +84 -10
- package/dist/cli/{chunk-BHGII2D6.js → chunk-3ACMRZRV.js} +8 -6
- package/dist/cli/{chunk-S4Q2XCGM.mjs → chunk-3KZCFOZ6.mjs} +1 -1
- package/dist/cli/chunk-4KQ6SIBC.mjs +1038 -0
- package/dist/cli/{chunk-Z2GUFYT6.js → chunk-5LEAXRHV.js} +11 -11
- package/dist/cli/{chunk-XAYO3Q2X.mjs → chunk-6NNVJDVR.mjs} +7 -5
- package/dist/cli/{chunk-NMJ4D2WO.js → chunk-6PMCRYDA.js} +216 -91
- package/dist/cli/{chunk-OMM54B3X.mjs → chunk-6RNQGWFD.mjs} +16 -806
- package/dist/cli/{chunk-EBU7LQ64.js → chunk-6V7KVRR5.js} +3 -3
- package/dist/cli/{chunk-RDPTDDKX.js → chunk-7LFIMWMW.js} +2 -2
- package/dist/cli/chunk-7MJ6VI2T.mjs +19 -0
- package/dist/cli/chunk-7SWNPNML.js +1039 -0
- package/dist/cli/{chunk-CTRPCARW.mjs → chunk-7U3XNPOA.mjs} +267 -38
- package/dist/cli/{chunk-RBTTUYZZ.js → chunk-7VZHXVOQ.js} +1 -1
- package/dist/cli/{chunk-YVBWO647.mjs → chunk-AT636LLU.mjs} +2 -2
- package/dist/cli/{chunk-VTODQWQA.js → chunk-BGTPIMSM.js} +392 -261
- package/dist/cli/{chunk-KADIT5NV.mjs → chunk-CKSOY47X.mjs} +2 -2
- package/dist/cli/{chunk-7DH2FD7Y.mjs → chunk-D464JW3N.mjs} +5 -85
- package/dist/cli/chunk-D6DXOUTO.mjs +827 -0
- package/dist/cli/{describe-QW54QQNB.js → chunk-EM4TXCCI.js} +27 -44
- package/dist/cli/chunk-F6QV42MC.mjs +91 -0
- package/dist/cli/chunk-FKVYR3QA.mjs +1039 -0
- package/dist/cli/{chunk-LMAULZLF.js → chunk-GEGW5HPZ.js} +29 -27
- package/dist/cli/{chunk-M52JQSLK.js → chunk-HUB3CCM2.js} +13 -4
- package/dist/cli/{chunk-YHTQM5XI.js → chunk-I6VHOU5O.js} +27 -27
- package/dist/cli/{chunk-Z2XUZC34.js → chunk-IAYSWZDJ.js} +188 -76
- package/dist/cli/chunk-JF3TLIMQ.js +19 -0
- package/dist/cli/{chunk-HDFRJ4IT.mjs → chunk-JVHOS3K3.mjs} +177 -65
- package/dist/cli/{chunk-LAWEURCT.mjs → chunk-LFXLQEZN.mjs} +1 -1
- package/dist/cli/{chunk-2PBN4SZE.js → chunk-LROWFPGN.js} +16 -16
- package/dist/cli/{chunk-SLEUWLJE.mjs → chunk-MSY22NQW.mjs} +5 -5
- package/dist/cli/{chunk-LVJOFUU7.mjs → chunk-MUUQZIOF.mjs} +23 -49
- package/dist/cli/{chunk-2XHWOQF4.js → chunk-NBLSCHKK.js} +52 -50
- package/dist/cli/{chunk-526R2HON.mjs → chunk-NHRWZPWZ.mjs} +4 -2
- package/dist/cli/{chunk-KXMNM4AQ.js → chunk-NMU3VR5U.js} +4 -5
- package/dist/cli/{chunk-W3BE3CHK.mjs → chunk-O2RX5A66.mjs} +14 -12
- package/dist/cli/{chunk-UKFZT7RF.js → chunk-OMQ23IXL.js} +2 -2
- package/dist/cli/{chunk-LJTFZOHU.js → chunk-OPJAJBD4.js} +28 -54
- package/dist/cli/{chunk-MLFR6DVF.mjs → chunk-PHSEINNH.mjs} +5 -5
- package/dist/cli/{chunk-3DNKFH2G.mjs → chunk-PKHVSIJG.mjs} +19 -1
- package/dist/cli/{chunk-WIYWSYDS.mjs → chunk-Q2EIZKVX.mjs} +3 -3
- package/dist/cli/{chunk-VJAEA4CS.js → chunk-QJZ6WNWD.js} +8 -8
- package/dist/cli/{chunk-EIMG75BT.mjs → chunk-S3A4IXBQ.mjs} +4 -5
- package/dist/cli/{chunk-RZSTT45F.mjs → chunk-S42YIQNE.mjs} +13 -8
- package/dist/cli/{chunk-YULBYKAK.mjs → chunk-S6UAJR6V.mjs} +3 -3
- package/dist/cli/{chunk-G7L27KZB.mjs → chunk-TKAQR3IB.mjs} +31 -21
- package/dist/cli/{chunk-QWRHGGUH.js → chunk-UAJFWWZE.js} +20 -2
- package/dist/cli/{chunk-ZR2RLI4O.mjs → chunk-UDRPEZ5F.mjs} +223 -92
- package/dist/cli/{chunk-QGAWTBIR.js → chunk-ULK7JGGI.js} +511 -282
- package/dist/cli/chunk-V335HGX5.js +1038 -0
- package/dist/cli/{chunk-NH6JSJW6.mjs → chunk-V3MEA7NB.mjs} +7 -5
- package/dist/cli/{chunk-NRGGFVVB.js → chunk-VFGVXXZP.js} +37 -35
- package/dist/cli/chunk-VLEVVDWG.js +91 -0
- package/dist/cli/{chunk-R2GLGLDR.mjs → chunk-VPHN2HAL.mjs} +216 -91
- package/dist/cli/{chunk-CFUTOWE4.js → chunk-WG3KGAII.js} +8 -88
- package/dist/cli/{describe-TOMIAT37.mjs → chunk-WIY6U6GD.mjs} +19 -36
- package/dist/cli/{chunk-YS2YBSJB.js → chunk-XANRM3QA.js} +6 -6
- package/dist/cli/{chunk-KGQNIA37.js → chunk-XQCTG5OO.js} +44 -34
- package/dist/cli/{chunk-EJGBLXDZ.mjs → chunk-XZAFIOUQ.mjs} +1 -1
- package/dist/cli/chunk-Y4DPU4OM.js +827 -0
- package/dist/cli/chunk-YT5EODJO.js +920 -0
- package/dist/cli/{chunk-2WRO2UJ5.js → chunk-ZGQSFYD2.js} +56 -51
- package/dist/cli/{chunk-OJ6RUKVT.mjs → chunk-ZUK6LIJB.mjs} +10 -1
- package/dist/cli/command-docs.d.mts +5 -0
- package/dist/cli/command-docs.d.ts +5 -0
- package/dist/cli/command-docs.js +2 -2
- package/dist/cli/command-docs.mjs +1 -1
- package/dist/cli/{csv-6FNFYJUC.js → csv-C56GSCEU.js} +13 -12
- package/dist/cli/{csv-MDHT4BKK.mjs → csv-L54JS7TF.mjs} +4 -3
- package/dist/cli/{db-D6AMVOEW.js → db-MOGRMZTA.js} +30 -27
- package/dist/cli/{db-74O3RFWY.mjs → db-YVBUW2DX.mjs} +10 -7
- package/dist/cli/describe-KYEULWPG.js +48 -0
- package/dist/cli/describe-NLZKKT7C.mjs +48 -0
- package/dist/cli/describe-core.d.mts +153 -5
- package/dist/cli/describe-core.d.ts +153 -5
- package/dist/cli/describe-core.js +9 -5
- package/dist/cli/describe-core.mjs +10 -6
- package/dist/cli/{executions-N4FMGVSC.js → executions-A7L4ULPP.js} +9 -7
- package/dist/cli/{executions-PVSF4HUF.mjs → executions-ZBGSTT37.mjs} +7 -5
- package/dist/cli/{feedback-OB7NANZW.mjs → feedback-G6GU37FD.mjs} +6 -5
- package/dist/cli/{feedback-WFSPCAI2.js → feedback-OHLY7RYD.js} +10 -9
- package/dist/cli/getting-started.d.mts +6 -6
- package/dist/cli/getting-started.d.ts +6 -6
- package/dist/cli/getting-started.js +2 -2
- package/dist/cli/getting-started.mjs +1 -1
- package/dist/cli/git-P76N4NDQ.js +25 -0
- package/dist/cli/{git-FHUSPAEU.mjs → git-PKZW3QD5.mjs} +5 -4
- package/dist/cli/index.js +152 -142
- package/dist/cli/index.mjs +56 -46
- package/dist/cli/{legacy-noops-ECHNA5HN.js → legacy-noops-5LM3D724.js} +8 -7
- package/dist/cli/{legacy-noops-HMK4BSCI.mjs → legacy-noops-LJJYG6YA.mjs} +5 -4
- package/dist/cli/{monitors-U7VQ3EZM.mjs → monitors-72QV4ALO.mjs} +19 -6
- package/dist/cli/{monitors-XLLFNENH.js → monitors-UX3H5DC2.js} +103 -90
- package/dist/cli/{org-6H3USO5M.mjs → org-6RRRKOCP.mjs} +9 -6
- package/dist/cli/{org-QX6ER2HN.js → org-IF7ISUVB.js} +70 -67
- package/dist/cli/pages-3SU3HKL7.js +252 -0
- package/dist/cli/pages-DXDYYKEK.mjs +252 -0
- package/dist/cli/{play-MOB2WYOR.mjs → play-AHASKRLA.mjs} +512 -1508
- package/dist/cli/{play-WNB7HBZK.js → play-EUIUVLUH.js} +1090 -2086
- package/dist/cli/{providers-CXEZ23E2.mjs → providers-GWGE3WY3.mjs} +7 -5
- package/dist/cli/{providers-7M3DISKX.js → providers-V5WU4VXJ.js} +10 -8
- package/dist/cli/{quickstart-ZFCHKHF6.mjs → quickstart-OXKQ5ZST.mjs} +5 -4
- package/dist/cli/{quickstart-CLJT4AZL.js → quickstart-QP65I5PF.js} +13 -12
- package/dist/cli/run-6MM72IGL.mjs +443 -0
- package/dist/cli/run-F3FTYS56.js +443 -0
- package/dist/cli/search-OYKOOI7A.js +32 -0
- package/dist/cli/{search-FVYWYIC3.mjs → search-ZMEBVJZB.mjs} +8 -6
- package/dist/cli/{secrets-YRLKZQS5.js → secrets-ACND5N6A.js} +16 -14
- package/dist/cli/{secrets-WAMPTQFC.mjs → secrets-XRCJFF3L.mjs} +7 -5
- package/dist/cli/{sessions-IQ332BJH.mjs → sessions-RTK3CEW6.mjs} +5 -4
- package/dist/cli/{sessions-44J6WIVU.js → sessions-ZGRJPCFF.js} +6 -5
- package/dist/cli/{settings-4IJR2I2T.js → settings-PD5KWQQZ.js} +80 -77
- package/dist/cli/{settings-22S6NIFQ.mjs → settings-XDRBPRDE.mjs} +14 -11
- package/dist/cli/{setup-OZUPKKPW.mjs → setup-3HPYAOCC.mjs} +12 -10
- package/dist/cli/{setup-3FWSRNOX.js → setup-E2Q6PXHC.js} +53 -51
- package/dist/cli/{skills-5CW3NFP3.mjs → skills-OOQFMPOV.mjs} +7 -6
- package/dist/cli/skills-XYWV52KB.js +31 -0
- package/dist/cli/tools-ASCDZJCK.js +55 -0
- package/dist/cli/tools-C5ZT3UIC.mjs +55 -0
- package/dist/cli/update-5RQSEIFM.js +33 -0
- package/dist/cli/{update-IBULOV7P.mjs → update-ZWJTADIW.mjs} +9 -8
- package/dist/cli/{workflow-MVZT7MWT.mjs → workflow-23NMPAJ7.mjs} +10 -8
- package/dist/cli/{workflow-Z7FW6GT2.js → workflow-TCXU5YJV.js} +20 -18
- package/dist/client-DS2IWYZF.d.ts +10 -0
- package/dist/client-DTwnz_AW.d.mts +10 -0
- package/dist/{client-CYqG23K1.d.ts → client-core-B_6HAIBy.d.ts} +385 -12
- package/dist/{client-BbU5_j7n.d.mts → client-core-DzBSlY1U.d.mts} +385 -12
- package/dist/{compiler-manifest-CWz_sNOQ.d.mts → compiler-manifest-Dcyd3-Zd.d.mts} +43 -1
- package/dist/{compiler-manifest-CWz_sNOQ.d.ts → compiler-manifest-Dcyd3-Zd.d.ts} +43 -1
- package/dist/errors-d-sTrZhe.d.mts +142 -0
- package/dist/errors-yQwfgrcX.d.ts +142 -0
- package/dist/{index-BF2JB_Ug.d.mts → index-C6oXchjN.d.mts} +1 -1
- package/dist/{index-4NPXDzlD.d.ts → index-P56zy_z5.d.ts} +1 -1
- package/dist/index.d.mts +11 -147
- package/dist/index.d.ts +11 -147
- package/dist/index.js +15 -11
- package/dist/index.mjs +14 -10
- package/dist/install-integrity.json +141 -104
- package/dist/plays/bundle-play-file.d.mts +3 -3
- package/dist/plays/bundle-play-file.d.ts +3 -3
- package/dist/plays/bundle-play-file.mjs +2 -1
- package/dist/plays/play-description.d.mts +2 -2
- package/dist/plays/play-description.d.ts +2 -2
- package/dist/plays/play-description.js +2 -2
- package/dist/plays/play-description.mjs +1 -1
- package/dist/plays/run-request.d.mts +2 -2
- package/dist/plays/run-request.d.ts +2 -2
- package/dist/release.d.mts +1 -1
- package/dist/release.d.ts +1 -1
- package/dist/release.js +2 -2
- package/dist/release.mjs +1 -1
- package/dist/server.d.mts +17 -0
- package/dist/server.d.ts +17 -0
- package/dist/server.js +76 -0
- package/dist/server.mjs +76 -0
- package/package.json +6 -1
- package/dist/cli/auth-VDOCNR7H.js +0 -40
- package/dist/cli/chunk-PQUUWFXY.js +0 -95
- package/dist/cli/chunk-QJLKYYPX.js +0 -1710
- package/dist/cli/chunk-YBWAG72P.mjs +0 -95
- package/dist/cli/git-4PMBJSH5.js +0 -24
- package/dist/cli/search-4CCQCTVD.js +0 -30
- package/dist/cli/skills-LMNM6B5F.js +0 -30
- package/dist/cli/tools-AGBBDJ7P.js +0 -45
- package/dist/cli/tools-NOPBDRGW.mjs +0 -45
- package/dist/cli/update-U3W6USVD.js +0 -32
package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-runtime-worker.ts
CHANGED
|
@@ -53,6 +53,7 @@ import { RUNTIME_CLAIM_HEARTBEAT_DIVISOR } from '../runtime-constants';
|
|
|
53
53
|
import { runtimeLimitExceededCause } from '../run-failure';
|
|
54
54
|
import {
|
|
55
55
|
playRunRecoveryLogDetail,
|
|
56
|
+
playRunRecoveryProviderText,
|
|
56
57
|
recoveryForDaytonaSandboxAcquisitionUnavailable,
|
|
57
58
|
recoveryForRuntimeInitializationTransportFailure,
|
|
58
59
|
recoveryForRuntimeSchedulerAdmissionFailure,
|
|
@@ -66,10 +67,14 @@ import type {
|
|
|
66
67
|
PlayRunnerExecutionConfig,
|
|
67
68
|
PlayRunnerResult,
|
|
68
69
|
} from '../protocol';
|
|
70
|
+
import { resolveRunnerHeartbeatSilenceMs } from '../test-runtime-seams';
|
|
69
71
|
import {
|
|
70
72
|
RuntimeResourceFenceLostError,
|
|
71
73
|
type PlayRunnerRuntimeResource,
|
|
72
74
|
} from '../runner-backends/types';
|
|
75
|
+
import {
|
|
76
|
+
daytonaSandboxMatchesOperationIdentity,
|
|
77
|
+
} from '../runner-backends/backends/daytona-labels';
|
|
73
78
|
import type { PlayBundleArtifact } from '../../plays/artifact-types';
|
|
74
79
|
import {
|
|
75
80
|
readRuntimeSheetRowOutcomeCounts,
|
|
@@ -161,8 +166,13 @@ import {
|
|
|
161
166
|
markPostgresSchedulerAttemptWaiting,
|
|
162
167
|
materializePostgresSchedulerFiles,
|
|
163
168
|
listPostgresSchedulerCompletedRunnerTerminals,
|
|
169
|
+
listPostgresSchedulerDetachedRunnerLivenessWork,
|
|
164
170
|
type PostgresSchedulerCompletedRunnerTerminal,
|
|
171
|
+
type PostgresSchedulerStaleDetachedRunner,
|
|
172
|
+
type PostgresSchedulerUnsettledRunnerTerminal,
|
|
173
|
+
type RunLedgerOutboxWithClient,
|
|
165
174
|
listPostgresSchedulerAmbiguousModalCreateIntents,
|
|
175
|
+
recordPostgresSchedulerDetachedRunnerLivenessVerdict,
|
|
166
176
|
recordPostgresSchedulerAttemptRuntimeResource,
|
|
167
177
|
recordPostgresSchedulerAmbiguousModalCreateIntent,
|
|
168
178
|
recordPostgresSchedulerLateManagedSandboxOperationForCleanup,
|
|
@@ -173,6 +183,7 @@ import {
|
|
|
173
183
|
recordPostgresSchedulerRuntimeLifecycleEvent,
|
|
174
184
|
recordPostgresSchedulerSandboxCrashDiagnostic,
|
|
175
185
|
recordPostgresSchedulerSandboxCleanupEvent,
|
|
186
|
+
readPostgresSchedulerSandboxDeleteAcceptance,
|
|
176
187
|
recordPostgresSchedulerRunnerTerminal,
|
|
177
188
|
resumePostgresSchedulerDetachedRunnerAttempt,
|
|
178
189
|
retryPostgresSchedulerComputeBillingJob,
|
|
@@ -204,8 +215,10 @@ import {
|
|
|
204
215
|
type PostgresSchedulerOptions,
|
|
205
216
|
type PostgresSchedulerQueryClient,
|
|
206
217
|
type PostgresSchedulerSandboxCleanupJob,
|
|
218
|
+
type PostgresSchedulerSandboxDeleteAcceptance,
|
|
207
219
|
type PostgresSchedulerSandboxCrashDiagnosticRequest,
|
|
208
220
|
type PostgresSchedulerSignalRecord,
|
|
221
|
+
cancelPostgresSchedulerRun,
|
|
209
222
|
} from './postgres';
|
|
210
223
|
import {
|
|
211
224
|
createPlayRunAttemptSuspensionJob,
|
|
@@ -224,15 +237,17 @@ import {
|
|
|
224
237
|
} from './postgres-rate-state';
|
|
225
238
|
import { verifyRuntimeReceiptGatewayHealth } from './runtime-receipt-gateway-health';
|
|
226
239
|
import {
|
|
227
|
-
|
|
240
|
+
runtimeSandboxPlacementPoliciesRunnableByWorker,
|
|
228
241
|
runtimeSandboxProviderCapabilities,
|
|
229
242
|
} from './absurd-worker-required-env';
|
|
230
243
|
import {
|
|
231
244
|
acquirePgNotifyHub,
|
|
232
245
|
isConnectionCategoryError,
|
|
233
246
|
isRuntimeSchedulerCapacityError,
|
|
247
|
+
isRuntimeSchedulerCircuitOpenError,
|
|
234
248
|
isRuntimeSchedulerQueryCancellationError,
|
|
235
249
|
isRuntimeSchedulerRetryableTransactionError,
|
|
250
|
+
isRuntimeSchedulerServerUnavailableError,
|
|
236
251
|
parseRuntimeSchedulerCapacityError,
|
|
237
252
|
RuntimeSchedulerCapacityError,
|
|
238
253
|
runtimeSchedulerCapacityRetryAfterMs,
|
|
@@ -296,7 +311,7 @@ import {
|
|
|
296
311
|
bindRuntimeSandboxCapacityLease,
|
|
297
312
|
createRuntimeSandboxOperationReservation,
|
|
298
313
|
readReservedDaytonaCancellationOperation,
|
|
299
|
-
|
|
314
|
+
recordDaytonaSandboxCleanupIntent,
|
|
300
315
|
reserveRuntimeSandboxOperation,
|
|
301
316
|
runtimeSandboxCapacityAdmissionMode,
|
|
302
317
|
RuntimeSandboxCapacityLimitError,
|
|
@@ -828,6 +843,7 @@ export type AbsurdRuntimeWorkerDeps = {
|
|
|
828
843
|
deleteRuntimeSandboxWithOutcome?: (
|
|
829
844
|
sandboxId: string,
|
|
830
845
|
expectedOrganizationId: string | null,
|
|
846
|
+
deleteAcceptance?: PostgresSchedulerSandboxDeleteAcceptance | null,
|
|
831
847
|
) => Promise<DaytonaSandboxDeleteOutcome>;
|
|
832
848
|
/** Maximum time a best-effort sandbox deletion may occupy a worker slot. */
|
|
833
849
|
sandboxGcTimeoutMs?: number;
|
|
@@ -859,6 +875,36 @@ function defaultProgressReporter(
|
|
|
859
875
|
});
|
|
860
876
|
}
|
|
861
877
|
|
|
878
|
+
/** What projecting a committed terminal needs: the progress fold and the
|
|
879
|
+
* run-ledger outbox delivery a lane worker runs after its own terminal. */
|
|
880
|
+
export type AbsurdRuntimeTerminalProjectionDeps = Pick<
|
|
881
|
+
AbsurdRuntimeWorkerDeps,
|
|
882
|
+
'runWithClient' | 'createProgressReporter' | 'projectOutbox'
|
|
883
|
+
>;
|
|
884
|
+
|
|
885
|
+
export function createAbsurdRuntimeTerminalProjectionDeps(input: {
|
|
886
|
+
runWithClient: AbsurdRuntimeWorkerDeps['runWithClient'];
|
|
887
|
+
outboxWithClient: RunLedgerOutboxWithClient;
|
|
888
|
+
}): AbsurdRuntimeTerminalProjectionDeps {
|
|
889
|
+
return {
|
|
890
|
+
runWithClient: input.runWithClient,
|
|
891
|
+
createProgressReporter: defaultProgressReporter,
|
|
892
|
+
projectOutbox: async (launch, runId, options, targetEventId, signal) =>
|
|
893
|
+
await createRunLedgerProjectionJob({
|
|
894
|
+
outbox: createGatewayRunLedgerProjectionAdapter({
|
|
895
|
+
runId,
|
|
896
|
+
launch,
|
|
897
|
+
runtimeApi: {
|
|
898
|
+
...absurdRuntimeApiFromLaunch(launch),
|
|
899
|
+
...(signal ? { signal } : {}),
|
|
900
|
+
},
|
|
901
|
+
withClient: input.outboxWithClient,
|
|
902
|
+
schedulerOptions: options,
|
|
903
|
+
}),
|
|
904
|
+
}).deliver(targetEventId ? { targetEventId } : {}),
|
|
905
|
+
};
|
|
906
|
+
}
|
|
907
|
+
|
|
862
908
|
function buildDefaultDeps(absurd: Absurd): AbsurdRuntimeWorkerDeps {
|
|
863
909
|
// Resolve the pool lazily: when a caller (tests) overrides `runWithClient`,
|
|
864
910
|
// the runtime scheduler URL is never required.
|
|
@@ -868,29 +914,18 @@ function buildDefaultDeps(absurd: Absurd): AbsurdRuntimeWorkerDeps {
|
|
|
868
914
|
process.env.DEEPLINE_PLAY_RUNNER_BACKEND || 'daytona',
|
|
869
915
|
);
|
|
870
916
|
return {
|
|
871
|
-
|
|
872
|
-
|
|
917
|
+
...createAbsurdRuntimeTerminalProjectionDeps({
|
|
918
|
+
runWithClient: (fn, trace) =>
|
|
919
|
+
withRuntimeSchedulerClient(getPool(), fn, trace),
|
|
920
|
+
outboxWithClient: (operation, trace) =>
|
|
921
|
+
withRuntimeSchedulerClient(getPool(), operation, trace),
|
|
922
|
+
}),
|
|
873
923
|
absurd,
|
|
874
924
|
configuredRuntimeBackend,
|
|
875
925
|
resolveBackend: (backendId, runtimeSandboxPlacementPolicyId) =>
|
|
876
926
|
runtimeSandboxPlacementPolicyId
|
|
877
927
|
? resolveRuntimeSandboxPlacementBackend(runtimeSandboxPlacementPolicyId)
|
|
878
928
|
: resolvePlayRunnerBackend(backendId),
|
|
879
|
-
createProgressReporter: defaultProgressReporter,
|
|
880
|
-
projectOutbox: async (launch, runId, options, targetEventId, signal) =>
|
|
881
|
-
await createRunLedgerProjectionJob({
|
|
882
|
-
outbox: createGatewayRunLedgerProjectionAdapter({
|
|
883
|
-
runId,
|
|
884
|
-
launch,
|
|
885
|
-
runtimeApi: {
|
|
886
|
-
...absurdRuntimeApiFromLaunch(launch),
|
|
887
|
-
...(signal ? { signal } : {}),
|
|
888
|
-
},
|
|
889
|
-
withClient: (operation, trace) =>
|
|
890
|
-
withRuntimeSchedulerClient(getPool(), operation, trace),
|
|
891
|
-
schedulerOptions: options,
|
|
892
|
-
}),
|
|
893
|
-
}).deliver(targetEventId ? { targetEventId } : {}),
|
|
894
929
|
runtimeApiFromLaunch: absurdRuntimeApiFromLaunch,
|
|
895
930
|
mintAttemptExecutorToken: requestAttemptExecutorToken,
|
|
896
931
|
deleteRuntimeSandbox: (sandboxId) =>
|
|
@@ -900,10 +935,15 @@ function buildDefaultDeps(absurd: Absurd): AbsurdRuntimeWorkerDeps {
|
|
|
900
935
|
}),
|
|
901
936
|
deleteRuntimeSandboxRef: (ref) =>
|
|
902
937
|
defaultRuntimeSandboxReconciliation.delete(ref),
|
|
903
|
-
deleteRuntimeSandboxWithOutcome: (
|
|
938
|
+
deleteRuntimeSandboxWithOutcome: (
|
|
939
|
+
sandboxId,
|
|
940
|
+
expectedOrganizationId,
|
|
941
|
+
deleteAcceptance,
|
|
942
|
+
) =>
|
|
904
943
|
deleteDaytonaSandboxByIdWithOutcome({
|
|
905
944
|
sandboxId,
|
|
906
945
|
expectedOrganizationId,
|
|
946
|
+
deleteAcceptance,
|
|
907
947
|
timeoutSeconds: ABSURD_SANDBOX_GC_TIMEOUT_MS / 1_000,
|
|
908
948
|
}),
|
|
909
949
|
sandboxGcTimeoutMs: ABSURD_SANDBOX_GC_TIMEOUT_MS,
|
|
@@ -1511,6 +1551,7 @@ export async function parkAndResolveSuspension(input: {
|
|
|
1511
1551
|
* durable park can wake at the runner's heartbeat deadline. */
|
|
1512
1552
|
readRunnerLiveness?: (runnerAttempt: number) => Promise<{
|
|
1513
1553
|
heartbeatAt: string | null;
|
|
1554
|
+
heartbeatIdentity?: string | null;
|
|
1514
1555
|
updatedAt: string | null;
|
|
1515
1556
|
status: string;
|
|
1516
1557
|
} | null>;
|
|
@@ -1542,15 +1583,11 @@ export async function parkAndResolveSuspension(input: {
|
|
|
1542
1583
|
if (existing) {
|
|
1543
1584
|
return absurdCheckpointBase(checkpoint);
|
|
1544
1585
|
}
|
|
1545
|
-
//
|
|
1546
|
-
//
|
|
1547
|
-
const
|
|
1548
|
-
const heartbeatTimeoutMs =
|
|
1549
|
-
Number.isFinite(configuredHeartbeatTimeoutMs) &&
|
|
1550
|
-
configuredHeartbeatTimeoutMs >= 0
|
|
1551
|
-
? configuredHeartbeatTimeoutMs
|
|
1552
|
-
: RUNTIME_CLAIM_LEASE_SECONDS * 1000;
|
|
1586
|
+
// Never let malformed durable JSON turn the deadline into NaN, which Absurd
|
|
1587
|
+
// treats as no timer.
|
|
1588
|
+
const heartbeatTimeoutMs = detachedRunnerHeartbeatTimeoutMs(suspension);
|
|
1553
1589
|
const ceilingDeadlineMs = suspension.startedAtMs + suspension.ceilingMs;
|
|
1590
|
+
let silenceConfirmation: PlayCheckpoint['detachedRunnerSilenceConfirmation'];
|
|
1554
1591
|
const readRunnerTerminal = input.readRunnerTerminal;
|
|
1555
1592
|
const readRunnerLiveness = input.readRunnerLiveness;
|
|
1556
1593
|
let rearms = 0;
|
|
@@ -1616,7 +1653,7 @@ export async function parkAndResolveSuspension(input: {
|
|
|
1616
1653
|
let quietUntilMs = 0;
|
|
1617
1654
|
const verdict = await confirmDetachedRunnerSilence({
|
|
1618
1655
|
ctx,
|
|
1619
|
-
stepPrefix: `${suspension.boundaryId}:liveness-confirm:${
|
|
1656
|
+
stepPrefix: `${suspension.boundaryId}:liveness-confirm-heartbeat:${renewed?.heartbeatIdentity ?? renewed?.heartbeatAt ?? 'none'}`,
|
|
1620
1657
|
cadenceMs: Math.floor(
|
|
1621
1658
|
heartbeatTimeoutMs / RUNTIME_CLAIM_HEARTBEAT_DIVISOR,
|
|
1622
1659
|
),
|
|
@@ -1628,10 +1665,10 @@ export async function parkAndResolveSuspension(input: {
|
|
|
1628
1665
|
if (Date.now() < quietUntilMs) return false;
|
|
1629
1666
|
if (await readRunnerTerminal(suspension.runnerAttempt)) return true;
|
|
1630
1667
|
const current = await readRunnerLiveness(suspension.runnerAttempt);
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1668
|
+
if (
|
|
1669
|
+
(current?.heartbeatIdentity ?? current?.heartbeatAt ?? null) !==
|
|
1670
|
+
(renewed?.heartbeatIdentity ?? renewed?.heartbeatAt ?? null)
|
|
1671
|
+
) {
|
|
1635
1672
|
return true;
|
|
1636
1673
|
}
|
|
1637
1674
|
quietUntilMs = Date.now() + LIVENESS_REPLAY_READ_REUSE_MS;
|
|
@@ -1639,11 +1676,24 @@ export async function parkAndResolveSuspension(input: {
|
|
|
1639
1676
|
},
|
|
1640
1677
|
});
|
|
1641
1678
|
if (verdict === 'responded') continue;
|
|
1679
|
+
if (verdict === 'gone') {
|
|
1680
|
+
silenceConfirmation = {
|
|
1681
|
+
boundaryId: suspension.boundaryId,
|
|
1682
|
+
runnerAttempt: suspension.runnerAttempt,
|
|
1683
|
+
heartbeatIdentity:
|
|
1684
|
+
renewed?.heartbeatIdentity ?? renewed?.heartbeatAt ?? null,
|
|
1685
|
+
};
|
|
1686
|
+
}
|
|
1642
1687
|
break;
|
|
1643
1688
|
}
|
|
1644
1689
|
// Truth is the persisted terminal (read by the caller's wake path); the
|
|
1645
|
-
// absurd event carries no payload we trust.
|
|
1646
|
-
|
|
1690
|
+
// absurd event carries no payload we trust. Discard any older proof when
|
|
1691
|
+
// this wake did not complete a confirmation of the same heartbeat.
|
|
1692
|
+
const resolved = absurdCheckpointBase(checkpoint);
|
|
1693
|
+
delete resolved.detachedRunnerSilenceConfirmation;
|
|
1694
|
+
if (silenceConfirmation)
|
|
1695
|
+
resolved.detachedRunnerSilenceConfirmation = silenceConfirmation;
|
|
1696
|
+
return resolved;
|
|
1647
1697
|
}
|
|
1648
1698
|
|
|
1649
1699
|
if (suspension.kind === 'sleep') {
|
|
@@ -1738,6 +1788,7 @@ export function createAbsurdEngineSuspensionAdapter(input: {
|
|
|
1738
1788
|
readRunnerTerminal?: (runnerAttempt: number) => Promise<unknown | null>;
|
|
1739
1789
|
readRunnerLiveness?: (runnerAttempt: number) => Promise<{
|
|
1740
1790
|
heartbeatAt: string | null;
|
|
1791
|
+
heartbeatIdentity?: string | null;
|
|
1741
1792
|
updatedAt: string | null;
|
|
1742
1793
|
status: string;
|
|
1743
1794
|
} | null>;
|
|
@@ -2334,8 +2385,8 @@ type ObservedTerminalSource =
|
|
|
2334
2385
|
* outbox controller owns retry after this attempt, so a worker lease never
|
|
2335
2386
|
* determines whether a completed Run becomes durable or eventually visible.
|
|
2336
2387
|
*/
|
|
2337
|
-
|
|
2338
|
-
deps: AbsurdRuntimeWorkerDeps
|
|
2388
|
+
type TerminalOutboxProjectionRequest = {
|
|
2389
|
+
deps: Pick<AbsurdRuntimeWorkerDeps, 'projectOutbox'>;
|
|
2339
2390
|
launch: PlaySchedulerSubmitInput;
|
|
2340
2391
|
runId: string;
|
|
2341
2392
|
options: Pick<PostgresSchedulerOptions, 'schema'>;
|
|
@@ -2343,7 +2394,19 @@ function requestTerminalOutboxProjection(input: {
|
|
|
2343
2394
|
trigger?: string;
|
|
2344
2395
|
/** When this worker began post-commit projection for the terminal. */
|
|
2345
2396
|
projectionStartedAtMs?: number;
|
|
2346
|
-
|
|
2397
|
+
signal?: AbortSignal;
|
|
2398
|
+
};
|
|
2399
|
+
|
|
2400
|
+
function requestTerminalOutboxProjection(
|
|
2401
|
+
input: TerminalOutboxProjectionRequest,
|
|
2402
|
+
): void {
|
|
2403
|
+
void dispatchTerminalOutboxProjection(input);
|
|
2404
|
+
}
|
|
2405
|
+
|
|
2406
|
+
/** Never rejects: the terminal and its outbox fact are already committed. */
|
|
2407
|
+
function dispatchTerminalOutboxProjection(
|
|
2408
|
+
input: TerminalOutboxProjectionRequest,
|
|
2409
|
+
): Promise<void> {
|
|
2347
2410
|
const targetEventId = `run-terminal:${input.runId}`;
|
|
2348
2411
|
const requestedAtMs = Date.now();
|
|
2349
2412
|
// Terminal-visibility evidence: the request itself, and how long the
|
|
@@ -2357,8 +2420,14 @@ function requestTerminalOutboxProjection(input: {
|
|
|
2357
2420
|
? null
|
|
2358
2421
|
: requestedAtMs - input.projectionStartedAtMs,
|
|
2359
2422
|
});
|
|
2360
|
-
|
|
2361
|
-
.projectOutbox(
|
|
2423
|
+
return input.deps
|
|
2424
|
+
.projectOutbox(
|
|
2425
|
+
input.launch,
|
|
2426
|
+
input.runId,
|
|
2427
|
+
input.options,
|
|
2428
|
+
targetEventId,
|
|
2429
|
+
input.signal,
|
|
2430
|
+
)
|
|
2362
2431
|
.then((projection) => {
|
|
2363
2432
|
// Terminal-visibility latency evidence (ADR-0022 budget): when this
|
|
2364
2433
|
// worker asked for delivery and how long the delivery took.
|
|
@@ -2635,21 +2704,21 @@ async function prepareAbsurdAttempt(input: {
|
|
|
2635
2704
|
const launchBackend = normalizePlayRuntimeBackend(
|
|
2636
2705
|
launch.runtimeBackend || configuredBackend,
|
|
2637
2706
|
);
|
|
2638
|
-
const
|
|
2639
|
-
|
|
2707
|
+
const runnablePlacementPolicies =
|
|
2708
|
+
runtimeSandboxPlacementPoliciesRunnableByWorker(configuredBackend);
|
|
2640
2709
|
const launchPlacementPolicy = resolveLaunchRuntimeSandboxPlacementPolicy({
|
|
2641
2710
|
runtimeBackend: launchBackend,
|
|
2642
2711
|
runtimeSandboxPlacementPolicyId: launch.runtimeSandboxPlacementPolicyId,
|
|
2643
2712
|
});
|
|
2644
2713
|
if (
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
!
|
|
2648
|
-
|
|
2649
|
-
|
|
2714
|
+
launchPlacementPolicy === null
|
|
2715
|
+
? launchBackend !== configuredBackend
|
|
2716
|
+
: !runnablePlacementPolicies.some(
|
|
2717
|
+
(policy) => policy.id === launchPlacementPolicy.id,
|
|
2718
|
+
)
|
|
2650
2719
|
) {
|
|
2651
2720
|
throw new DeferTask(
|
|
2652
|
-
`Runtime lane placement mismatch: worker=${
|
|
2721
|
+
`Runtime lane placement mismatch: worker=${runnablePlacementPolicies.map((policy) => policy.id).join(',') || configuredBackend}, launch=${launchPlacementPolicy?.id ?? launchBackend}. Reconcile the namespace before retrying.`,
|
|
2653
2722
|
30,
|
|
2654
2723
|
);
|
|
2655
2724
|
}
|
|
@@ -2791,6 +2860,108 @@ async function prepareAbsurdAttempt(input: {
|
|
|
2791
2860
|
);
|
|
2792
2861
|
}
|
|
2793
2862
|
|
|
2863
|
+
function isRetryableAttemptPreparationError(error: unknown): boolean {
|
|
2864
|
+
return (
|
|
2865
|
+
isConnectionCategoryError(error) ||
|
|
2866
|
+
isRuntimeSchedulerServerUnavailableError(error) ||
|
|
2867
|
+
isRuntimeSchedulerCircuitOpenError(error) ||
|
|
2868
|
+
isRuntimeSchedulerRetryableTransactionError(error) ||
|
|
2869
|
+
isRuntimeSchedulerQueryCancellationError(error)
|
|
2870
|
+
);
|
|
2871
|
+
}
|
|
2872
|
+
|
|
2873
|
+
/**
|
|
2874
|
+
* Preparation runs before an attempt exists, so nothing downstream can settle
|
|
2875
|
+
* the run if it throws. A scheduler outage parks the task without spending an
|
|
2876
|
+
* engine attempt: a terminal write needs that same database, so a deadline here
|
|
2877
|
+
* could not settle anything sooner. Any other failure spends an attempt, and
|
|
2878
|
+
* the final one settles the run rather than leaving it queued with no task.
|
|
2879
|
+
*/
|
|
2880
|
+
async function prepareAbsurdAttemptForTask(input: {
|
|
2881
|
+
ctx: TaskContext;
|
|
2882
|
+
deps: AbsurdRuntimeWorkerDeps;
|
|
2883
|
+
options: Pick<PostgresSchedulerOptions, 'schema'>;
|
|
2884
|
+
runId: string;
|
|
2885
|
+
}) {
|
|
2886
|
+
try {
|
|
2887
|
+
return await prepareAbsurdAttempt(input);
|
|
2888
|
+
} catch (error) {
|
|
2889
|
+
if (
|
|
2890
|
+
error instanceof DeferTask ||
|
|
2891
|
+
error instanceof SuspendTask ||
|
|
2892
|
+
isRuntimeSchedulerCapacityError(error)
|
|
2893
|
+
) {
|
|
2894
|
+
throw error;
|
|
2895
|
+
}
|
|
2896
|
+
if (isRetryableAttemptPreparationError(error)) {
|
|
2897
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
2898
|
+
console.warn('[absurd-runtime.worker] attempt_preparation_deferred', {
|
|
2899
|
+
runId: input.runId,
|
|
2900
|
+
reason,
|
|
2901
|
+
});
|
|
2902
|
+
throw new DeferTask(`Attempt preparation deferred: ${reason}`, 30);
|
|
2903
|
+
}
|
|
2904
|
+
await settleUnstartedRunOnFinalPreparationFailure({ ...input, error });
|
|
2905
|
+
throw error;
|
|
2906
|
+
}
|
|
2907
|
+
}
|
|
2908
|
+
|
|
2909
|
+
async function settleUnstartedRunOnFinalPreparationFailure(input: {
|
|
2910
|
+
ctx: TaskContext;
|
|
2911
|
+
deps: AbsurdRuntimeWorkerDeps;
|
|
2912
|
+
options: Pick<PostgresSchedulerOptions, 'schema'>;
|
|
2913
|
+
runId: string;
|
|
2914
|
+
error: unknown;
|
|
2915
|
+
}): Promise<void> {
|
|
2916
|
+
const { ctx, deps, options, runId, error } = input;
|
|
2917
|
+
if (ctx.maxAttempts === null || ctx.attempt < ctx.maxAttempts) return;
|
|
2918
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
2919
|
+
try {
|
|
2920
|
+
// The fence skips a run that already has an attempt or changed state.
|
|
2921
|
+
const cancelled = await deps.runWithClient((client) =>
|
|
2922
|
+
cancelPostgresSchedulerRun(
|
|
2923
|
+
client,
|
|
2924
|
+
{
|
|
2925
|
+
runId,
|
|
2926
|
+
reason: `SCHEDULER_ATTEMPT_PREPARATION_FAILED: ${reason}`,
|
|
2927
|
+
onlyIfStrandedPreExecution: {
|
|
2928
|
+
releaseId: deps.currentReleaseId(),
|
|
2929
|
+
absurdTaskId: ctx.taskID,
|
|
2930
|
+
engineTaskTable: null,
|
|
2931
|
+
},
|
|
2932
|
+
},
|
|
2933
|
+
options,
|
|
2934
|
+
),
|
|
2935
|
+
);
|
|
2936
|
+
console.error('[absurd-runtime.worker] attempt_preparation_exhausted', {
|
|
2937
|
+
runId,
|
|
2938
|
+
reason,
|
|
2939
|
+
cancelled,
|
|
2940
|
+
});
|
|
2941
|
+
} catch (settleError) {
|
|
2942
|
+
console.error(
|
|
2943
|
+
'[absurd-runtime.worker] attempt_preparation_settlement_failed',
|
|
2944
|
+
{
|
|
2945
|
+
runId,
|
|
2946
|
+
reason,
|
|
2947
|
+
error:
|
|
2948
|
+
settleError instanceof Error
|
|
2949
|
+
? settleError.message
|
|
2950
|
+
: String(settleError),
|
|
2951
|
+
},
|
|
2952
|
+
);
|
|
2953
|
+
// The original error would fail the task and strand the run, so keep this
|
|
2954
|
+
// final attempt until the cancellation can commit.
|
|
2955
|
+
if (isRuntimeSchedulerCapacityError(settleError)) throw settleError;
|
|
2956
|
+
if (isRetryableAttemptPreparationError(settleError)) {
|
|
2957
|
+
throw new DeferTask(
|
|
2958
|
+
`Attempt preparation settlement deferred: ${reason}`,
|
|
2959
|
+
30,
|
|
2960
|
+
);
|
|
2961
|
+
}
|
|
2962
|
+
}
|
|
2963
|
+
}
|
|
2964
|
+
|
|
2794
2965
|
async function executeAbsurdAttemptAdapter(
|
|
2795
2966
|
params: AbsurdPlayRunTaskParams,
|
|
2796
2967
|
ctx: TaskContext,
|
|
@@ -2802,7 +2973,9 @@ async function executeAbsurdAttemptAdapter(
|
|
|
2802
2973
|
schedulerOptions.schema ? { schema: schedulerOptions.schema } : {};
|
|
2803
2974
|
const runId = params.runId;
|
|
2804
2975
|
|
|
2805
|
-
|
|
2976
|
+
const prepareAttempt = () =>
|
|
2977
|
+
prepareAbsurdAttemptForTask({ ctx, deps, options, runId });
|
|
2978
|
+
let prepared = await prepareAttempt();
|
|
2806
2979
|
while (prepared.kind === 'created_projection_pending') {
|
|
2807
2980
|
const delaySeconds = prepared.delaySeconds;
|
|
2808
2981
|
// The preparation checkout is already released. Absurd retains a winning
|
|
@@ -2819,11 +2992,19 @@ async function executeAbsurdAttemptAdapter(
|
|
|
2819
2992
|
} catch (error) {
|
|
2820
2993
|
if (!(error instanceof TimeoutError)) throw error;
|
|
2821
2994
|
}
|
|
2822
|
-
prepared = await
|
|
2995
|
+
prepared = await prepareAttempt();
|
|
2823
2996
|
if (signalled && prepared.kind === 'created_projection_pending') {
|
|
2824
|
-
|
|
2997
|
+
const error = new Error(
|
|
2825
2998
|
`Run-created wake has no durable projection acknowledgement for ${runId}.`,
|
|
2826
2999
|
);
|
|
3000
|
+
await settleUnstartedRunOnFinalPreparationFailure({
|
|
3001
|
+
ctx,
|
|
3002
|
+
deps,
|
|
3003
|
+
options,
|
|
3004
|
+
runId,
|
|
3005
|
+
error,
|
|
3006
|
+
});
|
|
3007
|
+
throw error;
|
|
2827
3008
|
}
|
|
2828
3009
|
}
|
|
2829
3010
|
if (prepared.kind === 'missing') {
|
|
@@ -3297,11 +3478,15 @@ async function executeAssignedAbsurdAttempt(input: {
|
|
|
3297
3478
|
options,
|
|
3298
3479
|
),
|
|
3299
3480
|
);
|
|
3300
|
-
const recordRunnerTerminal = (
|
|
3481
|
+
const recordRunnerTerminal = (
|
|
3482
|
+
runnerAttempt: number,
|
|
3483
|
+
result: unknown,
|
|
3484
|
+
expectedHeartbeatIdentity?: string | null,
|
|
3485
|
+
) =>
|
|
3301
3486
|
deps.runWithClient((client) =>
|
|
3302
3487
|
recordPostgresSchedulerRunnerTerminal(
|
|
3303
3488
|
client,
|
|
3304
|
-
{ runId, attempt: runnerAttempt, result },
|
|
3489
|
+
{ runId, attempt: runnerAttempt, result, expectedHeartbeatIdentity },
|
|
3305
3490
|
options,
|
|
3306
3491
|
),
|
|
3307
3492
|
);
|
|
@@ -3328,46 +3513,19 @@ async function executeAssignedAbsurdAttempt(input: {
|
|
|
3328
3513
|
fromDetachedRunner: input.fromDetachedRunner,
|
|
3329
3514
|
terminalLogProducerAttempt: input.terminalLogProducerAttempt,
|
|
3330
3515
|
});
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
: null;
|
|
3341
|
-
if (
|
|
3342
|
-
input.fromDetachedRunner &&
|
|
3343
|
-
input.detachedSuspension &&
|
|
3344
|
-
canarySelection?.canary === 'modal' &&
|
|
3345
|
-
runtimeSandboxRefFromSuspension(input.detachedSuspension).provider ===
|
|
3346
|
-
'modal'
|
|
3347
|
-
) {
|
|
3348
|
-
console.warn(
|
|
3349
|
-
'[play-runner.sandbox_provider_canary]',
|
|
3350
|
-
JSON.stringify({
|
|
3351
|
-
runId,
|
|
3352
|
-
workflowId: launch.runtimeAuthority?.workflowId ?? null,
|
|
3353
|
-
runAttempt: generation,
|
|
3354
|
-
policyId: runtimeSandboxPlacementPolicy?.id ?? null,
|
|
3355
|
-
provider: 'modal',
|
|
3356
|
-
bucket: canarySelection.bucket,
|
|
3357
|
-
basisPoints: canarySelection.basisPoints,
|
|
3358
|
-
runtimeEnvironment: 'production',
|
|
3359
|
-
outcome: runnerResult.status,
|
|
3360
|
-
phase: 'detached_terminal',
|
|
3361
|
-
// A normal failed Play is customer/workflow truth, not evidence that
|
|
3362
|
-
// Modal is unhealthy. Only a runner that disappeared after Modal
|
|
3363
|
-
// accepted it is provider-infrastructure evidence at this boundary.
|
|
3364
|
-
providerHealthFailure:
|
|
3365
|
-
runnerResult.status === 'failed' &&
|
|
3366
|
-
runnerResult.error.includes('RUNTIME_RUNNER_LOST:'),
|
|
3367
|
-
errorName: null,
|
|
3368
|
-
recoveryReason: null,
|
|
3516
|
+
if (input.fromDetachedRunner && input.detachedSuspension) {
|
|
3517
|
+
reportDetachedTerminalToModalCanary({
|
|
3518
|
+
launch,
|
|
3519
|
+
runId,
|
|
3520
|
+
attempt: generation,
|
|
3521
|
+
policy: runtimeSandboxPlacementPolicy,
|
|
3522
|
+
production: isRuntimeSandboxCanaryProduction({
|
|
3523
|
+
nodeEnv: process.env.NODE_ENV,
|
|
3524
|
+
runtimeSchedulerSchema: options.schema,
|
|
3369
3525
|
}),
|
|
3370
|
-
|
|
3526
|
+
suspension: input.detachedSuspension,
|
|
3527
|
+
runnerResult,
|
|
3528
|
+
});
|
|
3371
3529
|
}
|
|
3372
3530
|
const sandboxCrashDiagnostic =
|
|
3373
3531
|
runnerResult.status === 'failed'
|
|
@@ -3813,6 +3971,9 @@ async function executeAssignedAbsurdAttempt(input: {
|
|
|
3813
3971
|
// canonicalizes an omitted schema to the explicit production schema so
|
|
3814
3972
|
// every runner-originated operation stays pinned to this physical lane.
|
|
3815
3973
|
const absurdSchema = baseRunnerConfig.context.runtimeSchedulerSchema;
|
|
3974
|
+
const heartbeatSilenceMs = resolveRunnerHeartbeatSilenceMs(
|
|
3975
|
+
launch.runtimeTestFaultHeader,
|
|
3976
|
+
);
|
|
3816
3977
|
return {
|
|
3817
3978
|
runnerConfig: {
|
|
3818
3979
|
...baseRunnerConfig,
|
|
@@ -3830,6 +3991,7 @@ async function executeAssignedAbsurdAttempt(input: {
|
|
|
3830
3991
|
runnerPushExecution: {
|
|
3831
3992
|
leaseSeconds: RUNTIME_CLAIM_LEASE_SECONDS,
|
|
3832
3993
|
runId,
|
|
3994
|
+
...(heartbeatSilenceMs > 0 ? { heartbeatSilenceMs } : {}),
|
|
3833
3995
|
},
|
|
3834
3996
|
}
|
|
3835
3997
|
: {}),
|
|
@@ -4493,10 +4655,33 @@ async function executeAssignedAbsurdAttempt(input: {
|
|
|
4493
4655
|
providerOperationToken: reserved.providerOperationToken,
|
|
4494
4656
|
resumeExistingProviderOperation:
|
|
4495
4657
|
reserved.resumeExistingProviderOperation,
|
|
4658
|
+
resumePendingDelete: reserved.resumePendingDelete,
|
|
4496
4659
|
existingProviderOperationAgeMs:
|
|
4497
4660
|
reserved.existingProviderOperationAgeMs,
|
|
4498
4661
|
};
|
|
4499
4662
|
},
|
|
4663
|
+
async recordDaytonaCleanupIntent(intent) {
|
|
4664
|
+
const recorded = await deps.runWithClient((client) =>
|
|
4665
|
+
recordDaytonaSandboxCleanupIntent(
|
|
4666
|
+
client,
|
|
4667
|
+
{
|
|
4668
|
+
operationKey: intent.leaseId,
|
|
4669
|
+
providerOperationToken:
|
|
4670
|
+
intent.providerOperationToken,
|
|
4671
|
+
orgId: intent.orgId,
|
|
4672
|
+
runId: intent.runId,
|
|
4673
|
+
sandboxId: intent.sandboxId,
|
|
4674
|
+
routingDomain: intent.routingDomain,
|
|
4675
|
+
},
|
|
4676
|
+
options,
|
|
4677
|
+
),
|
|
4678
|
+
);
|
|
4679
|
+
if (!recorded) {
|
|
4680
|
+
throw new Error(
|
|
4681
|
+
'Daytona cleanup intent did not persist under its exact operation identity; cleanup and fallback suppressed.',
|
|
4682
|
+
);
|
|
4683
|
+
}
|
|
4684
|
+
},
|
|
4500
4685
|
async releaseCapacity({ leaseId }) {
|
|
4501
4686
|
await deps.runWithClient((client) =>
|
|
4502
4687
|
abandonRuntimeSandboxOperationReservation(
|
|
@@ -4794,6 +4979,7 @@ async function executeAssignedAbsurdAttempt(input: {
|
|
|
4794
4979
|
// Log-only. The persisted reason groups several provider conditions
|
|
4795
4980
|
// into one decision; the detail names the one that was observed.
|
|
4796
4981
|
const detail = playRunRecoveryLogDetail(recoveryError);
|
|
4982
|
+
const providerText = playRunRecoveryProviderText(recoveryError);
|
|
4797
4983
|
console.warn('[absurd-runtime.worker] pre_code_recovery_deferred', {
|
|
4798
4984
|
runId,
|
|
4799
4985
|
generation,
|
|
@@ -4803,6 +4989,7 @@ async function executeAssignedAbsurdAttempt(input: {
|
|
|
4803
4989
|
deadlineAtMs: recovery.deadlineAtMs,
|
|
4804
4990
|
delaySeconds: recovery.delaySeconds,
|
|
4805
4991
|
...(detail ? { detail } : {}),
|
|
4992
|
+
...(providerText ? { providerText } : {}),
|
|
4806
4993
|
});
|
|
4807
4994
|
throw new DeferTask(
|
|
4808
4995
|
`Platform recovery deferred for ${recovery.dependency}: ${recovery.reason}`,
|
|
@@ -4878,48 +5065,22 @@ function createAbsurdWorkerTerminalProjectionAdapter(input: {
|
|
|
4878
5065
|
fromDetachedRunner: boolean;
|
|
4879
5066
|
terminalLogProducerAttempt?: number;
|
|
4880
5067
|
}): PlayRunAttemptWorkerTerminalProjectionPort {
|
|
4881
|
-
|
|
4882
|
-
|
|
4883
|
-
|
|
4884
|
-
|
|
4885
|
-
|
|
4886
|
-
|
|
4887
|
-
await input.deps
|
|
4888
|
-
.runWithClient((client) =>
|
|
4889
|
-
releasePostgresSchedulerReceiptsForAttempt(
|
|
4890
|
-
client,
|
|
4891
|
-
{ runId: input.runId, runAttempt: input.generation },
|
|
4892
|
-
{ ...input.options, orgId: input.launch.orgId },
|
|
4893
|
-
),
|
|
4894
|
-
)
|
|
4895
|
-
.then((released) => {
|
|
4896
|
-
if (released.released > 0) {
|
|
4897
|
-
console.info('[absurd-runtime.worker] released terminal receipts', {
|
|
4898
|
-
runId: input.runId,
|
|
4899
|
-
generation: input.generation,
|
|
4900
|
-
reason,
|
|
4901
|
-
releasedReceipts: released.released,
|
|
4902
|
-
});
|
|
4903
|
-
}
|
|
4904
|
-
})
|
|
4905
|
-
.catch((error) => {
|
|
4906
|
-
console.warn(
|
|
4907
|
-
'[absurd-runtime.worker] terminal receipt release failed',
|
|
4908
|
-
{
|
|
4909
|
-
runId: input.runId,
|
|
4910
|
-
generation: input.generation,
|
|
4911
|
-
reason,
|
|
4912
|
-
error: error instanceof Error ? error.message : String(error),
|
|
4913
|
-
},
|
|
4914
|
-
);
|
|
4915
|
-
});
|
|
5068
|
+
const attempt = {
|
|
5069
|
+
runWithClient: input.deps.runWithClient,
|
|
5070
|
+
launch: input.launch,
|
|
5071
|
+
runId: input.runId,
|
|
5072
|
+
generation: input.generation,
|
|
5073
|
+
options: input.options,
|
|
4916
5074
|
};
|
|
4917
5075
|
return createPlayRunAttemptWorkerTerminalProjection({
|
|
4918
5076
|
async observeFenceLost() {
|
|
4919
5077
|
// A lost fence means this attempt is dead, which a cancelled run is the
|
|
4920
5078
|
// loudest case of: its receipts must not stay leased to a generation
|
|
4921
5079
|
// that will never complete them.
|
|
4922
|
-
await
|
|
5080
|
+
await releaseTerminalAttemptReceipts({
|
|
5081
|
+
...attempt,
|
|
5082
|
+
reason: 'fence_lost',
|
|
5083
|
+
});
|
|
4923
5084
|
await settleObservedTerminal({
|
|
4924
5085
|
deps: input.deps,
|
|
4925
5086
|
launch: input.launch,
|
|
@@ -4941,55 +5102,7 @@ function createAbsurdWorkerTerminalProjectionAdapter(input: {
|
|
|
4941
5102
|
});
|
|
4942
5103
|
},
|
|
4943
5104
|
async releaseFailedAttempt() {
|
|
4944
|
-
await
|
|
4945
|
-
|
|
4946
|
-
// A normal runner-reported failure does not enter the thrown-error
|
|
4947
|
-
// cleanup path above. Its terminal is already durable, so leaving these
|
|
4948
|
-
// sheet leases until their TTL would make a corrected forced rerun wait
|
|
4949
|
-
// behind a run that can no longer write. Keep this best-effort exactly
|
|
4950
|
-
// like receipt release: terminal truth never depends on cleanup.
|
|
4951
|
-
const resources = await input.deps
|
|
4952
|
-
.runWithClient((client) =>
|
|
4953
|
-
readPostgresSchedulerRuntimeResourcesForRun(
|
|
4954
|
-
client,
|
|
4955
|
-
{ runId: input.runId, attempt: input.generation },
|
|
4956
|
-
input.options,
|
|
4957
|
-
),
|
|
4958
|
-
)
|
|
4959
|
-
.catch((error) => {
|
|
4960
|
-
console.warn(
|
|
4961
|
-
'[absurd-runtime.worker] failed terminal sheet-release resource read failed',
|
|
4962
|
-
{
|
|
4963
|
-
runId: input.runId,
|
|
4964
|
-
generation: input.generation,
|
|
4965
|
-
error: error instanceof Error ? error.message : String(error),
|
|
4966
|
-
},
|
|
4967
|
-
);
|
|
4968
|
-
return [];
|
|
4969
|
-
});
|
|
4970
|
-
if (resources.length === 0) return;
|
|
4971
|
-
try {
|
|
4972
|
-
await releaseRuntimeSheetAttemptResources({
|
|
4973
|
-
config: {
|
|
4974
|
-
context: buildPostgresSchedulerRunnerContext(
|
|
4975
|
-
input.launch,
|
|
4976
|
-
input.generation,
|
|
4977
|
-
),
|
|
4978
|
-
},
|
|
4979
|
-
runId: input.runId,
|
|
4980
|
-
runAttempt: input.generation,
|
|
4981
|
-
resources,
|
|
4982
|
-
});
|
|
4983
|
-
} catch (error) {
|
|
4984
|
-
console.warn(
|
|
4985
|
-
'[absurd-runtime.worker] failed terminal sheet-attempt release failed',
|
|
4986
|
-
{
|
|
4987
|
-
runId: input.runId,
|
|
4988
|
-
generation: input.generation,
|
|
4989
|
-
error: error instanceof Error ? error.message : String(error),
|
|
4990
|
-
},
|
|
4991
|
-
);
|
|
4992
|
-
}
|
|
5105
|
+
await releaseFailedTerminalAttempt(attempt);
|
|
4993
5106
|
},
|
|
4994
5107
|
queueProgressFinalization({ runnerResult }) {
|
|
4995
5108
|
void input.reporter
|
|
@@ -5019,33 +5132,181 @@ function createAbsurdWorkerTerminalProjectionAdapter(input: {
|
|
|
5019
5132
|
});
|
|
5020
5133
|
}
|
|
5021
5134
|
|
|
5022
|
-
|
|
5023
|
-
|
|
5024
|
-
|
|
5025
|
-
|
|
5026
|
-
|
|
5027
|
-
|
|
5028
|
-
|
|
5029
|
-
|
|
5030
|
-
|
|
5135
|
+
type TerminalAttemptRelease = {
|
|
5136
|
+
runWithClient: AbsurdRuntimeWorkerDeps['runWithClient'];
|
|
5137
|
+
launch: PlaySchedulerSubmitInput;
|
|
5138
|
+
runId: string;
|
|
5139
|
+
generation: number;
|
|
5140
|
+
options: Pick<PostgresSchedulerOptions, 'schema'>;
|
|
5141
|
+
};
|
|
5142
|
+
|
|
5143
|
+
/**
|
|
5144
|
+
* Every terminal releases what the attempt claimed. Best-effort by design:
|
|
5145
|
+
* terminal truth is already durable, and a failed release must never turn a
|
|
5146
|
+
* committed terminal into a worker error.
|
|
5147
|
+
*/
|
|
5148
|
+
async function releaseTerminalAttemptReceipts(
|
|
5149
|
+
input: TerminalAttemptRelease & { reason: string },
|
|
5150
|
+
): Promise<void> {
|
|
5151
|
+
await input
|
|
5152
|
+
.runWithClient((client) =>
|
|
5153
|
+
releasePostgresSchedulerReceiptsForAttempt(
|
|
5154
|
+
client,
|
|
5155
|
+
{ runId: input.runId, runAttempt: input.generation },
|
|
5156
|
+
{ ...input.options, orgId: input.launch.orgId },
|
|
5157
|
+
),
|
|
5158
|
+
)
|
|
5159
|
+
.then((released) => {
|
|
5160
|
+
if (released.released > 0) {
|
|
5161
|
+
console.info('[absurd-runtime.worker] released terminal receipts', {
|
|
5162
|
+
runId: input.runId,
|
|
5163
|
+
generation: input.generation,
|
|
5164
|
+
reason: input.reason,
|
|
5165
|
+
releasedReceipts: released.released,
|
|
5166
|
+
});
|
|
5167
|
+
}
|
|
5168
|
+
})
|
|
5169
|
+
.catch((error) => {
|
|
5170
|
+
console.warn('[absurd-runtime.worker] terminal receipt release failed', {
|
|
5171
|
+
runId: input.runId,
|
|
5172
|
+
generation: input.generation,
|
|
5173
|
+
reason: input.reason,
|
|
5174
|
+
error: error instanceof Error ? error.message : String(error),
|
|
5175
|
+
});
|
|
5176
|
+
});
|
|
5031
5177
|
}
|
|
5032
5178
|
|
|
5033
|
-
|
|
5034
|
-
|
|
5035
|
-
|
|
5179
|
+
async function releaseFailedTerminalAttempt(
|
|
5180
|
+
input: TerminalAttemptRelease,
|
|
5181
|
+
): Promise<void> {
|
|
5182
|
+
await releaseTerminalAttemptReceipts({
|
|
5183
|
+
...input,
|
|
5184
|
+
reason: 'failed_terminal',
|
|
5185
|
+
});
|
|
5036
5186
|
|
|
5037
|
-
|
|
5038
|
-
|
|
5039
|
-
|
|
5040
|
-
|
|
5041
|
-
|
|
5042
|
-
|
|
5043
|
-
|
|
5044
|
-
|
|
5045
|
-
|
|
5046
|
-
|
|
5047
|
-
|
|
5048
|
-
|
|
5187
|
+
// A normal runner-reported failure does not enter the thrown-error cleanup
|
|
5188
|
+
// path. Its terminal is already durable, so leaving these sheet leases until
|
|
5189
|
+
// their TTL would make a corrected forced rerun wait behind a run that can no
|
|
5190
|
+
// longer write. Keep this best-effort exactly like receipt release: terminal
|
|
5191
|
+
// truth never depends on cleanup.
|
|
5192
|
+
const resources = await input
|
|
5193
|
+
.runWithClient((client) =>
|
|
5194
|
+
readPostgresSchedulerRuntimeResourcesForRun(
|
|
5195
|
+
client,
|
|
5196
|
+
{ runId: input.runId, attempt: input.generation },
|
|
5197
|
+
input.options,
|
|
5198
|
+
),
|
|
5199
|
+
)
|
|
5200
|
+
.catch((error) => {
|
|
5201
|
+
console.warn(
|
|
5202
|
+
'[absurd-runtime.worker] failed terminal sheet-release resource read failed',
|
|
5203
|
+
{
|
|
5204
|
+
runId: input.runId,
|
|
5205
|
+
generation: input.generation,
|
|
5206
|
+
error: error instanceof Error ? error.message : String(error),
|
|
5207
|
+
},
|
|
5208
|
+
);
|
|
5209
|
+
return [];
|
|
5210
|
+
});
|
|
5211
|
+
if (resources.length === 0) return;
|
|
5212
|
+
try {
|
|
5213
|
+
await releaseRuntimeSheetAttemptResources({
|
|
5214
|
+
config: {
|
|
5215
|
+
context: buildPostgresSchedulerRunnerContext(
|
|
5216
|
+
input.launch,
|
|
5217
|
+
input.generation,
|
|
5218
|
+
),
|
|
5219
|
+
},
|
|
5220
|
+
runId: input.runId,
|
|
5221
|
+
runAttempt: input.generation,
|
|
5222
|
+
resources,
|
|
5223
|
+
});
|
|
5224
|
+
} catch (error) {
|
|
5225
|
+
console.warn(
|
|
5226
|
+
'[absurd-runtime.worker] failed terminal sheet-attempt release failed',
|
|
5227
|
+
{
|
|
5228
|
+
runId: input.runId,
|
|
5229
|
+
generation: input.generation,
|
|
5230
|
+
error: error instanceof Error ? error.message : String(error),
|
|
5231
|
+
},
|
|
5232
|
+
);
|
|
5233
|
+
}
|
|
5234
|
+
}
|
|
5235
|
+
|
|
5236
|
+
function reportDetachedTerminalToModalCanary(input: {
|
|
5237
|
+
launch: PlaySchedulerSubmitInput;
|
|
5238
|
+
runId: string;
|
|
5239
|
+
attempt: number;
|
|
5240
|
+
policy: ReturnType<typeof resolveLaunchRuntimeSandboxPlacementPolicy>;
|
|
5241
|
+
production: boolean;
|
|
5242
|
+
suspension: DetachedRunnerSuspension;
|
|
5243
|
+
runnerResult: PlayRunnerResult;
|
|
5244
|
+
}): void {
|
|
5245
|
+
const selection = input.policy
|
|
5246
|
+
? selectRuntimeSandboxCanary({
|
|
5247
|
+
policy: input.policy,
|
|
5248
|
+
runId: input.runId,
|
|
5249
|
+
production: input.production,
|
|
5250
|
+
})
|
|
5251
|
+
: null;
|
|
5252
|
+
if (
|
|
5253
|
+
selection?.canary !== 'modal' ||
|
|
5254
|
+
runtimeSandboxRefFromSuspension(input.suspension).provider !== 'modal'
|
|
5255
|
+
) {
|
|
5256
|
+
return;
|
|
5257
|
+
}
|
|
5258
|
+
console.warn(
|
|
5259
|
+
'[play-runner.sandbox_provider_canary]',
|
|
5260
|
+
JSON.stringify({
|
|
5261
|
+
runId: input.runId,
|
|
5262
|
+
workflowId: input.launch.runtimeAuthority?.workflowId ?? null,
|
|
5263
|
+
runAttempt: input.attempt,
|
|
5264
|
+
policyId: input.policy?.id ?? null,
|
|
5265
|
+
provider: 'modal',
|
|
5266
|
+
bucket: selection.bucket,
|
|
5267
|
+
basisPoints: selection.basisPoints,
|
|
5268
|
+
runtimeEnvironment: 'production',
|
|
5269
|
+
outcome: input.runnerResult.status,
|
|
5270
|
+
phase: 'detached_terminal',
|
|
5271
|
+
// A normal failed Play is customer/workflow truth, not evidence that
|
|
5272
|
+
// Modal is unhealthy. Only a runner that disappeared after Modal
|
|
5273
|
+
// accepted it is provider-infrastructure evidence at this boundary.
|
|
5274
|
+
providerHealthFailure:
|
|
5275
|
+
input.runnerResult.status === 'failed' &&
|
|
5276
|
+
input.runnerResult.error.includes('RUNTIME_RUNNER_LOST:'),
|
|
5277
|
+
errorName: null,
|
|
5278
|
+
recoveryReason: null,
|
|
5279
|
+
}),
|
|
5280
|
+
);
|
|
5281
|
+
}
|
|
5282
|
+
|
|
5283
|
+
function terminalEnvelopeFromRunnerResult(
|
|
5284
|
+
result: PlayRunnerResult,
|
|
5285
|
+
): PlayRunAttemptTerminalEnvelope {
|
|
5286
|
+
if (result.status === 'completed' || result.status === 'failed') {
|
|
5287
|
+
return result;
|
|
5288
|
+
}
|
|
5289
|
+
throw new Error(
|
|
5290
|
+
'Attempt terminal projection received a suspended runner result before suspension resolution.',
|
|
5291
|
+
);
|
|
5292
|
+
}
|
|
5293
|
+
|
|
5294
|
+
const DAYTONA_RUNNER_CRASH_WITHOUT_TERMINAL =
|
|
5295
|
+
/Daytona play runner exited with code .*without pushing a terminal \(crash containment epilogue\)/i;
|
|
5296
|
+
const DAYTONA_RUNNER_LOST_WITHOUT_TERMINAL = /RUNTIME_RUNNER_LOST:/i;
|
|
5297
|
+
|
|
5298
|
+
function sandboxCrashDiagnosticRequest(input: {
|
|
5299
|
+
error: string;
|
|
5300
|
+
suspension?: DetachedRunnerSuspension;
|
|
5301
|
+
}): PostgresSchedulerSandboxCrashDiagnosticRequest | undefined {
|
|
5302
|
+
const suspension = input.suspension;
|
|
5303
|
+
if (
|
|
5304
|
+
!suspension ||
|
|
5305
|
+
(!DAYTONA_RUNNER_CRASH_WITHOUT_TERMINAL.test(input.error) &&
|
|
5306
|
+
!DAYTONA_RUNNER_LOST_WITHOUT_TERMINAL.test(input.error))
|
|
5307
|
+
) {
|
|
5308
|
+
return undefined;
|
|
5309
|
+
}
|
|
5049
5310
|
try {
|
|
5050
5311
|
if (runtimeSandboxRefFromSuspension(suspension).provider !== 'daytona') {
|
|
5051
5312
|
return undefined;
|
|
@@ -5199,9 +5460,11 @@ export async function resolveDetachedRunnerOutcome(input: {
|
|
|
5199
5460
|
recordRunnerTerminal: (
|
|
5200
5461
|
runnerAttempt: number,
|
|
5201
5462
|
result: PlayRunnerResult,
|
|
5463
|
+
expectedHeartbeatIdentity?: string | null,
|
|
5202
5464
|
) => Promise<boolean>;
|
|
5203
5465
|
readRunnerLiveness: () => Promise<{
|
|
5204
5466
|
heartbeatAt: string | null;
|
|
5467
|
+
heartbeatIdentity?: string | null;
|
|
5205
5468
|
updatedAt: string | null;
|
|
5206
5469
|
status: string;
|
|
5207
5470
|
} | null>;
|
|
@@ -5247,13 +5510,34 @@ export async function resolveDetachedRunnerOutcome(input: {
|
|
|
5247
5510
|
},
|
|
5248
5511
|
};
|
|
5249
5512
|
};
|
|
5513
|
+
const suspend = (): PlayRunnerResult => ({
|
|
5514
|
+
status: 'suspended',
|
|
5515
|
+
suspension,
|
|
5516
|
+
logs: [],
|
|
5517
|
+
stats: {},
|
|
5518
|
+
steps: [],
|
|
5519
|
+
checkpoint: input.checkpoint,
|
|
5520
|
+
tableNamespace: null,
|
|
5521
|
+
});
|
|
5250
5522
|
const settleFirstWriteWinner = async (
|
|
5251
5523
|
candidate: PlayRunnerResult,
|
|
5524
|
+
expectedHeartbeatIdentity?: string | null,
|
|
5252
5525
|
): Promise<PlayRunnerResult> => {
|
|
5253
5526
|
const attemptExists = await input.recordRunnerTerminal(
|
|
5254
5527
|
suspension.runnerAttempt,
|
|
5255
5528
|
candidate,
|
|
5529
|
+
expectedHeartbeatIdentity,
|
|
5256
5530
|
);
|
|
5531
|
+
if (!attemptExists && expectedHeartbeatIdentity !== undefined) {
|
|
5532
|
+
// A heartbeat can win while sandbox inspection or the terminal write is
|
|
5533
|
+
// in flight. The SQL fence refuses only this synthetic outcome; preserve
|
|
5534
|
+
// a concurrent pushed result and otherwise confirm the new heartbeat.
|
|
5535
|
+
const winner = parseDetachedRunnerResult(
|
|
5536
|
+
await input.readRunnerTerminal(suspension.runnerAttempt),
|
|
5537
|
+
);
|
|
5538
|
+
if (winner) return settle(winner);
|
|
5539
|
+
if (await input.readRunnerLiveness()) return suspend();
|
|
5540
|
+
}
|
|
5257
5541
|
if (!attemptExists) {
|
|
5258
5542
|
throw new Error(
|
|
5259
5543
|
`Detached runner attempt ${suspension.runnerAttempt} for ${input.runId} disappeared before terminal arbitration.`,
|
|
@@ -5297,51 +5581,31 @@ export async function resolveDetachedRunnerOutcome(input: {
|
|
|
5297
5581
|
? Math.max(0, now - heartbeatAtMs)
|
|
5298
5582
|
: null;
|
|
5299
5583
|
const reachedCeiling = now >= suspension.startedAtMs + suspension.ceilingMs;
|
|
5300
|
-
const
|
|
5301
|
-
const heartbeatTimeoutMs =
|
|
5302
|
-
Number.isFinite(configuredHeartbeatTimeoutMs) &&
|
|
5303
|
-
configuredHeartbeatTimeoutMs >= 0
|
|
5304
|
-
? configuredHeartbeatTimeoutMs
|
|
5305
|
-
: RUNTIME_CLAIM_LEASE_SECONDS * 1000;
|
|
5306
|
-
const reparkDetachedRunner = (): PlayRunnerResult => ({
|
|
5307
|
-
status: 'suspended',
|
|
5308
|
-
suspension,
|
|
5309
|
-
logs: [],
|
|
5310
|
-
stats: {},
|
|
5311
|
-
steps: [],
|
|
5312
|
-
checkpoint: input.checkpoint,
|
|
5313
|
-
tableNamespace: null,
|
|
5314
|
-
});
|
|
5584
|
+
const heartbeatTimeoutMs = detachedRunnerHeartbeatTimeoutMs(suspension);
|
|
5315
5585
|
if (!reachedCeiling && heartbeatAgeMs !== null) {
|
|
5316
5586
|
if (heartbeatAgeMs < heartbeatTimeoutMs) {
|
|
5317
5587
|
// We woke on an older deadline while the runner renewed liveness. Park
|
|
5318
5588
|
// again from the new durable timestamp; do not touch Daytona.
|
|
5319
|
-
return
|
|
5589
|
+
return suspend();
|
|
5320
5590
|
}
|
|
5321
5591
|
}
|
|
5322
|
-
const
|
|
5323
|
-
|
|
5324
|
-
|
|
5325
|
-
|
|
5326
|
-
|
|
5327
|
-
|
|
5328
|
-
|
|
5329
|
-
|
|
5330
|
-
|
|
5331
|
-
|
|
5332
|
-
|
|
5333
|
-
|
|
5334
|
-
|
|
5335
|
-
|
|
5336
|
-
|
|
5337
|
-
|
|
5338
|
-
|
|
5339
|
-
liveness,
|
|
5340
|
-
heartbeatAgeMs,
|
|
5341
|
-
pushedTerminalPresent: false,
|
|
5342
|
-
}),
|
|
5343
|
-
);
|
|
5344
|
-
};
|
|
5592
|
+
const confirmation = input.checkpoint.detachedRunnerSilenceConfirmation;
|
|
5593
|
+
const heartbeatIdentity =
|
|
5594
|
+
liveness?.heartbeatIdentity ?? liveness?.heartbeatAt ?? null;
|
|
5595
|
+
const silenceConfirmed = Boolean(
|
|
5596
|
+
confirmation &&
|
|
5597
|
+
confirmation.boundaryId === suspension.boundaryId &&
|
|
5598
|
+
confirmation.runnerAttempt === suspension.runnerAttempt &&
|
|
5599
|
+
confirmation.heartbeatIdentity === heartbeatIdentity,
|
|
5600
|
+
);
|
|
5601
|
+
const reportLivenessExpired = () =>
|
|
5602
|
+
reportDetachedRunnerLivenessExpired({
|
|
5603
|
+
runId: input.runId,
|
|
5604
|
+
suspension,
|
|
5605
|
+
observedAtMs: now,
|
|
5606
|
+
liveness,
|
|
5607
|
+
heartbeatAgeMs,
|
|
5608
|
+
});
|
|
5345
5609
|
if (reachedCeiling) {
|
|
5346
5610
|
reportLivenessExpired();
|
|
5347
5611
|
return settleFirstWriteWinner({
|
|
@@ -5407,7 +5671,7 @@ export async function resolveDetachedRunnerOutcome(input: {
|
|
|
5407
5671
|
}
|
|
5408
5672
|
}
|
|
5409
5673
|
// Inspection takes seconds. A terminal or heartbeat that landed meanwhile
|
|
5410
|
-
// outranks everything read before it.
|
|
5674
|
+
// outranks everything read before it, including a sub-millisecond renewal.
|
|
5411
5675
|
const storedDuringInspection = parseDetachedRunnerResult(
|
|
5412
5676
|
await input.readRunnerTerminal(suspension.runnerAttempt),
|
|
5413
5677
|
);
|
|
@@ -5415,23 +5679,92 @@ export async function resolveDetachedRunnerOutcome(input: {
|
|
|
5415
5679
|
return settle(storedDuringInspection);
|
|
5416
5680
|
}
|
|
5417
5681
|
const latestLiveness = await input.readRunnerLiveness();
|
|
5418
|
-
const latestHeartbeatAtMs = latestLiveness?.heartbeatAt
|
|
5419
|
-
? Date.parse(latestLiveness.heartbeatAt)
|
|
5420
|
-
: Number.NaN;
|
|
5421
5682
|
if (
|
|
5422
|
-
|
|
5423
|
-
|
|
5683
|
+
(latestLiveness?.heartbeatIdentity ??
|
|
5684
|
+
latestLiveness?.heartbeatAt ??
|
|
5685
|
+
null) !== heartbeatIdentity
|
|
5424
5686
|
) {
|
|
5425
|
-
return
|
|
5687
|
+
return suspend();
|
|
5426
5688
|
}
|
|
5689
|
+
if (!silenceConfirmed) return suspend();
|
|
5427
5690
|
if (
|
|
5428
5691
|
input.inspectDetachedRunner &&
|
|
5429
5692
|
!provenGone &&
|
|
5430
5693
|
!detachedRunnerInspectionProvesGone(inspection)
|
|
5431
5694
|
) {
|
|
5432
|
-
return
|
|
5695
|
+
return suspend();
|
|
5433
5696
|
}
|
|
5434
5697
|
reportLivenessExpired();
|
|
5698
|
+
return settleFirstWriteWinner(
|
|
5699
|
+
detachedRunnerLostResult({
|
|
5700
|
+
suspension,
|
|
5701
|
+
observedAtMs: now,
|
|
5702
|
+
livenessStatus: liveness?.status,
|
|
5703
|
+
heartbeatAtMs,
|
|
5704
|
+
heartbeatAgeMs,
|
|
5705
|
+
inspectionAttempted: Boolean(input.inspectDetachedRunner),
|
|
5706
|
+
inspection,
|
|
5707
|
+
inspectionFailed,
|
|
5708
|
+
}),
|
|
5709
|
+
heartbeatIdentity,
|
|
5710
|
+
);
|
|
5711
|
+
}
|
|
5712
|
+
|
|
5713
|
+
/** Legacy parked rows predate heartbeatTimeoutMs, and the field is durable JSON. */
|
|
5714
|
+
function detachedRunnerHeartbeatTimeoutMs(
|
|
5715
|
+
suspension: DetachedRunnerSuspension,
|
|
5716
|
+
): number {
|
|
5717
|
+
const configured = Number(suspension.heartbeatTimeoutMs);
|
|
5718
|
+
return Number.isFinite(configured) && configured >= 0
|
|
5719
|
+
? configured
|
|
5720
|
+
: RUNTIME_CLAIM_LEASE_SECONDS * 1000;
|
|
5721
|
+
}
|
|
5722
|
+
|
|
5723
|
+
function reportDetachedRunnerLivenessExpired(input: {
|
|
5724
|
+
runId: string;
|
|
5725
|
+
suspension: DetachedRunnerSuspension;
|
|
5726
|
+
observedAtMs: number;
|
|
5727
|
+
liveness: unknown;
|
|
5728
|
+
heartbeatAgeMs: number | null;
|
|
5729
|
+
}): void {
|
|
5730
|
+
const { suspension } = input;
|
|
5731
|
+
// This event is consumed directly by the operational-health dashboard.
|
|
5732
|
+
// Keep its second argument strict JSON so Fly's text log stream remains an
|
|
5733
|
+
// executable producer contract, independent of Bun's object-inspect format.
|
|
5734
|
+
console.error(
|
|
5735
|
+
'[absurd-runtime.worker] detached_runner_liveness_expired',
|
|
5736
|
+
JSON.stringify({
|
|
5737
|
+
runId: input.runId,
|
|
5738
|
+
runnerAttempt: suspension.runnerAttempt,
|
|
5739
|
+
sandboxId: suspension.sandboxId,
|
|
5740
|
+
sandboxProvider: suspension.sandboxProvider ?? 'daytona',
|
|
5741
|
+
sessionId: suspension.sessionId,
|
|
5742
|
+
cmdId: suspension.cmdId,
|
|
5743
|
+
startedAtMs: suspension.startedAtMs,
|
|
5744
|
+
heartbeatTimeoutMs: detachedRunnerHeartbeatTimeoutMs(suspension),
|
|
5745
|
+
ceilingMs: suspension.ceilingMs,
|
|
5746
|
+
observedAtMs: input.observedAtMs,
|
|
5747
|
+
reachedCeiling:
|
|
5748
|
+
input.observedAtMs >= suspension.startedAtMs + suspension.ceilingMs,
|
|
5749
|
+
liveness: input.liveness,
|
|
5750
|
+
heartbeatAgeMs: input.heartbeatAgeMs,
|
|
5751
|
+
pushedTerminalPresent: false,
|
|
5752
|
+
}),
|
|
5753
|
+
);
|
|
5754
|
+
}
|
|
5755
|
+
|
|
5756
|
+
/** The one failed terminal for a runner the provider proved gone. */
|
|
5757
|
+
function detachedRunnerLostResult(input: {
|
|
5758
|
+
suspension: DetachedRunnerSuspension;
|
|
5759
|
+
observedAtMs: number;
|
|
5760
|
+
livenessStatus: unknown;
|
|
5761
|
+
heartbeatAtMs: number;
|
|
5762
|
+
heartbeatAgeMs: number | null;
|
|
5763
|
+
inspectionAttempted: boolean;
|
|
5764
|
+
inspection: DetachedRunnerInspection | null;
|
|
5765
|
+
inspectionFailed: boolean;
|
|
5766
|
+
}): PlayRunnerResult {
|
|
5767
|
+
const { suspension, inspection } = input;
|
|
5435
5768
|
const safeDiagnosticLabel = (value: unknown): string =>
|
|
5436
5769
|
typeof value === 'string' && /^[a-z0-9_]{1,80}$/i.test(value)
|
|
5437
5770
|
? value
|
|
@@ -5439,36 +5772,37 @@ export async function resolveDetachedRunnerOutcome(input: {
|
|
|
5439
5772
|
const runnerLiveness = {
|
|
5440
5773
|
event: 'detached_runner_liveness_expired',
|
|
5441
5774
|
runnerAttempt: suspension.runnerAttempt,
|
|
5442
|
-
livenessStatus: safeDiagnosticLabel(
|
|
5443
|
-
lastHeartbeatAt: Number.isFinite(heartbeatAtMs)
|
|
5444
|
-
? new Date(heartbeatAtMs).toISOString()
|
|
5775
|
+
livenessStatus: safeDiagnosticLabel(input.livenessStatus),
|
|
5776
|
+
lastHeartbeatAt: Number.isFinite(input.heartbeatAtMs)
|
|
5777
|
+
? new Date(input.heartbeatAtMs).toISOString()
|
|
5445
5778
|
: null,
|
|
5446
|
-
heartbeatAgeMs,
|
|
5447
|
-
heartbeatTimeoutMs,
|
|
5448
|
-
runElapsedMs: Math.max(0,
|
|
5779
|
+
heartbeatAgeMs: input.heartbeatAgeMs,
|
|
5780
|
+
heartbeatTimeoutMs: detachedRunnerHeartbeatTimeoutMs(suspension),
|
|
5781
|
+
runElapsedMs: Math.max(0, input.observedAtMs - suspension.startedAtMs),
|
|
5449
5782
|
runtimeCeilingMs: suspension.ceilingMs,
|
|
5450
|
-
reachedRuntimeCeiling:
|
|
5783
|
+
reachedRuntimeCeiling:
|
|
5784
|
+
input.observedAtMs >= suspension.startedAtMs + suspension.ceilingMs,
|
|
5451
5785
|
terminalResultFound: false,
|
|
5452
|
-
inspectionAttempted:
|
|
5786
|
+
inspectionAttempted: input.inspectionAttempted,
|
|
5453
5787
|
inspectionStage: inspection
|
|
5454
5788
|
? safeDiagnosticLabel(inspection.diagnosis?.stage)
|
|
5455
|
-
: inspectionFailed
|
|
5789
|
+
: input.inspectionFailed
|
|
5456
5790
|
? 'inspection_failed'
|
|
5457
|
-
: input.
|
|
5791
|
+
: input.inspectionAttempted
|
|
5458
5792
|
? 'inspection_empty'
|
|
5459
5793
|
: 'not_configured',
|
|
5460
5794
|
inspectionExitCode: inspection?.exitCode ?? null,
|
|
5461
5795
|
inspectedResultStatus: inspection?.result?.status ?? null,
|
|
5462
5796
|
};
|
|
5463
|
-
return
|
|
5797
|
+
return {
|
|
5464
5798
|
status: 'failed',
|
|
5465
|
-
error: `RUNTIME_RUNNER_LOST: Detached runner heartbeat expired after ${heartbeatAgeMs ?? 'unknown'}ms without a terminal result. Deepline did not automatically retry because external actions may already have completed.`,
|
|
5799
|
+
error: `RUNTIME_RUNNER_LOST: Detached runner heartbeat expired after ${input.heartbeatAgeMs ?? 'unknown'}ms without a terminal result. Deepline did not automatically retry because external actions may already have completed.`,
|
|
5466
5800
|
logs: [`[runtime.failure_diagnostic] ${JSON.stringify(runnerLiveness)}`],
|
|
5467
5801
|
stats: { runnerLiveness },
|
|
5468
5802
|
steps: [],
|
|
5469
5803
|
checkpoint: null,
|
|
5470
5804
|
tableNamespace: null,
|
|
5471
|
-
}
|
|
5805
|
+
};
|
|
5472
5806
|
}
|
|
5473
5807
|
|
|
5474
5808
|
type RuntimeCleanupApiContext = Parameters<
|
|
@@ -6283,6 +6617,71 @@ async function cancelAbsurdEngineTaskAfterSchedulerTerminal(input: {
|
|
|
6283
6617
|
}
|
|
6284
6618
|
}
|
|
6285
6619
|
|
|
6620
|
+
type SettledRunnerTerminal = {
|
|
6621
|
+
terminal: PostgresSchedulerCompletedRunnerTerminal;
|
|
6622
|
+
launch: PlaySchedulerSubmitInput;
|
|
6623
|
+
runnerResult: PlayRunnerResult;
|
|
6624
|
+
schedulerStatus: 'completed' | 'failed';
|
|
6625
|
+
suspension: DetachedRunnerSuspension | null;
|
|
6626
|
+
/** Another writer committed this terminal and owns what follows it. */
|
|
6627
|
+
terminalizedByAnotherWorker: boolean;
|
|
6628
|
+
};
|
|
6629
|
+
|
|
6630
|
+
/**
|
|
6631
|
+
* What a lane worker does after its own terminal commits: fold the run's
|
|
6632
|
+
* durable facts into progress, finalize it, and ask for the terminal outbox
|
|
6633
|
+
* row. Never rejects: terminal truth is already durable, and the run-ledger
|
|
6634
|
+
* projector delivers the outbox row without this.
|
|
6635
|
+
*/
|
|
6636
|
+
async function projectRecoveredRunnerTerminal(input: {
|
|
6637
|
+
deps: AbsurdRuntimeTerminalProjectionDeps;
|
|
6638
|
+
launch: PlaySchedulerSubmitInput;
|
|
6639
|
+
runId: string;
|
|
6640
|
+
attempt: number;
|
|
6641
|
+
runnerResult: PlayRunnerResult;
|
|
6642
|
+
options: Pick<PostgresSchedulerOptions, 'schema'>;
|
|
6643
|
+
signal?: AbortSignal;
|
|
6644
|
+
}): Promise<void> {
|
|
6645
|
+
const progress = (async () => {
|
|
6646
|
+
const reporter = input.deps.createProgressReporter(
|
|
6647
|
+
input.launch,
|
|
6648
|
+
input.attempt,
|
|
6649
|
+
);
|
|
6650
|
+
await reconcileTerminalTruthIntoReporter({
|
|
6651
|
+
deps: input.deps,
|
|
6652
|
+
launch: input.launch,
|
|
6653
|
+
runId: input.runId,
|
|
6654
|
+
attempt: input.attempt,
|
|
6655
|
+
options: input.options,
|
|
6656
|
+
reporter,
|
|
6657
|
+
});
|
|
6658
|
+
await reporter.finalize(input.runnerResult, {
|
|
6659
|
+
status: 'running',
|
|
6660
|
+
skipTerminalLogReplay: true,
|
|
6661
|
+
terminalLogProducerAttempt: input.attempt,
|
|
6662
|
+
});
|
|
6663
|
+
})().catch((error) => {
|
|
6664
|
+
console.warn(
|
|
6665
|
+
'[absurd-runtime.worker] recovered_terminal_projection_failed',
|
|
6666
|
+
{
|
|
6667
|
+
runId: input.runId,
|
|
6668
|
+
attempt: input.attempt,
|
|
6669
|
+
error: error instanceof Error ? error.message : String(error),
|
|
6670
|
+
},
|
|
6671
|
+
);
|
|
6672
|
+
});
|
|
6673
|
+
await Promise.all([
|
|
6674
|
+
progress,
|
|
6675
|
+
dispatchTerminalOutboxProjection({
|
|
6676
|
+
deps: input.deps,
|
|
6677
|
+
launch: input.launch,
|
|
6678
|
+
runId: input.runId,
|
|
6679
|
+
options: input.options,
|
|
6680
|
+
...(input.signal ? { signal: input.signal } : {}),
|
|
6681
|
+
}),
|
|
6682
|
+
]);
|
|
6683
|
+
}
|
|
6684
|
+
|
|
6286
6685
|
/**
|
|
6287
6686
|
* Close completed runner handoffs before this lane's claim loop starts.
|
|
6288
6687
|
*
|
|
@@ -6299,9 +6698,11 @@ async function cancelAbsurdEngineTaskAfterSchedulerTerminal(input: {
|
|
|
6299
6698
|
export async function recoverAbsurdRuntimeCompletedRunnerTerminals(input: {
|
|
6300
6699
|
client: PostgresSchedulerQueryClient;
|
|
6301
6700
|
/** A lane worker supplies post-terminal projection and sandbox cleanup. The
|
|
6302
|
-
* fixed controller intentionally omits these: the
|
|
6303
|
-
* durable outbox is delivered by its singleton outbox
|
|
6304
|
-
* worker → gateway retry chain during global
|
|
6701
|
+
* fixed controller's lane-wide recovery intentionally omits these: the
|
|
6702
|
+
* terminal transaction's durable outbox is delivered by its singleton outbox
|
|
6703
|
+
* controller, avoiding a worker → gateway retry chain during global
|
|
6704
|
+
* recovery. The liveness sweep projects what it settles itself, inside its
|
|
6705
|
+
* pass budget. */
|
|
6305
6706
|
deps?: AbsurdRuntimeWorkerDeps;
|
|
6306
6707
|
queue: string;
|
|
6307
6708
|
schedulerOptions?: Pick<PostgresSchedulerOptions, 'schema'>;
|
|
@@ -6309,6 +6710,8 @@ export async function recoverAbsurdRuntimeCompletedRunnerTerminals(input: {
|
|
|
6309
6710
|
limit?: number;
|
|
6310
6711
|
/** Preselected by the fixed controller's one-query cross-lane scan. */
|
|
6311
6712
|
terminals?: readonly PostgresSchedulerCompletedRunnerTerminal[];
|
|
6713
|
+
/** Told of each terminal now durable, whichever writer won it. */
|
|
6714
|
+
onTerminalSettled?: (settled: SettledRunnerTerminal) => void;
|
|
6312
6715
|
}): Promise<{ recovered: number; skipped: number }> {
|
|
6313
6716
|
const options = input.schedulerOptions ?? {};
|
|
6314
6717
|
const terminals =
|
|
@@ -6356,6 +6759,22 @@ export async function recoverAbsurdRuntimeCompletedRunnerTerminals(input: {
|
|
|
6356
6759
|
// outbox semantics instead of inventing a recovery-only terminal write.
|
|
6357
6760
|
let schedulerStatus: 'completed' | 'failed';
|
|
6358
6761
|
let terminalizedByAnotherWorker = false;
|
|
6762
|
+
const parsedSuspension = parseOverdueDetachedRunnerSuspension(
|
|
6763
|
+
terminal.suspension,
|
|
6764
|
+
);
|
|
6765
|
+
// The run's suspension can name a newer attempt's sandbox. Only this
|
|
6766
|
+
// terminal's own sandbox may supply its crash diagnostic or canary fact.
|
|
6767
|
+
const suspension =
|
|
6768
|
+
parsedSuspension?.runnerAttempt === terminal.attempt
|
|
6769
|
+
? parsedSuspension
|
|
6770
|
+
: null;
|
|
6771
|
+
const sandboxCrashDiagnostic =
|
|
6772
|
+
runnerResult.status === 'failed' && suspension
|
|
6773
|
+
? sandboxCrashDiagnosticRequest({
|
|
6774
|
+
error: runnerResult.error,
|
|
6775
|
+
suspension,
|
|
6776
|
+
})
|
|
6777
|
+
: undefined;
|
|
6359
6778
|
try {
|
|
6360
6779
|
await markPostgresSchedulerAttemptRunning(
|
|
6361
6780
|
input.client,
|
|
@@ -6379,6 +6798,7 @@ export async function recoverAbsurdRuntimeCompletedRunnerTerminals(input: {
|
|
|
6379
6798
|
{
|
|
6380
6799
|
...options,
|
|
6381
6800
|
terminalLogOutbox: 'ctx_log_tail',
|
|
6801
|
+
...(sandboxCrashDiagnostic ? { sandboxCrashDiagnostic } : {}),
|
|
6382
6802
|
},
|
|
6383
6803
|
);
|
|
6384
6804
|
if (settled.status !== 'completed' && settled.status !== 'failed') {
|
|
@@ -6422,44 +6842,25 @@ export async function recoverAbsurdRuntimeCompletedRunnerTerminals(input: {
|
|
|
6422
6842
|
} else {
|
|
6423
6843
|
recovered += 1;
|
|
6424
6844
|
}
|
|
6845
|
+
input.onTerminalSettled?.({
|
|
6846
|
+
terminal,
|
|
6847
|
+
launch,
|
|
6848
|
+
runnerResult,
|
|
6849
|
+
schedulerStatus,
|
|
6850
|
+
suspension,
|
|
6851
|
+
terminalizedByAnotherWorker,
|
|
6852
|
+
});
|
|
6425
6853
|
|
|
6426
6854
|
// The worker which won the scheduler terminal can still disappear after
|
|
6427
6855
|
// its atomic write but before best-effort projection. Cleanup is already
|
|
6428
6856
|
// a durable obligation owned by the independent cleanup controller.
|
|
6429
6857
|
if (input.deps) {
|
|
6430
|
-
|
|
6431
|
-
void (async () => {
|
|
6432
|
-
const reporter = workerDeps.createProgressReporter(
|
|
6433
|
-
launch,
|
|
6434
|
-
terminal.attempt,
|
|
6435
|
-
);
|
|
6436
|
-
await reconcileTerminalTruthIntoReporter({
|
|
6437
|
-
deps: workerDeps,
|
|
6438
|
-
launch,
|
|
6439
|
-
runId: terminal.runId,
|
|
6440
|
-
attempt: terminal.attempt,
|
|
6441
|
-
options,
|
|
6442
|
-
reporter,
|
|
6443
|
-
});
|
|
6444
|
-
await reporter.finalize(runnerResult, {
|
|
6445
|
-
status: 'running',
|
|
6446
|
-
skipTerminalLogReplay: true,
|
|
6447
|
-
terminalLogProducerAttempt: terminal.attempt,
|
|
6448
|
-
});
|
|
6449
|
-
})().catch((error) => {
|
|
6450
|
-
console.warn(
|
|
6451
|
-
'[absurd-runtime.worker] recovered_terminal_projection_failed',
|
|
6452
|
-
{
|
|
6453
|
-
runId: terminal.runId,
|
|
6454
|
-
attempt: terminal.attempt,
|
|
6455
|
-
error: error instanceof Error ? error.message : String(error),
|
|
6456
|
-
},
|
|
6457
|
-
);
|
|
6458
|
-
});
|
|
6459
|
-
requestTerminalOutboxProjection({
|
|
6858
|
+
void projectRecoveredRunnerTerminal({
|
|
6460
6859
|
deps: input.deps,
|
|
6461
6860
|
launch,
|
|
6462
6861
|
runId: terminal.runId,
|
|
6862
|
+
attempt: terminal.attempt,
|
|
6863
|
+
runnerResult,
|
|
6463
6864
|
options,
|
|
6464
6865
|
});
|
|
6465
6866
|
}
|
|
@@ -6735,6 +7136,661 @@ export async function recoverAbsurdRuntimeOverdueDetachedRunnerCeilingsAcrossAll
|
|
|
6735
7136
|
return recoverAbsurdRuntimeOverdueDetachedRunnerCeilings(input);
|
|
6736
7137
|
}
|
|
6737
7138
|
|
|
7139
|
+
/** Inside `ABSURD_OVERDUE_DETACHED_RUNNER_RECOVERY_INTERVAL_MS`. */
|
|
7140
|
+
export const ABSURD_DETACHED_RUNNER_LIVENESS_PASS_BUDGET_MS = 10_000;
|
|
7141
|
+
/** The fixed controller stops waiting for a pass here, even on a stuck store. */
|
|
7142
|
+
export const ABSURD_DETACHED_RUNNER_LIVENESS_HARD_STOP_MS = 12_000;
|
|
7143
|
+
/** Reads hold memory in a 256 MiB controller, so keep them few. Hung reads keep
|
|
7144
|
+
* their slots across passes, which stops a stuck provider piling them up. */
|
|
7145
|
+
export const ABSURD_DETACHED_RUNNER_LIVENESS_READS_PER_PROVIDER = 3;
|
|
7146
|
+
/** Each provider's read has a shorter deadline of its own. This backstop gives
|
|
7147
|
+
* back the slot of any read that outlives it instead of blinding the sweep
|
|
7148
|
+
* until the controller restarts. */
|
|
7149
|
+
export const ABSURD_DETACHED_RUNNER_LIVENESS_READ_DEADLINE_MS = 60_000;
|
|
7150
|
+
/** Past this many unsettled reads a provider gets no more, which bounds what
|
|
7151
|
+
* a provider that never answers can hold. */
|
|
7152
|
+
export const ABSURD_DETACHED_RUNNER_LIVENESS_MAX_ABANDONED_READS =
|
|
7153
|
+
2 * ABSURD_DETACHED_RUNNER_LIVENESS_READS_PER_PROVIDER;
|
|
7154
|
+
export const ABSURD_DETACHED_RUNNER_LIVENESS_PAGE_LIMIT = 100;
|
|
7155
|
+
/** How soon an unproven sandbox may be read again. */
|
|
7156
|
+
export const ABSURD_DETACHED_RUNNER_LIVENESS_RECHECK_MS = 60_000;
|
|
7157
|
+
|
|
7158
|
+
const ABSURD_DETACHED_RUNNER_LIVENESS_PROVIDERS = ['daytona', 'modal'] as const;
|
|
7159
|
+
/** Inspection stages that carry no answer from the provider about the sandbox. */
|
|
7160
|
+
const DETACHED_RUNNER_UNANSWERED_STAGES: ReadonlySet<string> = new Set([
|
|
7161
|
+
'sandbox_lookup_failed',
|
|
7162
|
+
'missing_routing_domain',
|
|
7163
|
+
'wrong_routing_domain',
|
|
7164
|
+
]);
|
|
7165
|
+
type DetachedRunnerLivenessProvider =
|
|
7166
|
+
(typeof ABSURD_DETACHED_RUNNER_LIVENESS_PROVIDERS)[number];
|
|
7167
|
+
|
|
7168
|
+
/**
|
|
7169
|
+
* Process-local on purpose. Losing it costs each candidate one extra read, and
|
|
7170
|
+
* `nextCheckAtMs` is bounded by construction: a pass adds at most a page per
|
|
7171
|
+
* provider, and every entry is pruned once its recheck time passes.
|
|
7172
|
+
*/
|
|
7173
|
+
export type AbsurdDetachedRunnerLivenessSweepState = {
|
|
7174
|
+
generation: number;
|
|
7175
|
+
nextCheckAtMs: Map<string, number>;
|
|
7176
|
+
afterRunIds: Record<DetachedRunnerLivenessProvider, string | null>;
|
|
7177
|
+
readsInFlight: Record<DetachedRunnerLivenessProvider, number>;
|
|
7178
|
+
/** Reads past their deadline that have still not settled. */
|
|
7179
|
+
abandonedReads: Record<DetachedRunnerLivenessProvider, number>;
|
|
7180
|
+
/** A settlement runs, perhaps for a pass that has returned. No other starts
|
|
7181
|
+
* until it ends, so a stalled store holds one settlement, not one per pass. */
|
|
7182
|
+
settling: boolean;
|
|
7183
|
+
/** Likewise for a settled terminal's progress and outbox projection. */
|
|
7184
|
+
projecting: boolean;
|
|
7185
|
+
};
|
|
7186
|
+
|
|
7187
|
+
export function createAbsurdDetachedRunnerLivenessSweepState(): AbsurdDetachedRunnerLivenessSweepState {
|
|
7188
|
+
return {
|
|
7189
|
+
generation: 0,
|
|
7190
|
+
nextCheckAtMs: new Map(),
|
|
7191
|
+
afterRunIds: { daytona: null, modal: null },
|
|
7192
|
+
readsInFlight: { daytona: 0, modal: 0 },
|
|
7193
|
+
abandonedReads: { daytona: 0, modal: 0 },
|
|
7194
|
+
settling: false,
|
|
7195
|
+
projecting: false,
|
|
7196
|
+
};
|
|
7197
|
+
}
|
|
7198
|
+
|
|
7199
|
+
export type AbsurdDetachedRunnerLivenessOutcome = {
|
|
7200
|
+
stale: number;
|
|
7201
|
+
notDue: number;
|
|
7202
|
+
inspected: number;
|
|
7203
|
+
/** Enforce mode: recorded terminals this pass tried to settle. */
|
|
7204
|
+
recorded: number;
|
|
7205
|
+
unproven: number;
|
|
7206
|
+
proven: number;
|
|
7207
|
+
/** Observe mode: proven gone, and nothing was written. */
|
|
7208
|
+
wouldSettle: number;
|
|
7209
|
+
/** A heartbeat, terminal or cancellation moved during the read and won. */
|
|
7210
|
+
fenced: number;
|
|
7211
|
+
settled: number;
|
|
7212
|
+
settlementFailed: number;
|
|
7213
|
+
invalidSuspensions: number;
|
|
7214
|
+
/** No answer: the read failed, hung, or was not started. Never proof. */
|
|
7215
|
+
unreadable: number;
|
|
7216
|
+
/** Providers with due sandboxes and no answered read this pass, so their
|
|
7217
|
+
* silence in `wouldSettle` says nothing about false positives. */
|
|
7218
|
+
blindProviders: DetachedRunnerLivenessProvider[];
|
|
7219
|
+
budgetExhausted: boolean;
|
|
7220
|
+
};
|
|
7221
|
+
|
|
7222
|
+
async function runDetachedRunnerLivenessLane<T>(input: {
|
|
7223
|
+
items: readonly T[];
|
|
7224
|
+
concurrency: number;
|
|
7225
|
+
active: () => boolean;
|
|
7226
|
+
visit: (item: T) => Promise<void>;
|
|
7227
|
+
}): Promise<number> {
|
|
7228
|
+
let visited = 0;
|
|
7229
|
+
await Promise.all(
|
|
7230
|
+
Array.from(
|
|
7231
|
+
{ length: Math.min(input.concurrency, input.items.length) },
|
|
7232
|
+
async () => {
|
|
7233
|
+
while (visited < input.items.length && input.active()) {
|
|
7234
|
+
const item = input.items[visited]!;
|
|
7235
|
+
visited += 1;
|
|
7236
|
+
await input.visit(item);
|
|
7237
|
+
}
|
|
7238
|
+
},
|
|
7239
|
+
),
|
|
7240
|
+
);
|
|
7241
|
+
return visited;
|
|
7242
|
+
}
|
|
7243
|
+
|
|
7244
|
+
/**
|
|
7245
|
+
* One pass of the fixed controller's liveness sweep. Parked confirmation slices
|
|
7246
|
+
* wait behind their own org's backlog in org-ranked claims, which delayed 298
|
|
7247
|
+
* lost verdicts about 27 minutes on Oct 29, so this reads each stale sandbox
|
|
7248
|
+
* outside those claims. An ended sandbox settles at once. A running or
|
|
7249
|
+
* unreadable one is left alone, however long it stays silent.
|
|
7250
|
+
*
|
|
7251
|
+
* Discovery reads, then no scheduler client is held across a provider read, and
|
|
7252
|
+
* the verdict is one conditional first write. That write is the same runner
|
|
7253
|
+
* terminal the parked attempt would record, so a racing wake commits the same
|
|
7254
|
+
* result, and the same settlement path leaves the same cleanup, billing and
|
|
7255
|
+
* outbox facts on any lane. No customer code runs and nothing is replayed.
|
|
7256
|
+
* A terminal this pass commits is then projected and released as its parked
|
|
7257
|
+
* worker would have; one another writer committed is left to that writer.
|
|
7258
|
+
*
|
|
7259
|
+
* The pass waits on verdict writes, settlement and projection only until its
|
|
7260
|
+
* budget. A settlement or projection that overruns keeps its slot, so no pass
|
|
7261
|
+
* starts another until it ends; a verdict that commits late settles next pass.
|
|
7262
|
+
*
|
|
7263
|
+
* In `observe` mode the pass reads providers and reports what it would settle,
|
|
7264
|
+
* and writes nothing.
|
|
7265
|
+
*/
|
|
7266
|
+
export async function recoverAbsurdRuntimeStaleDetachedRunners(input: {
|
|
7267
|
+
withClient: <T>(
|
|
7268
|
+
fn: (client: PostgresSchedulerQueryClient) => Promise<T>,
|
|
7269
|
+
) => Promise<T>;
|
|
7270
|
+
state: AbsurdDetachedRunnerLivenessSweepState;
|
|
7271
|
+
mode: 'observe' | 'enforce';
|
|
7272
|
+
inspect?: (
|
|
7273
|
+
suspension: DetachedRunnerSuspension,
|
|
7274
|
+
) => Promise<DetachedRunnerInspection | null>;
|
|
7275
|
+
schedulerOptions?: Pick<PostgresSchedulerOptions, 'schema'>;
|
|
7276
|
+
/** Whether a Modal canary run's terminal counts as production evidence. */
|
|
7277
|
+
canaryProduction: boolean;
|
|
7278
|
+
/** Projects each terminal this sweep settles, as its parked worker would
|
|
7279
|
+
* have. Without it, only the run-ledger projector delivers the terminal. */
|
|
7280
|
+
projection?: AbsurdRuntimeTerminalProjectionDeps;
|
|
7281
|
+
pageLimit?: number;
|
|
7282
|
+
concurrency?: number;
|
|
7283
|
+
clock?: {
|
|
7284
|
+
now: () => number;
|
|
7285
|
+
budgetElapsed?: Promise<void>;
|
|
7286
|
+
readDeadline?: () => Promise<void>;
|
|
7287
|
+
};
|
|
7288
|
+
}): Promise<AbsurdDetachedRunnerLivenessOutcome> {
|
|
7289
|
+
const { state } = input;
|
|
7290
|
+
state.abandonedReads ??= { daytona: 0, modal: 0 };
|
|
7291
|
+
state.settling ??= false;
|
|
7292
|
+
state.projecting ??= false;
|
|
7293
|
+
const readDeadline =
|
|
7294
|
+
input.clock?.readDeadline ??
|
|
7295
|
+
(() =>
|
|
7296
|
+
new Promise<void>((resolve) => {
|
|
7297
|
+
setTimeout(
|
|
7298
|
+
resolve,
|
|
7299
|
+
ABSURD_DETACHED_RUNNER_LIVENESS_READ_DEADLINE_MS,
|
|
7300
|
+
).unref?.();
|
|
7301
|
+
}));
|
|
7302
|
+
const answeredReads: Record<DetachedRunnerLivenessProvider, number> = {
|
|
7303
|
+
daytona: 0,
|
|
7304
|
+
modal: 0,
|
|
7305
|
+
};
|
|
7306
|
+
const options = input.schedulerOptions ?? {};
|
|
7307
|
+
const now = input.clock?.now ?? Date.now;
|
|
7308
|
+
const limit = input.pageLimit ?? ABSURD_DETACHED_RUNNER_LIVENESS_PAGE_LIMIT;
|
|
7309
|
+
const concurrency =
|
|
7310
|
+
input.concurrency ?? ABSURD_DETACHED_RUNNER_LIVENESS_READS_PER_PROVIDER;
|
|
7311
|
+
// A provider-state read only: see `skipCapturedOutput`.
|
|
7312
|
+
const inspect =
|
|
7313
|
+
input.inspect ??
|
|
7314
|
+
((suspension: DetachedRunnerSuspension) =>
|
|
7315
|
+
defaultRuntimeSandboxReconciliation.inspectDetachedRunner(
|
|
7316
|
+
runtimeSandboxRefFromSuspension(suspension),
|
|
7317
|
+
suspension,
|
|
7318
|
+
{ skipCapturedOutput: true },
|
|
7319
|
+
));
|
|
7320
|
+
state.generation += 1;
|
|
7321
|
+
const generation = state.generation;
|
|
7322
|
+
const deadlineAtMs = now() + ABSURD_DETACHED_RUNNER_LIVENESS_PASS_BUDGET_MS;
|
|
7323
|
+
const active = () => state.generation === generation && now() < deadlineAtMs;
|
|
7324
|
+
for (const [key, dueAtMs] of state.nextCheckAtMs) {
|
|
7325
|
+
if (dueAtMs <= now()) state.nextCheckAtMs.delete(key);
|
|
7326
|
+
}
|
|
7327
|
+
const outcome: AbsurdDetachedRunnerLivenessOutcome = {
|
|
7328
|
+
stale: 0,
|
|
7329
|
+
notDue: 0,
|
|
7330
|
+
inspected: 0,
|
|
7331
|
+
recorded: 0,
|
|
7332
|
+
unproven: 0,
|
|
7333
|
+
proven: 0,
|
|
7334
|
+
wouldSettle: 0,
|
|
7335
|
+
fenced: 0,
|
|
7336
|
+
settled: 0,
|
|
7337
|
+
settlementFailed: 0,
|
|
7338
|
+
invalidSuspensions: 0,
|
|
7339
|
+
unreadable: 0,
|
|
7340
|
+
blindProviders: [],
|
|
7341
|
+
budgetExhausted: false,
|
|
7342
|
+
};
|
|
7343
|
+
const work = await input.withClient((client) =>
|
|
7344
|
+
listPostgresSchedulerDetachedRunnerLivenessWork(
|
|
7345
|
+
client,
|
|
7346
|
+
{
|
|
7347
|
+
limit,
|
|
7348
|
+
defaultHeartbeatTimeoutMs: RUNTIME_CLAIM_LEASE_SECONDS * 1000,
|
|
7349
|
+
afterRunIds: state.afterRunIds,
|
|
7350
|
+
},
|
|
7351
|
+
options,
|
|
7352
|
+
),
|
|
7353
|
+
);
|
|
7354
|
+
outcome.stale = work.stale.length;
|
|
7355
|
+
|
|
7356
|
+
let budgetTimer: ReturnType<typeof setTimeout> | undefined;
|
|
7357
|
+
const budgetElapsed =
|
|
7358
|
+
input.clock?.budgetElapsed ??
|
|
7359
|
+
new Promise<void>((resolve) => {
|
|
7360
|
+
budgetTimer = setTimeout(
|
|
7361
|
+
resolve,
|
|
7362
|
+
ABSURD_DETACHED_RUNNER_LIVENESS_PASS_BUDGET_MS,
|
|
7363
|
+
);
|
|
7364
|
+
});
|
|
7365
|
+
|
|
7366
|
+
/** Never waits on a projection an earlier settlement left running. */
|
|
7367
|
+
const projectTerminal = async (
|
|
7368
|
+
settled: SettledRunnerTerminal,
|
|
7369
|
+
): Promise<void> => {
|
|
7370
|
+
if (!input.projection) return;
|
|
7371
|
+
const { terminal } = settled;
|
|
7372
|
+
if (state.projecting) {
|
|
7373
|
+
// The run-ledger projector still delivers this terminal.
|
|
7374
|
+
console.warn(
|
|
7375
|
+
'[absurd-runtime.worker] detached_runner_liveness_projection_skipped',
|
|
7376
|
+
{ runId: terminal.runId, attempt: terminal.attempt },
|
|
7377
|
+
);
|
|
7378
|
+
return;
|
|
7379
|
+
}
|
|
7380
|
+
state.projecting = true;
|
|
7381
|
+
const deadline = new AbortController();
|
|
7382
|
+
void budgetElapsed.then(() => deadline.abort());
|
|
7383
|
+
await projectRecoveredRunnerTerminal({
|
|
7384
|
+
deps: input.projection,
|
|
7385
|
+
launch: settled.launch,
|
|
7386
|
+
runId: terminal.runId,
|
|
7387
|
+
attempt: terminal.attempt,
|
|
7388
|
+
runnerResult: settled.runnerResult,
|
|
7389
|
+
options,
|
|
7390
|
+
signal: deadline.signal,
|
|
7391
|
+
}).finally(() => {
|
|
7392
|
+
state.projecting = false;
|
|
7393
|
+
});
|
|
7394
|
+
};
|
|
7395
|
+
|
|
7396
|
+
const releaseAfterTerminal = async (settled: SettledRunnerTerminal) => {
|
|
7397
|
+
const { terminal, launch, runnerResult, suspension } = settled;
|
|
7398
|
+
try {
|
|
7399
|
+
if (suspension) {
|
|
7400
|
+
reportDetachedTerminalToModalCanary({
|
|
7401
|
+
launch,
|
|
7402
|
+
runId: terminal.runId,
|
|
7403
|
+
attempt: terminal.attempt,
|
|
7404
|
+
policy: resolveLaunchRuntimeSandboxPlacementPolicy({
|
|
7405
|
+
runtimeBackend: normalizePlayRuntimeBackend(
|
|
7406
|
+
launch.runtimeBackend || 'daytona',
|
|
7407
|
+
),
|
|
7408
|
+
runtimeSandboxPlacementPolicyId:
|
|
7409
|
+
launch.runtimeSandboxPlacementPolicyId,
|
|
7410
|
+
}),
|
|
7411
|
+
production: input.canaryProduction,
|
|
7412
|
+
suspension,
|
|
7413
|
+
runnerResult,
|
|
7414
|
+
});
|
|
7415
|
+
}
|
|
7416
|
+
} catch (error) {
|
|
7417
|
+
console.warn(
|
|
7418
|
+
'[absurd-runtime.worker] detached_runner_liveness_canary_failed',
|
|
7419
|
+
{
|
|
7420
|
+
runId: terminal.runId,
|
|
7421
|
+
error: error instanceof Error ? error.message : String(error),
|
|
7422
|
+
},
|
|
7423
|
+
);
|
|
7424
|
+
}
|
|
7425
|
+
// A customer database or the app can stall here, so this waits only for
|
|
7426
|
+
// the budget. Terminal truth is committed and an expired lease is the
|
|
7427
|
+
// crash path.
|
|
7428
|
+
await Promise.race([
|
|
7429
|
+
Promise.all([
|
|
7430
|
+
projectTerminal(settled),
|
|
7431
|
+
settled.schedulerStatus === 'failed'
|
|
7432
|
+
? releaseFailedTerminalAttempt({
|
|
7433
|
+
runWithClient: input.withClient,
|
|
7434
|
+
launch,
|
|
7435
|
+
runId: terminal.runId,
|
|
7436
|
+
generation: terminal.attempt,
|
|
7437
|
+
options,
|
|
7438
|
+
})
|
|
7439
|
+
: undefined,
|
|
7440
|
+
]),
|
|
7441
|
+
budgetElapsed,
|
|
7442
|
+
]);
|
|
7443
|
+
};
|
|
7444
|
+
|
|
7445
|
+
type Settlement =
|
|
7446
|
+
| { kind: 'settled'; entries: SettledRunnerTerminal[] }
|
|
7447
|
+
| { kind: 'deferred'; reason: 'pass_ended' | 'store_busy' };
|
|
7448
|
+
// One at a time, across passes too: a settlement loads the launch, which
|
|
7449
|
+
// can be large, and a store that stalls one must not gain one per pass.
|
|
7450
|
+
let settlementTurn: Promise<unknown> = Promise.resolve();
|
|
7451
|
+
const settleTerminal = async (
|
|
7452
|
+
terminal: PostgresSchedulerUnsettledRunnerTerminal,
|
|
7453
|
+
): Promise<void> => {
|
|
7454
|
+
const settle = settlementTurn.then(async (): Promise<Settlement> => {
|
|
7455
|
+
// Queued behind a stalled one, it never starts after its pass's budget.
|
|
7456
|
+
if (!active()) return { kind: 'deferred', reason: 'pass_ended' };
|
|
7457
|
+
if (state.settling) return { kind: 'deferred', reason: 'store_busy' };
|
|
7458
|
+
state.settling = true;
|
|
7459
|
+
try {
|
|
7460
|
+
const entries: SettledRunnerTerminal[] = [];
|
|
7461
|
+
await input.withClient(async (client) => {
|
|
7462
|
+
await recoverAbsurdRuntimeCompletedRunnerTerminals({
|
|
7463
|
+
client,
|
|
7464
|
+
// The run's immutable launch names its lane, so no lane list is
|
|
7465
|
+
// needed and a long-retired lane settles like a live one.
|
|
7466
|
+
queue: absurdQueueNameForRelease(
|
|
7467
|
+
absurdReleaseIdForLaunch({ absurdReleaseId: terminal.releaseId }),
|
|
7468
|
+
),
|
|
7469
|
+
schedulerOptions: options,
|
|
7470
|
+
terminals: [
|
|
7471
|
+
{
|
|
7472
|
+
...terminal,
|
|
7473
|
+
result: await readPostgresSchedulerRunnerTerminal(
|
|
7474
|
+
client,
|
|
7475
|
+
{ runId: terminal.runId, attempt: terminal.attempt },
|
|
7476
|
+
options,
|
|
7477
|
+
),
|
|
7478
|
+
},
|
|
7479
|
+
],
|
|
7480
|
+
onTerminalSettled: (entry) => entries.push(entry),
|
|
7481
|
+
});
|
|
7482
|
+
});
|
|
7483
|
+
return { kind: 'settled', entries };
|
|
7484
|
+
} finally {
|
|
7485
|
+
state.settling = false;
|
|
7486
|
+
}
|
|
7487
|
+
});
|
|
7488
|
+
settlementTurn = settle.catch(() => undefined);
|
|
7489
|
+
const logFailure = (error: unknown, afterPass: boolean) =>
|
|
7490
|
+
console.error(
|
|
7491
|
+
'[absurd-runtime.worker] detached_runner_liveness_settlement_failed',
|
|
7492
|
+
{
|
|
7493
|
+
runId: terminal.runId,
|
|
7494
|
+
attempt: terminal.attempt,
|
|
7495
|
+
afterPass,
|
|
7496
|
+
error: error instanceof Error ? error.message : String(error),
|
|
7497
|
+
},
|
|
7498
|
+
);
|
|
7499
|
+
try {
|
|
7500
|
+
const settlement = await Promise.race([
|
|
7501
|
+
settle,
|
|
7502
|
+
budgetElapsed.then(() => 'budget_spent' as const),
|
|
7503
|
+
]);
|
|
7504
|
+
if (settlement === 'budget_spent') {
|
|
7505
|
+
// It holds the store until it ends, and no settlement starts before.
|
|
7506
|
+
// A terminal it commits still releases its attempt and folds its
|
|
7507
|
+
// progress. The spent budget has already cancelled its outbox
|
|
7508
|
+
// request, so the run-ledger projector delivers that row.
|
|
7509
|
+
console.warn(
|
|
7510
|
+
'[absurd-runtime.worker] detached_runner_liveness_settlement_overran',
|
|
7511
|
+
{ runId: terminal.runId, attempt: terminal.attempt },
|
|
7512
|
+
);
|
|
7513
|
+
void settle
|
|
7514
|
+
.then(async (late) => {
|
|
7515
|
+
if (late.kind !== 'settled') return;
|
|
7516
|
+
for (const entry of late.entries) await releaseAfterTerminal(entry);
|
|
7517
|
+
})
|
|
7518
|
+
.catch((error: unknown) => logFailure(error, true));
|
|
7519
|
+
return;
|
|
7520
|
+
}
|
|
7521
|
+
if (settlement.kind === 'deferred') {
|
|
7522
|
+
// The recorded terminal stays for the next pass or the parked attempt.
|
|
7523
|
+
if (settlement.reason === 'store_busy') {
|
|
7524
|
+
console.warn(
|
|
7525
|
+
'[absurd-runtime.worker] detached_runner_liveness_settlement_deferred',
|
|
7526
|
+
{ runId: terminal.runId, attempt: terminal.attempt },
|
|
7527
|
+
);
|
|
7528
|
+
}
|
|
7529
|
+
return;
|
|
7530
|
+
}
|
|
7531
|
+
for (const entry of settlement.entries) {
|
|
7532
|
+
// A terminal another writer committed counts as theirs, but its
|
|
7533
|
+
// cleanup still runs here: the controller's lane recovery does none
|
|
7534
|
+
// of it, and a worker can die between its commit and its cleanup.
|
|
7535
|
+
// Each step is safe to repeat, as the lane recovery's projection is.
|
|
7536
|
+
if (entry.terminalizedByAnotherWorker) outcome.fenced += 1;
|
|
7537
|
+
else outcome.settled += 1;
|
|
7538
|
+
await releaseAfterTerminal(entry);
|
|
7539
|
+
}
|
|
7540
|
+
} catch (error) {
|
|
7541
|
+
// The terminal stays on the attempt, so the next pass retries it and the
|
|
7542
|
+
// parked attempt can still commit the same verdict.
|
|
7543
|
+
outcome.settlementFailed += 1;
|
|
7544
|
+
logFailure(error, false);
|
|
7545
|
+
}
|
|
7546
|
+
};
|
|
7547
|
+
|
|
7548
|
+
const inspectStaleRunner = async (
|
|
7549
|
+
candidate: PostgresSchedulerStaleDetachedRunner,
|
|
7550
|
+
): Promise<void> => {
|
|
7551
|
+
const provider = candidate.provider;
|
|
7552
|
+
const recheckLater = () =>
|
|
7553
|
+
state.nextCheckAtMs.set(
|
|
7554
|
+
`${candidate.runId}:${candidate.attempt}`,
|
|
7555
|
+
now() + ABSURD_DETACHED_RUNNER_LIVENESS_RECHECK_MS,
|
|
7556
|
+
);
|
|
7557
|
+
const suspension = parseOverdueDetachedRunnerSuspension(
|
|
7558
|
+
candidate.suspension,
|
|
7559
|
+
);
|
|
7560
|
+
if (!suspension) {
|
|
7561
|
+
outcome.invalidSuspensions += 1;
|
|
7562
|
+
recheckLater();
|
|
7563
|
+
console.warn(
|
|
7564
|
+
'[absurd-runtime.worker] detached_runner_liveness_invalid_suspension',
|
|
7565
|
+
{ runId: candidate.runId, attempt: candidate.attempt },
|
|
7566
|
+
);
|
|
7567
|
+
return;
|
|
7568
|
+
}
|
|
7569
|
+
if (
|
|
7570
|
+
state.abandonedReads[provider] >=
|
|
7571
|
+
ABSURD_DETACHED_RUNNER_LIVENESS_MAX_ABANDONED_READS
|
|
7572
|
+
) {
|
|
7573
|
+
outcome.unreadable += 1;
|
|
7574
|
+
return;
|
|
7575
|
+
}
|
|
7576
|
+
outcome.inspected += 1;
|
|
7577
|
+
state.readsInFlight[provider] += 1;
|
|
7578
|
+
let slotHeld = true;
|
|
7579
|
+
let abandoned = false;
|
|
7580
|
+
const releaseSlot = () => {
|
|
7581
|
+
if (!slotHeld) return;
|
|
7582
|
+
slotHeld = false;
|
|
7583
|
+
state.readsInFlight[provider] -= 1;
|
|
7584
|
+
};
|
|
7585
|
+
const read = Promise.resolve()
|
|
7586
|
+
.then(() => inspect(suspension))
|
|
7587
|
+
.catch((error: unknown) => {
|
|
7588
|
+
console.warn(
|
|
7589
|
+
'[absurd-runtime.worker] detached_runner_liveness_evidence_failed',
|
|
7590
|
+
JSON.stringify({
|
|
7591
|
+
runId: candidate.runId,
|
|
7592
|
+
sandboxId: suspension.sandboxId,
|
|
7593
|
+
error: error instanceof Error ? error.message : String(error),
|
|
7594
|
+
}),
|
|
7595
|
+
);
|
|
7596
|
+
return null;
|
|
7597
|
+
})
|
|
7598
|
+
.finally(() => {
|
|
7599
|
+
if (abandoned) state.abandonedReads[provider] -= 1;
|
|
7600
|
+
releaseSlot();
|
|
7601
|
+
});
|
|
7602
|
+
void readDeadline().then(() => {
|
|
7603
|
+
if (!slotHeld) return;
|
|
7604
|
+
abandoned = true;
|
|
7605
|
+
state.abandonedReads[provider] += 1;
|
|
7606
|
+
releaseSlot();
|
|
7607
|
+
console.warn(
|
|
7608
|
+
'[absurd-runtime.worker] detached_runner_liveness_read_abandoned',
|
|
7609
|
+
JSON.stringify({
|
|
7610
|
+
runId: candidate.runId,
|
|
7611
|
+
sandboxId: suspension.sandboxId,
|
|
7612
|
+
provider,
|
|
7613
|
+
abandonedReads: state.abandonedReads[provider],
|
|
7614
|
+
}),
|
|
7615
|
+
);
|
|
7616
|
+
});
|
|
7617
|
+
const inspection = await Promise.race([
|
|
7618
|
+
read,
|
|
7619
|
+
budgetElapsed.then(() => null),
|
|
7620
|
+
]);
|
|
7621
|
+
recheckLater();
|
|
7622
|
+
if (
|
|
7623
|
+
inspection &&
|
|
7624
|
+
!DETACHED_RUNNER_UNANSWERED_STAGES.has(inspection.diagnosis?.stage ?? '')
|
|
7625
|
+
) {
|
|
7626
|
+
answeredReads[provider] += 1;
|
|
7627
|
+
} else {
|
|
7628
|
+
outcome.unreadable += 1;
|
|
7629
|
+
}
|
|
7630
|
+
if (!inspection || !detachedRunnerInspectionProvesGone(inspection)) {
|
|
7631
|
+
outcome.unproven += 1;
|
|
7632
|
+
console.warn(
|
|
7633
|
+
'[absurd-runtime.worker] detached_runner_liveness_unproven',
|
|
7634
|
+
JSON.stringify({
|
|
7635
|
+
runId: candidate.runId,
|
|
7636
|
+
runnerAttempt: candidate.attempt,
|
|
7637
|
+
sandboxId: suspension.sandboxId,
|
|
7638
|
+
diagnosis: inspection?.diagnosis?.stage ?? 'unavailable',
|
|
7639
|
+
}),
|
|
7640
|
+
);
|
|
7641
|
+
return;
|
|
7642
|
+
}
|
|
7643
|
+
outcome.proven += 1;
|
|
7644
|
+
if (input.mode === 'observe') {
|
|
7645
|
+
outcome.wouldSettle += 1;
|
|
7646
|
+
console.info(
|
|
7647
|
+
'[absurd-runtime.worker] detached_runner_liveness_would_settle',
|
|
7648
|
+
JSON.stringify({
|
|
7649
|
+
runId: candidate.runId,
|
|
7650
|
+
runnerAttempt: candidate.attempt,
|
|
7651
|
+
sandboxId: suspension.sandboxId,
|
|
7652
|
+
diagnosis: inspection.diagnosis?.stage ?? 'unknown',
|
|
7653
|
+
}),
|
|
7654
|
+
);
|
|
7655
|
+
return;
|
|
7656
|
+
}
|
|
7657
|
+
const observedAtMs = now();
|
|
7658
|
+
const heartbeatAtMs = candidate.heartbeatAtMs ?? Number.NaN;
|
|
7659
|
+
const heartbeatAgeMs = Number.isFinite(heartbeatAtMs)
|
|
7660
|
+
? Math.max(0, observedAtMs - heartbeatAtMs)
|
|
7661
|
+
: null;
|
|
7662
|
+
const verdict: PlayRunnerResult =
|
|
7663
|
+
inspection.result && inspection.result.status !== 'suspended'
|
|
7664
|
+
? inspection.result
|
|
7665
|
+
: detachedRunnerLostResult({
|
|
7666
|
+
suspension,
|
|
7667
|
+
observedAtMs,
|
|
7668
|
+
livenessStatus: 'waiting',
|
|
7669
|
+
heartbeatAtMs,
|
|
7670
|
+
heartbeatAgeMs,
|
|
7671
|
+
inspectionAttempted: true,
|
|
7672
|
+
inspection,
|
|
7673
|
+
inspectionFailed: false,
|
|
7674
|
+
});
|
|
7675
|
+
const logVerdictFailure = (error: unknown, afterPass: boolean) =>
|
|
7676
|
+
console.error(
|
|
7677
|
+
'[absurd-runtime.worker] detached_runner_liveness_verdict_failed',
|
|
7678
|
+
{
|
|
7679
|
+
runId: candidate.runId,
|
|
7680
|
+
attempt: candidate.attempt,
|
|
7681
|
+
afterPass,
|
|
7682
|
+
error: error instanceof Error ? error.message : String(error),
|
|
7683
|
+
},
|
|
7684
|
+
);
|
|
7685
|
+
const reportExpired = () => {
|
|
7686
|
+
if (verdict.status !== 'failed' || verdict === inspection.result) return;
|
|
7687
|
+
reportDetachedRunnerLivenessExpired({
|
|
7688
|
+
runId: candidate.runId,
|
|
7689
|
+
suspension,
|
|
7690
|
+
observedAtMs,
|
|
7691
|
+
liveness: { heartbeatAt: candidate.heartbeatAt, status: 'waiting' },
|
|
7692
|
+
heartbeatAgeMs,
|
|
7693
|
+
});
|
|
7694
|
+
};
|
|
7695
|
+
const write = input.withClient((client) =>
|
|
7696
|
+
recordPostgresSchedulerDetachedRunnerLivenessVerdict(
|
|
7697
|
+
client,
|
|
7698
|
+
{
|
|
7699
|
+
runId: candidate.runId,
|
|
7700
|
+
attempt: candidate.attempt,
|
|
7701
|
+
leaseToken: candidate.leaseToken,
|
|
7702
|
+
observedHeartbeatAt: candidate.heartbeatAt,
|
|
7703
|
+
// Fallback can leave `runtimeBackend` naming a provider that never
|
|
7704
|
+
// ran this sandbox, so record the one that did.
|
|
7705
|
+
result: {
|
|
7706
|
+
...verdict,
|
|
7707
|
+
stats: { ...verdict.stats, sandboxProvider: provider },
|
|
7708
|
+
},
|
|
7709
|
+
},
|
|
7710
|
+
options,
|
|
7711
|
+
),
|
|
7712
|
+
);
|
|
7713
|
+
try {
|
|
7714
|
+
const recorded = await Promise.race([
|
|
7715
|
+
write,
|
|
7716
|
+
budgetElapsed.then(() => 'budget_spent' as const),
|
|
7717
|
+
]);
|
|
7718
|
+
if (recorded === 'budget_spent') {
|
|
7719
|
+
// If it commits, the next pass settles it like any recorded terminal.
|
|
7720
|
+
// The scheduler pool's acquire and query timeouts end it otherwise.
|
|
7721
|
+
console.warn(
|
|
7722
|
+
'[absurd-runtime.worker] detached_runner_liveness_verdict_overran',
|
|
7723
|
+
{ runId: candidate.runId, attempt: candidate.attempt },
|
|
7724
|
+
);
|
|
7725
|
+
void write
|
|
7726
|
+
.then((late) => {
|
|
7727
|
+
if (late) reportExpired();
|
|
7728
|
+
})
|
|
7729
|
+
.catch((error: unknown) => logVerdictFailure(error, true));
|
|
7730
|
+
return;
|
|
7731
|
+
}
|
|
7732
|
+
if (!recorded) {
|
|
7733
|
+
outcome.fenced += 1;
|
|
7734
|
+
return;
|
|
7735
|
+
}
|
|
7736
|
+
} catch (error) {
|
|
7737
|
+
outcome.settlementFailed += 1;
|
|
7738
|
+
logVerdictFailure(error, false);
|
|
7739
|
+
return;
|
|
7740
|
+
}
|
|
7741
|
+
reportExpired();
|
|
7742
|
+
await settleTerminal(candidate);
|
|
7743
|
+
};
|
|
7744
|
+
|
|
7745
|
+
try {
|
|
7746
|
+
await Promise.all([
|
|
7747
|
+
input.mode === 'enforce'
|
|
7748
|
+
? runDetachedRunnerLivenessLane({
|
|
7749
|
+
items: work.terminals,
|
|
7750
|
+
concurrency: 1,
|
|
7751
|
+
active,
|
|
7752
|
+
visit: settleTerminal,
|
|
7753
|
+
}).then((visited) => {
|
|
7754
|
+
outcome.recorded = visited;
|
|
7755
|
+
})
|
|
7756
|
+
: null,
|
|
7757
|
+
...ABSURD_DETACHED_RUNNER_LIVENESS_PROVIDERS.map(async (provider) => {
|
|
7758
|
+
const page = work.stale.filter(
|
|
7759
|
+
(candidate) => candidate.provider === provider,
|
|
7760
|
+
);
|
|
7761
|
+
const due = page.filter(
|
|
7762
|
+
(candidate) =>
|
|
7763
|
+
!state.nextCheckAtMs.has(`${candidate.runId}:${candidate.attempt}`),
|
|
7764
|
+
);
|
|
7765
|
+
outcome.notDue += page.length - due.length;
|
|
7766
|
+
const visited = await runDetachedRunnerLivenessLane({
|
|
7767
|
+
items: due,
|
|
7768
|
+
concurrency: Math.max(0, concurrency - state.readsInFlight[provider]),
|
|
7769
|
+
active,
|
|
7770
|
+
visit: inspectStaleRunner,
|
|
7771
|
+
});
|
|
7772
|
+
if (due.length > 0 && answeredReads[provider] === 0) {
|
|
7773
|
+
outcome.blindProviders.push(provider);
|
|
7774
|
+
}
|
|
7775
|
+
// Walk the set rather than rereading its head, so a stale set larger
|
|
7776
|
+
// than a page still reaches every sandbox. A short page wraps.
|
|
7777
|
+
if (visited < due.length) {
|
|
7778
|
+
if (visited > 0) {
|
|
7779
|
+
state.afterRunIds[provider] = due[visited - 1]!.runId;
|
|
7780
|
+
}
|
|
7781
|
+
} else {
|
|
7782
|
+
state.afterRunIds[provider] =
|
|
7783
|
+
page.length < limit ? null : (page.at(-1)?.runId ?? null);
|
|
7784
|
+
}
|
|
7785
|
+
}),
|
|
7786
|
+
]);
|
|
7787
|
+
} finally {
|
|
7788
|
+
if (budgetTimer) clearTimeout(budgetTimer);
|
|
7789
|
+
}
|
|
7790
|
+
outcome.budgetExhausted = now() >= deadlineAtMs;
|
|
7791
|
+
return outcome;
|
|
7792
|
+
}
|
|
7793
|
+
|
|
6738
7794
|
/**
|
|
6739
7795
|
* Non-terminal task states on a lane queue's `absurd.t_<queue>` table. A queue
|
|
6740
7796
|
* with zero of these has no runnable/parked work left — the lane is drainable.
|
|
@@ -7006,12 +8062,16 @@ export async function runAbsurdLaneAdoptionSupervisor(input: {
|
|
|
7006
8062
|
startAdoptionWorker: (queue: string) => Promise<AbsurdAdoptedLaneWorker>;
|
|
7007
8063
|
intervalMs?: number;
|
|
7008
8064
|
maxQueues?: number;
|
|
7009
|
-
sleep?: (ms: number) => Promise<void>;
|
|
8065
|
+
sleep?: (ms: number, signal?: AbortSignal) => Promise<void>;
|
|
8066
|
+
signal?: AbortSignal;
|
|
7010
8067
|
shouldStop?: () => boolean;
|
|
7011
8068
|
onAttached?: (queues: readonly string[]) => void;
|
|
7012
8069
|
onError?: (error: unknown) => void;
|
|
7013
8070
|
}): Promise<void> {
|
|
7014
|
-
const wait =
|
|
8071
|
+
const wait =
|
|
8072
|
+
input.sleep ??
|
|
8073
|
+
((ms: number, signal?: AbortSignal) => sleep(ms, undefined, { signal }));
|
|
8074
|
+
const stopped = () => input.signal?.aborted || input.shouldStop?.();
|
|
7015
8075
|
const intervalMs =
|
|
7016
8076
|
input.intervalMs ?? ABSURD_LANE_ADOPTION_DISCOVERY_INTERVAL_MS;
|
|
7017
8077
|
const maxQueues = input.maxQueues ?? ABSURD_LANE_ADOPTION_MAX_QUEUES;
|
|
@@ -7045,9 +8105,10 @@ export async function runAbsurdLaneAdoptionSupervisor(input: {
|
|
|
7045
8105
|
};
|
|
7046
8106
|
try {
|
|
7047
8107
|
for (;;) {
|
|
7048
|
-
if (
|
|
8108
|
+
if (stopped()) return;
|
|
7049
8109
|
try {
|
|
7050
8110
|
const adoptableReleaseIds = await input.discoverAdoptableReleaseIds();
|
|
8111
|
+
if (stopped()) return;
|
|
7051
8112
|
const allQueues = absurdAdoptionQueuesForReleases({
|
|
7052
8113
|
workerReleaseId: input.workerReleaseId,
|
|
7053
8114
|
adoptableReleaseIds,
|
|
@@ -7076,10 +8137,12 @@ export async function runAbsurdLaneAdoptionSupervisor(input: {
|
|
|
7076
8137
|
console.info('[absurd-runtime.worker] adoption_detached', { queue });
|
|
7077
8138
|
}
|
|
7078
8139
|
for (const queue of wanted) {
|
|
8140
|
+
if (stopped()) return;
|
|
7079
8141
|
if (attached.has(queue) || draining.has(queue)) continue;
|
|
7080
8142
|
if (attached.size + draining.size >= maxQueues) break;
|
|
7081
8143
|
const worker = await input.startAdoptionWorker(queue);
|
|
7082
8144
|
attached.set(queue, worker);
|
|
8145
|
+
if (stopped()) return;
|
|
7083
8146
|
console.info('[absurd-runtime.worker] adoption_attached', {
|
|
7084
8147
|
queue,
|
|
7085
8148
|
workerReleaseId: input.workerReleaseId,
|
|
@@ -7105,8 +8168,18 @@ export async function runAbsurdLaneAdoptionSupervisor(input: {
|
|
|
7105
8168
|
error: error instanceof Error ? error.message : String(error),
|
|
7106
8169
|
});
|
|
7107
8170
|
}
|
|
7108
|
-
if (
|
|
7109
|
-
|
|
8171
|
+
if (stopped()) return;
|
|
8172
|
+
try {
|
|
8173
|
+
await wait(intervalMs, input.signal);
|
|
8174
|
+
} catch (error) {
|
|
8175
|
+
if (
|
|
8176
|
+
input.signal?.aborted &&
|
|
8177
|
+
error instanceof Error &&
|
|
8178
|
+
error.name === 'AbortError'
|
|
8179
|
+
)
|
|
8180
|
+
return;
|
|
8181
|
+
throw error;
|
|
8182
|
+
}
|
|
7110
8183
|
}
|
|
7111
8184
|
} finally {
|
|
7112
8185
|
await closeAll();
|
|
@@ -7680,6 +8753,9 @@ async function processPostgresSchedulerSandboxCleanupJob(input: {
|
|
|
7680
8753
|
cleanupAttempt: job.attemptCount,
|
|
7681
8754
|
failureCount: job.failureCount,
|
|
7682
8755
|
} as const;
|
|
8756
|
+
const deleteProof: {
|
|
8757
|
+
acceptance: PostgresSchedulerSandboxDeleteAcceptance | null;
|
|
8758
|
+
} = { acceptance: null };
|
|
7683
8759
|
const cleanup = createSandboxCleanupJob({
|
|
7684
8760
|
store: createPostgresSandboxCleanupStore({
|
|
7685
8761
|
runWithClient: input.runWithClient,
|
|
@@ -7692,7 +8768,27 @@ async function processPostgresSchedulerSandboxCleanupJob(input: {
|
|
|
7692
8768
|
timeoutMs,
|
|
7693
8769
|
}): Promise<SandboxCleanupDeleteOutcome> => {
|
|
7694
8770
|
try {
|
|
7695
|
-
|
|
8771
|
+
// Only a job claimed before can have an earlier accepted delete. A
|
|
8772
|
+
// failed read retries, so a 404 is never judged without this proof.
|
|
8773
|
+
deleteProof.acceptance =
|
|
8774
|
+
resource.provider === 'daytona' && resource.cleanupAttempt > 1
|
|
8775
|
+
? await input.runWithClient((client) =>
|
|
8776
|
+
readPostgresSchedulerSandboxDeleteAcceptance(
|
|
8777
|
+
client,
|
|
8778
|
+
{
|
|
8779
|
+
provider: resource.provider,
|
|
8780
|
+
sandboxId: resource.sandboxId,
|
|
8781
|
+
routingDomain: resource.routingDomain,
|
|
8782
|
+
runId: resource.runId,
|
|
8783
|
+
attempt: resource.attempt,
|
|
8784
|
+
},
|
|
8785
|
+
input.schedulerOptions,
|
|
8786
|
+
),
|
|
8787
|
+
)
|
|
8788
|
+
: null;
|
|
8789
|
+
if (!deleteProof.acceptance) {
|
|
8790
|
+
await recordCleanupEvent('provider_delete_started', { timeoutMs });
|
|
8791
|
+
}
|
|
7696
8792
|
const providerOutcome = await deleteRuntimeSandboxBounded({
|
|
7697
8793
|
deleteRuntimeSandbox: async (sandboxId) => {
|
|
7698
8794
|
if (resource.provider === 'modal') {
|
|
@@ -7728,6 +8824,7 @@ async function processPostgresSchedulerSandboxCleanupJob(input: {
|
|
|
7728
8824
|
return await input.deleteRuntimeSandboxWithOutcome(
|
|
7729
8825
|
sandboxId,
|
|
7730
8826
|
resource.routingDomain,
|
|
8827
|
+
deleteProof.acceptance,
|
|
7731
8828
|
);
|
|
7732
8829
|
}
|
|
7733
8830
|
return (await input.deleteRuntimeSandbox(sandboxId))
|
|
@@ -7754,6 +8851,24 @@ async function processPostgresSchedulerSandboxCleanupJob(input: {
|
|
|
7754
8851
|
routingDomain: providerOutcome.organizationId,
|
|
7755
8852
|
};
|
|
7756
8853
|
}
|
|
8854
|
+
if (providerOutcome.kind === 'delete_pending') {
|
|
8855
|
+
// The first acceptance is durable proof for the read-back, and the
|
|
8856
|
+
// moment billing keeps as this sandbox's end.
|
|
8857
|
+
if (providerOutcome.deleteAccepted && !deleteProof.acceptance) {
|
|
8858
|
+
await recordCleanupEvent('provider_delete_accepted', {
|
|
8859
|
+
routingDomain: providerOutcome.organizationId,
|
|
8860
|
+
});
|
|
8861
|
+
} else {
|
|
8862
|
+
await recordCleanupEvent('provider_delete_pending', {
|
|
8863
|
+
code: providerOutcome.code,
|
|
8864
|
+
});
|
|
8865
|
+
}
|
|
8866
|
+
return {
|
|
8867
|
+
kind: 'pending',
|
|
8868
|
+
code: providerOutcome.code,
|
|
8869
|
+
detail: `${providerOutcome.code}: ${providerOutcome.detail}`,
|
|
8870
|
+
};
|
|
8871
|
+
}
|
|
7757
8872
|
await recordCleanupEvent('provider_delete_failed', {
|
|
7758
8873
|
code:
|
|
7759
8874
|
'code' in providerOutcome
|
|
@@ -7815,6 +8930,15 @@ async function processPostgresSchedulerSandboxCleanupJob(input: {
|
|
|
7815
8930
|
cleanupAuthority: 'durable_cleanup_claim',
|
|
7816
8931
|
providerOutcome: outcome.providerOutcome,
|
|
7817
8932
|
completionOutcome: outcome.completion,
|
|
8933
|
+
// An upper bound: the 404 was seen on a paced read, not as it happened.
|
|
8934
|
+
...(deleteProof.acceptance
|
|
8935
|
+
? {
|
|
8936
|
+
disappearedAfterMs: Math.max(
|
|
8937
|
+
0,
|
|
8938
|
+
Date.now() - deleteProof.acceptance.acceptedAtMs,
|
|
8939
|
+
),
|
|
8940
|
+
}
|
|
8941
|
+
: {}),
|
|
7818
8942
|
});
|
|
7819
8943
|
return { deleted: 1, retried: 0 };
|
|
7820
8944
|
}
|
|
@@ -8000,7 +9124,7 @@ export async function reconcilePostgresSchedulerAmbiguousModalCreatesOnce(input:
|
|
|
8000
9124
|
return { observed: intents.length, resourcesRecorded, resolvedAbsent };
|
|
8001
9125
|
}
|
|
8002
9126
|
|
|
8003
|
-
async function reconcileReservedDaytonaCancellationOnce(input: {
|
|
9127
|
+
export async function reconcileReservedDaytonaCancellationOnce(input: {
|
|
8004
9128
|
runWithClient: AbsurdRuntimeWorkerDeps['runWithClient'];
|
|
8005
9129
|
environment: 'preview' | 'production';
|
|
8006
9130
|
schedulerOptions: Pick<PostgresSchedulerOptions, 'schema'>;
|
|
@@ -8037,7 +9161,11 @@ async function reconcileReservedDaytonaCancellationOnce(input: {
|
|
|
8037
9161
|
await input.runWithClient((client) =>
|
|
8038
9162
|
abandonRuntimeSandboxOperationReservation(
|
|
8039
9163
|
client,
|
|
8040
|
-
{
|
|
9164
|
+
{
|
|
9165
|
+
operationKey: operation.operationKey,
|
|
9166
|
+
// Execution recovery ending is not proof a sent create cannot land.
|
|
9167
|
+
onlyWhileRunActive: true,
|
|
9168
|
+
},
|
|
8041
9169
|
input.schedulerOptions,
|
|
8042
9170
|
),
|
|
8043
9171
|
);
|
|
@@ -8045,27 +9173,75 @@ async function reconcileReservedDaytonaCancellationOnce(input: {
|
|
|
8045
9173
|
} finally {
|
|
8046
9174
|
control.dispose();
|
|
8047
9175
|
}
|
|
8048
|
-
|
|
9176
|
+
await persistReservedDaytonaCancellationCleanupIntent({
|
|
9177
|
+
operation,
|
|
9178
|
+
sandbox,
|
|
9179
|
+
configuredOrganizationId: process.env.DAYTONA_ORGANIZATION_ID?.trim(),
|
|
9180
|
+
recordCleanupIntent: (identity) =>
|
|
9181
|
+
input.runWithClient((client) =>
|
|
9182
|
+
recordDaytonaSandboxCleanupIntent(
|
|
9183
|
+
client,
|
|
9184
|
+
identity,
|
|
9185
|
+
input.schedulerOptions,
|
|
9186
|
+
),
|
|
9187
|
+
),
|
|
9188
|
+
});
|
|
9189
|
+
}
|
|
9190
|
+
|
|
9191
|
+
export async function persistReservedDaytonaCancellationCleanupIntent(input: {
|
|
9192
|
+
operation: {
|
|
9193
|
+
operationKey: string;
|
|
9194
|
+
providerOperationToken: string;
|
|
9195
|
+
orgId: string;
|
|
9196
|
+
runId: string;
|
|
9197
|
+
};
|
|
9198
|
+
sandbox: {
|
|
9199
|
+
id: string;
|
|
9200
|
+
name?: string | null;
|
|
9201
|
+
organizationId?: string | null;
|
|
9202
|
+
labels?: Record<string, string | undefined> | null;
|
|
9203
|
+
};
|
|
9204
|
+
configuredOrganizationId?: string | null;
|
|
9205
|
+
recordCleanupIntent: (identity: {
|
|
9206
|
+
operationKey: string;
|
|
9207
|
+
providerOperationToken: string;
|
|
9208
|
+
orgId: string;
|
|
9209
|
+
runId: string;
|
|
9210
|
+
sandboxId: string;
|
|
9211
|
+
routingDomain: string;
|
|
9212
|
+
}) => Promise<boolean>;
|
|
9213
|
+
}): Promise<void> {
|
|
9214
|
+
const sandboxName = `dl-${input.operation.providerOperationToken}`;
|
|
9215
|
+
if (
|
|
9216
|
+
!daytonaSandboxMatchesOperationIdentity({
|
|
9217
|
+
sandbox: input.sandbox,
|
|
9218
|
+
sandboxName,
|
|
9219
|
+
providerOperationToken: input.operation.providerOperationToken,
|
|
9220
|
+
orgId: input.operation.orgId,
|
|
9221
|
+
runId: input.operation.runId,
|
|
9222
|
+
configuredOrganizationId: input.configuredOrganizationId,
|
|
9223
|
+
})
|
|
9224
|
+
) {
|
|
8049
9225
|
throw new Error(
|
|
8050
|
-
`Daytona cancellation lookup ${sandboxName}
|
|
9226
|
+
`Daytona cancellation lookup ${sandboxName} has a mismatched sandbox identity or organization routing; cleanup suppressed.`,
|
|
8051
9227
|
);
|
|
8052
9228
|
}
|
|
8053
|
-
|
|
9229
|
+
const routingDomain = input.sandbox.organizationId?.trim();
|
|
9230
|
+
if (!routingDomain) {
|
|
8054
9231
|
throw new Error(
|
|
8055
|
-
`Daytona cancellation lookup ${sandboxName} lacks its routing organization.`,
|
|
9232
|
+
`Daytona cancellation lookup ${sandboxName} lacks its routing organization; cleanup suppressed.`,
|
|
9233
|
+
);
|
|
9234
|
+
}
|
|
9235
|
+
const recorded = await input.recordCleanupIntent({
|
|
9236
|
+
...input.operation,
|
|
9237
|
+
sandboxId: input.sandbox.id,
|
|
9238
|
+
routingDomain,
|
|
9239
|
+
});
|
|
9240
|
+
if (!recorded) {
|
|
9241
|
+
throw new Error(
|
|
9242
|
+
`Daytona cancellation cleanup intent for ${sandboxName} was not durably recorded.`,
|
|
8056
9243
|
);
|
|
8057
9244
|
}
|
|
8058
|
-
await input.runWithClient((client) =>
|
|
8059
|
-
recordRecoveredDaytonaCancellationSandbox(
|
|
8060
|
-
client,
|
|
8061
|
-
{
|
|
8062
|
-
operationKey: operation.operationKey,
|
|
8063
|
-
sandboxId: sandbox.id,
|
|
8064
|
-
routingDomain: sandbox.organizationId,
|
|
8065
|
-
},
|
|
8066
|
-
input.schedulerOptions,
|
|
8067
|
-
),
|
|
8068
|
-
);
|
|
8069
9245
|
}
|
|
8070
9246
|
|
|
8071
9247
|
export async function runPostgresSchedulerSandboxCleanupController(input: {
|
|
@@ -8773,7 +9949,7 @@ export async function startRuntimeWorkerHost(
|
|
|
8773
9949
|
// claiming and still hold parked durable waits, so a retired lane does not
|
|
8774
9950
|
// have to stay alive until its last sleeping run wakes. A draining lane
|
|
8775
9951
|
// attaches nothing — it is on its way out itself.
|
|
8776
|
-
|
|
9952
|
+
const adoptionStop = new AbortController();
|
|
8777
9953
|
let adoptionSupervisor: Promise<void> | null = null;
|
|
8778
9954
|
if (!drainExit) {
|
|
8779
9955
|
const releaseEnvironment =
|
|
@@ -8782,7 +9958,7 @@ export async function startRuntimeWorkerHost(
|
|
|
8782
9958
|
: ('production' as const);
|
|
8783
9959
|
adoptionSupervisor = runAbsurdLaneAdoptionSupervisor({
|
|
8784
9960
|
workerReleaseId: releaseId,
|
|
8785
|
-
|
|
9961
|
+
signal: adoptionStop.signal,
|
|
8786
9962
|
discoverAdoptableReleaseIds: async () => {
|
|
8787
9963
|
const lanes = await withRuntimeSchedulerClient(poolOptions, (client) =>
|
|
8788
9964
|
listPostgresSchedulerAdoptableReleaseLanes(
|
|
@@ -8903,7 +10079,7 @@ export async function startRuntimeWorkerHost(
|
|
|
8903
10079
|
drainStopped = true;
|
|
8904
10080
|
billingStopped = true;
|
|
8905
10081
|
cleanupStopped = true;
|
|
8906
|
-
|
|
10082
|
+
adoptionStop.abort();
|
|
8907
10083
|
overdueCeilingStopped = true;
|
|
8908
10084
|
},
|
|
8909
10085
|
controllers: [
|