@vercel/factory 0.0.14 → 0.0.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +356 -0
- package/README.md +96 -2
- package/dist/agent-routes.d.mts +113 -0
- package/dist/agent-routes.mjs +91 -0
- package/dist/agent-routes.mjs.map +1 -0
- package/dist/api-contracts.d.mts +1768 -0
- package/dist/api-contracts.mjs +119 -0
- package/dist/api-contracts.mjs.map +1 -0
- package/dist/api-images.d.mts +30 -0
- package/dist/api-images.mjs +28 -0
- package/dist/api-images.mjs.map +1 -0
- package/dist/api-task-graph.mjs +57 -0
- package/dist/api-task-graph.mjs.map +1 -0
- package/dist/api.d.mts +111 -216
- package/dist/api.mjs +1063 -99
- package/dist/api.mjs.map +1 -1
- package/dist/approval-access.d.mts +39 -0
- package/dist/approval-access.mjs +62 -0
- package/dist/approval-access.mjs.map +1 -0
- package/dist/approval-contracts.d.mts +698 -0
- package/dist/approval-contracts.mjs +27 -0
- package/dist/approval-contracts.mjs.map +1 -0
- package/dist/approval.d.mts +49 -0
- package/dist/approval.mjs +39 -0
- package/dist/approval.mjs.map +1 -0
- package/dist/blob/artifacts.d.mts +37 -0
- package/dist/blob/artifacts.mjs +45 -0
- package/dist/blob/artifacts.mjs.map +1 -0
- package/dist/blob/index.d.mts +84 -12
- package/dist/blob/index.mjs +179 -45
- package/dist/blob/index.mjs.map +1 -1
- package/dist/budget.d.mts +115 -0
- package/dist/budget.mjs +147 -0
- package/dist/budget.mjs.map +1 -0
- package/dist/budgets.d.mts +4 -0
- package/dist/budgets.mjs +4 -0
- package/dist/build-factory.d.mts +7 -4
- package/dist/build-factory.mjs +2 -1
- package/dist/build-factory.mjs.map +1 -1
- package/dist/change-verification/brief.d.mts +9 -0
- package/dist/change-verification/brief.mjs +13 -0
- package/dist/change-verification/brief.mjs.map +1 -0
- package/dist/change-verification/dispatch.d.mts +37 -0
- package/dist/change-verification/dispatch.mjs +83 -0
- package/dist/change-verification/dispatch.mjs.map +1 -0
- package/dist/change-verification/eve-tool.d.mts +61 -0
- package/dist/change-verification/eve-tool.mjs +89 -0
- package/dist/change-verification/eve-tool.mjs.map +1 -0
- package/dist/change-verification/result.mjs +130 -0
- package/dist/change-verification/result.mjs.map +1 -0
- package/dist/changes/eve-record-change.d.mts +56 -0
- package/dist/changes/eve-record-change.mjs +131 -0
- package/dist/changes/eve-record-change.mjs.map +1 -0
- package/dist/changes/presentation.d.mts +14 -0
- package/dist/changes.d.mts +24 -0
- package/dist/changes.mjs +29 -0
- package/dist/changes.mjs.map +1 -0
- package/dist/client-events.d.mts +25 -0
- package/dist/client-events.mjs +45 -0
- package/dist/client-events.mjs.map +1 -0
- package/dist/client-stream.d.mts +28 -0
- package/dist/client-stream.mjs +210 -0
- package/dist/client-stream.mjs.map +1 -0
- package/dist/client-transcript.d.mts +41 -0
- package/dist/client-transcript.mjs +233 -0
- package/dist/client-transcript.mjs.map +1 -0
- package/dist/client.d.mts +343 -0
- package/dist/client.mjs +366 -0
- package/dist/client.mjs.map +1 -0
- package/dist/code-review/contracts.d.mts +27 -0
- package/dist/code-review/eve-post-review.d.mts +70 -0
- package/dist/code-review/eve-post-review.mjs +195 -0
- package/dist/code-review/eve-post-review.mjs.map +1 -0
- package/dist/code-review/eve-review-comments.d.mts +75 -0
- package/dist/code-review/eve-review-comments.mjs +193 -0
- package/dist/code-review/eve-review-comments.mjs.map +1 -0
- package/dist/code-review/github-contracts.d.mts +10 -0
- package/dist/code-review/github-reporter.d.mts +46 -0
- package/dist/code-review/github-reporter.mjs +262 -0
- package/dist/code-review/github-reporter.mjs.map +1 -0
- package/dist/code-review/preparation.d.mts +34 -0
- package/dist/code-review/preparation.mjs +23 -0
- package/dist/code-review/preparation.mjs.map +1 -0
- package/dist/code-review/stall.d.mts +17 -0
- package/dist/code-review/stall.mjs +30 -0
- package/dist/code-review/stall.mjs.map +1 -0
- package/dist/code-review.d.mts +7 -0
- package/dist/code-review.mjs +5 -0
- package/dist/communication.d.mts +9 -0
- package/dist/communication.mjs +9 -0
- package/dist/connectors/github.d.mts +26 -0
- package/dist/connectors/github.mjs +55 -0
- package/dist/connectors/github.mjs.map +1 -0
- package/dist/connectors/index.d.mts +35 -3
- package/dist/connectors/index.mjs +3 -52
- package/dist/connectors/index.mjs.map +1 -1
- package/dist/connectors/outbox.d.mts +34 -0
- package/dist/connectors/outbox.mjs +50 -0
- package/dist/connectors/outbox.mjs.map +1 -0
- package/dist/connectors/slack-approval-blocks.mjs +136 -0
- package/dist/connectors/slack-approval-blocks.mjs.map +1 -0
- package/dist/connectors/slack-approvals.d.mts +29 -0
- package/dist/connectors/slack-approvals.mjs +346 -0
- package/dist/connectors/slack-approvals.mjs.map +1 -0
- package/dist/connectors/slack.d.mts +35 -52
- package/dist/connectors/slack.mjs +13 -85
- package/dist/connectors/slack.mjs.map +1 -1
- package/dist/deepsec/eve-tool.d.mts +110 -0
- package/dist/deepsec/eve-tool.mjs +166 -0
- package/dist/deepsec/eve-tool.mjs.map +1 -0
- package/dist/deepsec/finding.d.mts +84 -0
- package/dist/deepsec/finding.mjs +56 -0
- package/dist/deepsec/finding.mjs.map +1 -0
- package/dist/deepsec.d.mts +3 -0
- package/dist/delivery-metrics/metrics.d.mts +93 -0
- package/dist/delivery-metrics/metrics.mjs +200 -0
- package/dist/delivery-metrics/metrics.mjs.map +1 -0
- package/dist/delivery-metrics.d.mts +2 -0
- package/dist/delivery-metrics.mjs +2 -0
- package/dist/dispatch.d.mts +152 -0
- package/dist/dispatch.mjs +199 -0
- package/dist/dispatch.mjs.map +1 -0
- package/dist/errors.d.mts +15 -0
- package/dist/errors.mjs +38 -0
- package/dist/errors.mjs.map +1 -0
- package/dist/eve/index.d.mts +209 -1277
- package/dist/eve/index.mjs +312 -1073
- package/dist/eve/index.mjs.map +1 -1
- package/dist/eve/invoke.d.mts +144 -0
- package/dist/eve/invoke.mjs +254 -0
- package/dist/eve/invoke.mjs.map +1 -0
- package/dist/eve/session-client.d.mts +186 -0
- package/dist/eve/session-client.mjs +245 -0
- package/dist/eve/session-client.mjs.map +1 -0
- package/dist/eve/task-execution.d.mts +380 -0
- package/dist/eve/task-execution.mjs +73 -0
- package/dist/eve/task-execution.mjs.map +1 -0
- package/dist/eve/task-session.d.mts +129 -0
- package/dist/eve/task-session.mjs +245 -0
- package/dist/eve/task-session.mjs.map +1 -0
- package/dist/eve/transcript.d.mts +37 -0
- package/dist/eve/transcript.mjs +69 -0
- package/dist/eve/transcript.mjs.map +1 -0
- package/dist/execution-public.d.mts +8 -0
- package/dist/execution-public.mjs +7 -0
- package/dist/execution.d.mts +183 -0
- package/dist/execution.mjs +223 -0
- package/dist/execution.mjs.map +1 -0
- package/dist/finding-remediation/admission.d.mts +53 -0
- package/dist/finding-remediation/admission.mjs +90 -0
- package/dist/finding-remediation/admission.mjs.map +1 -0
- package/dist/finding-remediation/brief.d.mts +14 -0
- package/dist/finding-remediation/brief.mjs +25 -0
- package/dist/finding-remediation/brief.mjs.map +1 -0
- package/dist/finding-remediation/eve-tool.d.mts +29 -0
- package/dist/finding-remediation/eve-tool.mjs +34 -0
- package/dist/finding-remediation/eve-tool.mjs.map +1 -0
- package/dist/findings/fingerprint.d.mts +14 -0
- package/dist/findings/fingerprint.mjs +28 -0
- package/dist/findings/fingerprint.mjs.map +1 -0
- package/dist/findings.d.mts +7 -0
- package/dist/findings.mjs +6 -0
- package/dist/github-app.d.mts +54 -0
- package/dist/github-app.mjs +121 -0
- package/dist/github-app.mjs.map +1 -0
- package/dist/github-publication.d.mts +199 -0
- package/dist/github-publication.mjs +442 -0
- package/dist/github-publication.mjs.map +1 -0
- package/dist/github-transfer.d.mts +81 -0
- package/dist/github-transfer.mjs +273 -0
- package/dist/github-transfer.mjs.map +1 -0
- package/dist/github.d.mts +446 -2
- package/dist/github.mjs +357 -56
- package/dist/github.mjs.map +1 -1
- package/dist/image-input.d.mts +51 -0
- package/dist/image-input.mjs +63 -0
- package/dist/image-input.mjs.map +1 -0
- package/dist/inbox-activity.d.mts +53 -0
- package/dist/inbox-activity.mjs +41 -0
- package/dist/inbox-activity.mjs.map +1 -0
- package/dist/index.d.mts +16 -484
- package/dist/index.mjs +15 -522
- package/dist/intake-contracts.d.mts +55 -0
- package/dist/intake-contracts.mjs +33 -0
- package/dist/intake-contracts.mjs.map +1 -0
- package/dist/integrations/deepsec.d.mts +5 -0
- package/dist/integrations/deepsec.mjs +3 -0
- package/dist/integrations/github.d.mts +11 -0
- package/dist/integrations/github.mjs +9 -0
- package/dist/integrations/slack.d.mts +5 -0
- package/dist/integrations/slack.mjs +5 -0
- package/dist/integrations/vercel.d.mts +5 -0
- package/dist/integrations/vercel.mjs +4 -0
- package/dist/merge-resolution/contracts.d.mts +17 -0
- package/dist/merge-resolution/contracts.mjs +19 -0
- package/dist/merge-resolution/contracts.mjs.map +1 -0
- package/dist/merge-resolution/eve-tools.d.mts +47 -0
- package/dist/merge-resolution/eve-tools.mjs +122 -0
- package/dist/merge-resolution/eve-tools.mjs.map +1 -0
- package/dist/model-settings.d.mts +41 -0
- package/dist/model-settings.mjs +35 -0
- package/dist/model-settings.mjs.map +1 -0
- package/dist/multi-repository-changes/coordination.d.mts +49 -0
- package/dist/multi-repository-changes/coordination.mjs +115 -0
- package/dist/multi-repository-changes/coordination.mjs.map +1 -0
- package/dist/planning/cancellation.mjs +82 -0
- package/dist/planning/cancellation.mjs.map +1 -0
- package/dist/planning/configuration.d.mts +23 -0
- package/dist/planning/configuration.mjs +56 -0
- package/dist/planning/configuration.mjs.map +1 -0
- package/dist/planning/contracts.d.mts +136 -0
- package/dist/planning/contracts.mjs +42 -0
- package/dist/planning/contracts.mjs.map +1 -0
- package/dist/planning/dependencies.mjs +44 -0
- package/dist/planning/dependencies.mjs.map +1 -0
- package/dist/planning/effects.mjs +122 -0
- package/dist/planning/effects.mjs.map +1 -0
- package/dist/planning/reconcile.mjs +279 -0
- package/dist/planning/reconcile.mjs.map +1 -0
- package/dist/planning/runtime.d.mts +27 -0
- package/dist/planning/runtime.mjs +25 -0
- package/dist/planning/runtime.mjs.map +1 -0
- package/dist/planning/state.mjs +92 -0
- package/dist/planning/state.mjs.map +1 -0
- package/dist/planning/turns.mjs +262 -0
- package/dist/planning/turns.mjs.map +1 -0
- package/dist/planning/validation.mjs +106 -0
- package/dist/planning/validation.mjs.map +1 -0
- package/dist/postgres/index.d.mts +48 -1
- package/dist/postgres/index.mjs +45 -3
- package/dist/postgres/index.mjs.map +1 -1
- package/dist/presets/software-development/approval.d.mts +14 -0
- package/dist/presets/software-development/approval.mjs +21 -0
- package/dist/presets/software-development/approval.mjs.map +1 -0
- package/dist/presets/software-development/deepsec-brief.d.mts +8 -0
- package/dist/presets/software-development/deepsec-brief.mjs +14 -0
- package/dist/presets/software-development/deepsec-brief.mjs.map +1 -0
- package/dist/presets/software-development/dispatch.d.mts +56 -0
- package/dist/presets/software-development/dispatch.mjs +67 -0
- package/dist/presets/software-development/dispatch.mjs.map +1 -0
- package/dist/presets/software-development/eve-delegation-policy.d.mts +7 -0
- package/dist/presets/software-development/eve-delegation-policy.mjs +34 -0
- package/dist/presets/software-development/eve-delegation-policy.mjs.map +1 -0
- package/dist/presets/software-development/eve.d.mts +18 -0
- package/dist/presets/software-development/eve.mjs +19 -0
- package/dist/presets/software-development/eve.mjs.map +1 -0
- package/dist/presets/software-development/finish-task-policy.d.mts +7 -0
- package/dist/presets/software-development/finish-task-policy.mjs +9 -0
- package/dist/presets/software-development/finish-task-policy.mjs.map +1 -0
- package/dist/presets/software-development/recovery.d.mts +32 -0
- package/dist/presets/software-development/recovery.mjs +120 -0
- package/dist/presets/software-development/recovery.mjs.map +1 -0
- package/dist/presets/software-development/task-communication.d.mts +24 -0
- package/dist/presets/software-development/task-communication.mjs +157 -0
- package/dist/presets/software-development/task-communication.mjs.map +1 -0
- package/dist/presets/software-development.d.mts +10 -0
- package/dist/presets/software-development.mjs +9 -0
- package/dist/pull-requests/contracts.d.mts +72 -0
- package/dist/pull-requests/eve-fetch.d.mts +57 -0
- package/dist/pull-requests/eve-fetch.mjs +151 -0
- package/dist/pull-requests/eve-fetch.mjs.map +1 -0
- package/dist/pull-requests/github-label-manager.d.mts +27 -0
- package/dist/pull-requests/github-label-manager.mjs +40 -0
- package/dist/pull-requests/github-label-manager.mjs.map +1 -0
- package/dist/pull-requests/github-publisher.d.mts +67 -0
- package/dist/pull-requests/github-publisher.mjs +405 -0
- package/dist/pull-requests/github-publisher.mjs.map +1 -0
- package/dist/pull-requests.d.mts +6 -0
- package/dist/pull-requests.mjs +4 -0
- package/dist/receipts.d.mts +4 -0
- package/dist/receipts.mjs +3 -0
- package/dist/repositories.d.mts +18 -0
- package/dist/repositories.mjs +33 -0
- package/dist/repositories.mjs.map +1 -0
- package/dist/sandbox/command.d.mts +18 -0
- package/dist/sandbox/execution-abort.d.mts +16 -0
- package/dist/sandbox/execution-abort.mjs +49 -0
- package/dist/sandbox/execution-abort.mjs.map +1 -0
- package/dist/sandbox/execution-environment.d.mts +25 -0
- package/dist/sandbox/execution-run-ledger.d.mts +115 -0
- package/dist/sandbox/execution-run-ledger.mjs +209 -0
- package/dist/sandbox/execution-run-ledger.mjs.map +1 -0
- package/dist/sandbox/execution.d.mts +54 -0
- package/dist/sandbox/execution.mjs +228 -0
- package/dist/sandbox/execution.mjs.map +1 -0
- package/dist/sandbox/harness-runner.d.mts +79 -0
- package/dist/sandbox/harness-runner.mjs +162 -0
- package/dist/sandbox/harness-runner.mjs.map +1 -0
- package/dist/sandbox/index.d.mts +10 -0
- package/dist/sandbox/index.mjs +7 -0
- package/dist/sandbox/task-environment.d.mts +50 -0
- package/dist/sandbox/task-environment.mjs +140 -0
- package/dist/sandbox/task-environment.mjs.map +1 -0
- package/dist/sandbox/workspace.d.mts +29 -0
- package/dist/sandbox/workspace.mjs +28 -0
- package/dist/sandbox/workspace.mjs.map +1 -0
- package/dist/schema/agent-route.d.mts +40 -0
- package/dist/schema/agent-route.mjs +33 -0
- package/dist/schema/agent-route.mjs.map +1 -0
- package/dist/schema/answer.d.mts +23 -0
- package/dist/schema/answer.mjs +21 -0
- package/dist/schema/answer.mjs.map +1 -0
- package/dist/schema/budget.d.mts +38 -0
- package/dist/schema/budget.mjs +30 -0
- package/dist/schema/budget.mjs.map +1 -0
- package/dist/schema/catalog.mjs +72 -0
- package/dist/schema/catalog.mjs.map +1 -0
- package/dist/schema/change.d.mts +117 -0
- package/dist/schema/change.mjs +68 -0
- package/dist/schema/change.mjs.map +1 -0
- package/dist/schema/common.d.mts +33 -0
- package/dist/{id-BLIkCVue.mjs → schema/common.mjs} +7 -16
- package/dist/schema/common.mjs.map +1 -0
- package/dist/schema/communication.d.mts +52 -0
- package/dist/{communication-Kd9nU5RP.mjs → schema/communication.mjs} +8 -3
- package/dist/schema/communication.mjs.map +1 -0
- package/dist/schema/execution.d.mts +101 -0
- package/dist/schema/execution.mjs +86 -0
- package/dist/schema/execution.mjs.map +1 -0
- package/dist/schema/factory-config.d.mts +122 -0
- package/dist/schema/factory-config.mjs +90 -0
- package/dist/schema/factory-config.mjs.map +1 -0
- package/dist/schema/finding.d.mts +125 -0
- package/dist/schema/finding.mjs +67 -0
- package/dist/schema/finding.mjs.map +1 -0
- package/dist/schema/id.d.mts +43 -0
- package/dist/schema/id.mjs +32 -0
- package/dist/schema/id.mjs.map +1 -0
- package/dist/schema/outbox.d.mts +240 -0
- package/dist/schema/outbox.mjs +53 -0
- package/dist/schema/outbox.mjs.map +1 -0
- package/dist/schema/question-route.mjs +16 -0
- package/dist/schema/question-route.mjs.map +1 -0
- package/dist/schema/receipt.d.mts +81 -0
- package/dist/schema/receipt.mjs +94 -0
- package/dist/schema/receipt.mjs.map +1 -0
- package/dist/schema/repository.d.mts +30 -0
- package/dist/schema/repository.mjs +29 -0
- package/dist/schema/repository.mjs.map +1 -0
- package/dist/schema/session.d.mts +136 -0
- package/dist/schema/session.mjs +54 -0
- package/dist/schema/session.mjs.map +1 -0
- package/dist/schema/signal.d.mts +105 -0
- package/dist/schema/signal.mjs +81 -0
- package/dist/schema/signal.mjs.map +1 -0
- package/dist/schema/slack-pr-notifications.d.mts +12 -0
- package/dist/schema/slack-pr-notifications.mjs +11 -0
- package/dist/schema/slack-pr-notifications.mjs.map +1 -0
- package/dist/schema/stored-session.mjs +10 -0
- package/dist/schema/stored-session.mjs.map +1 -0
- package/dist/schema/stored-task.mjs +20 -0
- package/dist/schema/stored-task.mjs.map +1 -0
- package/dist/schema/task-effect.d.mts +39 -0
- package/dist/schema/task-effect.mjs +29 -0
- package/dist/schema/task-effect.mjs.map +1 -0
- package/dist/schema/task-graph.d.mts +3173 -0
- package/dist/schema/task-graph.mjs +97 -0
- package/dist/schema/task-graph.mjs.map +1 -0
- package/dist/schema/task-message.d.mts +254 -0
- package/dist/schema/task-message.mjs +189 -0
- package/dist/schema/task-message.mjs.map +1 -0
- package/dist/schema/task.d.mts +273 -0
- package/dist/schema/task.mjs +200 -0
- package/dist/schema/task.mjs.map +1 -0
- package/dist/schema/transcript.d.mts +176 -0
- package/dist/schema/transcript.mjs +87 -0
- package/dist/schema/transcript.mjs.map +1 -0
- package/dist/schema/transitions.d.mts +50 -0
- package/dist/schema/transitions.mjs +303 -0
- package/dist/schema/transitions.mjs.map +1 -0
- package/dist/schema/work.d.mts +150 -0
- package/dist/schema/work.mjs +102 -0
- package/dist/schema/work.mjs.map +1 -0
- package/dist/session-previews.d.mts +76 -0
- package/dist/session-previews.mjs +55 -0
- package/dist/session-previews.mjs.map +1 -0
- package/dist/session-review.d.mts +120 -0
- package/dist/session-review.mjs +79 -0
- package/dist/session-review.mjs.map +1 -0
- package/dist/signal-triage.d.mts +20 -0
- package/dist/signal-triage.mjs +85 -0
- package/dist/signal-triage.mjs.map +1 -0
- package/dist/signals.d.mts +20 -0
- package/dist/signals.mjs +19 -0
- package/dist/signals.mjs.map +1 -0
- package/dist/stall.d.mts +41 -0
- package/dist/stall.mjs +83 -0
- package/dist/stall.mjs.map +1 -0
- package/dist/storage.d.mts +4 -0
- package/dist/storage.mjs +4 -0
- package/dist/store/driver.d.mts +65 -0
- package/dist/store/driver.mjs +42 -0
- package/dist/store/driver.mjs.map +1 -0
- package/dist/store/engine.d.mts +683 -0
- package/dist/store/engine.mjs +2309 -0
- package/dist/store/engine.mjs.map +1 -0
- package/dist/store/memory.d.mts +24 -0
- package/dist/store/memory.mjs +69 -0
- package/dist/store/memory.mjs.map +1 -0
- package/dist/store/slack-pr-notifications.d.mts +44 -0
- package/dist/store/slack-pr-notifications.mjs +121 -0
- package/dist/store/slack-pr-notifications.mjs.map +1 -0
- package/dist/store/task-effects.d.mts +38 -0
- package/dist/store/task-effects.mjs +118 -0
- package/dist/store/task-effects.mjs.map +1 -0
- package/dist/store/task-graphs.d.mts +60 -0
- package/dist/store/task-graphs.mjs +212 -0
- package/dist/store/task-graphs.mjs.map +1 -0
- package/dist/store/task-messages.d.mts +136 -0
- package/dist/store/task-messages.mjs +475 -0
- package/dist/store/task-messages.mjs.map +1 -0
- package/dist/store/task-work.d.mts +180 -0
- package/dist/store/task-work.mjs +137 -0
- package/dist/store/task-work.mjs.map +1 -0
- package/dist/store/work-notifications.d.mts +19 -0
- package/dist/store/work-notifications.mjs +48 -0
- package/dist/store/work-notifications.mjs.map +1 -0
- package/dist/sweep.d.mts +79 -0
- package/dist/sweep.mjs +171 -0
- package/dist/sweep.mjs.map +1 -0
- package/dist/task-communication.d.mts +35 -0
- package/dist/task-communication.mjs +47 -0
- package/dist/task-communication.mjs.map +1 -0
- package/dist/task-graph-view.d.mts +267 -0
- package/dist/task-graph-view.mjs +17 -0
- package/dist/task-graph-view.mjs.map +1 -0
- package/dist/task-message-processing.d.mts +60 -0
- package/dist/task-message-processing.mjs +200 -0
- package/dist/task-message-processing.mjs.map +1 -0
- package/dist/task-protocols.d.mts +80 -0
- package/dist/task-protocols.mjs +47 -0
- package/dist/task-protocols.mjs.map +1 -0
- package/dist/tasks.d.mts +17 -0
- package/dist/tasks.mjs +17 -0
- package/dist/vercel-git.d.mts +53 -0
- package/dist/vercel-git.mjs +344 -0
- package/dist/vercel-git.mjs.map +1 -0
- package/dist/vercel-github-api.d.mts +103 -0
- package/dist/vercel-github-api.mjs +363 -0
- package/dist/vercel-github-api.mjs.map +1 -0
- package/dist/vercel.d.mts +34 -2
- package/dist/vercel.mjs +49 -3
- package/dist/vercel.mjs.map +1 -1
- package/dist/work-triage/eve-tool.d.mts +29 -0
- package/dist/work-triage/eve-tool.mjs +32 -0
- package/dist/work-triage/eve-tool.mjs.map +1 -0
- package/dist/work-triage/find-existing-work.d.mts +32 -0
- package/dist/work-triage/find-existing-work.mjs +31 -0
- package/dist/work-triage/find-existing-work.mjs.map +1 -0
- package/dist/work-triage/github.d.mts +15 -0
- package/dist/work-triage/github.mjs +14 -0
- package/dist/work-triage/github.mjs.map +1 -0
- package/dist/work-triage.d.mts +4 -0
- package/dist/work-triage.mjs +3 -0
- package/dist/workflows.d.mts +146 -0
- package/dist/workflows.mjs +139 -0
- package/dist/workflows.mjs.map +1 -0
- package/dist/workspace-diff-assembly.d.mts +33 -0
- package/dist/workspace-diff-assembly.mjs +95 -0
- package/dist/workspace-diff-assembly.mjs.map +1 -0
- package/dist/workspace-diff.d.mts +90 -0
- package/dist/workspace-diff.mjs +61 -0
- package/dist/workspace-diff.mjs.map +1 -0
- package/dist/workspace-files-git.d.mts +15 -0
- package/dist/workspace-files-git.mjs +61 -0
- package/dist/workspace-files-git.mjs.map +1 -0
- package/dist/workspace-files.d.mts +107 -0
- package/dist/workspace-files.mjs +74 -0
- package/dist/workspace-files.mjs.map +1 -0
- package/dist/workspace-snapshot.d.mts +26 -0
- package/dist/workspace-snapshot.mjs +80 -0
- package/dist/workspace-snapshot.mjs.map +1 -0
- package/docs/getting-started.md +104 -0
- package/docs/index.md +100 -0
- package/docs/recipes/cancellation.md +215 -0
- package/docs/recipes/custom-workflow.md +153 -0
- package/docs/recipes/dependent-tasks.md +207 -0
- package/docs/recipes/eve-agent.md +277 -0
- package/docs/recipes/human-input.md +204 -0
- package/docs/recipes/persistence-recovery.md +268 -0
- package/docs/recipes/retry-recovery.md +241 -0
- package/docs/recipes/task-messaging.md +215 -0
- package/docs/recipes/typed-eve-result.md +161 -0
- package/docs/runtime-integration.md +137 -0
- package/package.json +142 -46
- package/dist/approval-JX2Px9IC.mjs +0 -132
- package/dist/approval-JX2Px9IC.mjs.map +0 -1
- package/dist/budget-DzSiIA4t.mjs +0 -42
- package/dist/budget-DzSiIA4t.mjs.map +0 -1
- package/dist/change-D2U6v7Ek.mjs +0 -554
- package/dist/change-D2U6v7Ek.mjs.map +0 -1
- package/dist/communication-Kd9nU5RP.mjs.map +0 -1
- package/dist/coordination-Cq08hb1l.mjs +0 -76
- package/dist/coordination-Cq08hb1l.mjs.map +0 -1
- package/dist/dispatch-BaZS3PT8.d.mts +0 -76
- package/dist/driver-BC0Mt5KG.d.mts +0 -34
- package/dist/driver-Bbd25rNM.mjs +0 -23
- package/dist/driver-Bbd25rNM.mjs.map +0 -1
- package/dist/engine-DFHU2Pe-.d.mts +0 -781
- package/dist/engine-WaUM2cGA.mjs +0 -1068
- package/dist/engine-WaUM2cGA.mjs.map +0 -1
- package/dist/factory-config-BoRXOw43.mjs +0 -50
- package/dist/factory-config-BoRXOw43.mjs.map +0 -1
- package/dist/factory-config-D2yU3G0c.d.mts +0 -58
- package/dist/github-C5HNBN6p.d.mts +0 -279
- package/dist/id-BLIkCVue.mjs.map +0 -1
- package/dist/index-Bw6hggoH.d.mts +0 -40
- package/dist/index.mjs.map +0 -1
- package/dist/task-CcOyOJ3k.mjs +0 -154
- package/dist/task-CcOyOJ3k.mjs.map +0 -1
package/dist/blob/index.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { RecordExistsError, RecordNotFoundError, TransientStoreError, VersionConflictError } from "../store/driver.mjs";
|
|
2
|
+
import { artifactReference, createArtifactStore, parseArtifactRef } from "./artifacts.mjs";
|
|
2
3
|
import { get, list, put } from "@vercel/blob";
|
|
3
4
|
//#region src/blob/index.ts
|
|
4
5
|
function isNotFoundError(error) {
|
|
@@ -8,10 +9,49 @@ function isAlreadyExistsError(error) {
|
|
|
8
9
|
return error instanceof Error && /already exists|not allowed to overwrite|overwrite/iu.test(error.message);
|
|
9
10
|
}
|
|
10
11
|
function isPreconditionError(error) {
|
|
11
|
-
return error instanceof Error && (error.name === "BlobPreconditionFailedError" || /precondition|etag|412/iu.test(error.message));
|
|
12
|
+
return error instanceof Error && (error.name === "BlobPreconditionFailedError" || /precondition|etag|412/iu.test(error.message) || error.message === "Vercel Blob: The conditional request cannot succeed due to a conflicting operation against this resource.");
|
|
12
13
|
}
|
|
14
|
+
const TRANSIENT_NETWORK_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
15
|
+
"ECONNRESET",
|
|
16
|
+
"ECONNREFUSED",
|
|
17
|
+
"ETIMEDOUT",
|
|
18
|
+
"ENOTFOUND",
|
|
19
|
+
"EAI_AGAIN",
|
|
20
|
+
"EPIPE",
|
|
21
|
+
"EHOSTUNREACH",
|
|
22
|
+
"ENETUNREACH",
|
|
23
|
+
"UND_ERR_CONNECT_TIMEOUT",
|
|
24
|
+
"UND_ERR_SOCKET",
|
|
25
|
+
"UND_ERR_HEADERS_TIMEOUT"
|
|
26
|
+
]);
|
|
27
|
+
function isTransientBlobError(error) {
|
|
28
|
+
if (!(error instanceof Error)) return false;
|
|
29
|
+
if (isAlreadyExistsError(error) || isPreconditionError(error) || isNotFoundError(error)) return false;
|
|
30
|
+
const code = error.code;
|
|
31
|
+
if (code !== void 0 && TRANSIENT_NETWORK_ERROR_CODES.has(code)) return true;
|
|
32
|
+
if (error.name === "AbortError") return true;
|
|
33
|
+
if (/fetch failed|network|socket hang up|timed? ?out|ECONNRESET|ECONNREFUSED|ETIMEDOUT|ENOTFOUND|EAI_AGAIN/iu.test(error.message)) return true;
|
|
34
|
+
return /(?:^|[^0-9])(?:429|5\d{2})(?:[^0-9]|$)/u.test(error.message);
|
|
35
|
+
}
|
|
36
|
+
function jsonValuesEqual(a, b) {
|
|
37
|
+
if (a === b) return true;
|
|
38
|
+
if (typeof a !== typeof b) return false;
|
|
39
|
+
if (a === null || b === null || typeof a !== "object" || typeof b !== "object") return false;
|
|
40
|
+
if (Array.isArray(a) || Array.isArray(b)) {
|
|
41
|
+
if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false;
|
|
42
|
+
return a.every((item, index) => jsonValuesEqual(item, b[index]));
|
|
43
|
+
}
|
|
44
|
+
const aKeys = Object.keys(a);
|
|
45
|
+
const bKeys = Object.keys(b);
|
|
46
|
+
if (aKeys.length !== bKeys.length) return false;
|
|
47
|
+
return aKeys.every((key) => Object.hasOwn(b, key) && jsonValuesEqual(a[key], b[key]));
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Default private Vercel Blob client used by the durable Factory store driver.
|
|
51
|
+
* Reads explicitly bypass the CDN cache, and writes resolve only after the SDK accepts them.
|
|
52
|
+
*/
|
|
13
53
|
const vercelBlobClient = {
|
|
14
|
-
async read(pathname) {
|
|
54
|
+
async read({ pathname }) {
|
|
15
55
|
let result;
|
|
16
56
|
try {
|
|
17
57
|
result = await get(pathname, {
|
|
@@ -28,15 +68,15 @@ const vercelBlobClient = {
|
|
|
28
68
|
etag: result.blob.etag
|
|
29
69
|
};
|
|
30
70
|
},
|
|
31
|
-
async write(pathname, body, overwrite, ifMatch) {
|
|
32
|
-
await put(pathname, body, {
|
|
71
|
+
async write({ pathname, body, overwrite, ifMatch }) {
|
|
72
|
+
await put(pathname, body === "" ? Buffer.alloc(0) : body, {
|
|
33
73
|
access: "private",
|
|
34
74
|
allowOverwrite: overwrite,
|
|
35
75
|
contentType: "application/json",
|
|
36
|
-
...ifMatch === void 0 ? {} : { ifMatch }
|
|
76
|
+
...ifMatch === void 0 ? {} : { ifMatch: ifMatch.replace(/^W\//u, "") }
|
|
37
77
|
});
|
|
38
78
|
},
|
|
39
|
-
async listPaths(prefix) {
|
|
79
|
+
async listPaths({ prefix }) {
|
|
40
80
|
const paths = [];
|
|
41
81
|
let cursor;
|
|
42
82
|
do {
|
|
@@ -52,24 +92,71 @@ const vercelBlobClient = {
|
|
|
52
92
|
}
|
|
53
93
|
};
|
|
54
94
|
/**
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
* overwriting the winner.
|
|
95
|
+
* Creates a durable Vercel Blob driver with atomic compare-and-swap updates.
|
|
96
|
+
*
|
|
97
|
+
* @remarks
|
|
98
|
+
* Inserts are create-if-absent. Updates bypass Blob's CDN cache and use the current ETag, so a
|
|
99
|
+
* writer that loses a race receives `VersionConflictError` instead of overwriting the winner.
|
|
100
|
+
* Reads and writes retry only 5xx, 429, and network failures within the configured attempt and
|
|
101
|
+
* elapsed-time bounds; exhaustion surfaces as `TransientStoreError`. If a transient write may
|
|
102
|
+
* have landed before its response was lost, the retry reads the record back and treats only the
|
|
103
|
+
* exact intended value and version as success. A different value remains a real conflict.
|
|
104
|
+
*
|
|
105
|
+
* Collection listing downloads records under the shared `readConcurrency` bound, coalesces
|
|
106
|
+
* concurrent lists of the same collection, and rejects malformed persisted values. Listing the
|
|
107
|
+
* provider's paths itself is not retried by this driver. The caller owns credentials and any
|
|
108
|
+
* injected client's lifecycle.
|
|
109
|
+
*
|
|
110
|
+
* @param options - Optional Blob transport, namespace, read limit, retry policy, and retry clock.
|
|
111
|
+
* @returns A `blob` driver suitable for `createStores`.
|
|
112
|
+
* @throws When concurrency or retry bounds are invalid.
|
|
60
113
|
*/
|
|
61
114
|
function createBlobDriver(options = {}) {
|
|
62
115
|
const client = options.client ?? vercelBlobClient;
|
|
116
|
+
const now = options.now ?? Date.now;
|
|
63
117
|
const prefix = options.prefix ?? "factory";
|
|
64
118
|
const readConcurrency = options.readConcurrency ?? 32;
|
|
65
119
|
if (!Number.isInteger(readConcurrency) || readConcurrency < 1) throw new Error("Blob read concurrency must be a positive integer");
|
|
120
|
+
const retry = {
|
|
121
|
+
attempts: options.retry?.attempts ?? 3,
|
|
122
|
+
baseDelayMs: options.retry?.baseDelayMs ?? 250,
|
|
123
|
+
maxTotalMs: options.retry?.maxTotalMs ?? 5e3
|
|
124
|
+
};
|
|
125
|
+
if (!Number.isInteger(retry.attempts) || retry.attempts < 1) throw new Error("Blob retry attempts must be a positive integer");
|
|
126
|
+
if (!Number.isFinite(retry.baseDelayMs) || retry.baseDelayMs < 0) throw new Error("Blob retry base delay must be a finite non-negative number");
|
|
127
|
+
if (!Number.isFinite(retry.maxTotalMs) || retry.maxTotalMs < 0) throw new Error("Blob retry elapsed-time limit must be a finite non-negative number");
|
|
128
|
+
let activeReads = 0;
|
|
129
|
+
const readWaiters = [];
|
|
130
|
+
const activeLists = /* @__PURE__ */ new Map();
|
|
131
|
+
async function withTransientRetry(collection, id, operation) {
|
|
132
|
+
const start = now();
|
|
133
|
+
for (let attempt = 0;; attempt += 1) try {
|
|
134
|
+
return await operation();
|
|
135
|
+
} catch (error) {
|
|
136
|
+
if (!isTransientBlobError(error)) throw error;
|
|
137
|
+
const nextDelayMs = retry.baseDelayMs * 2 ** attempt;
|
|
138
|
+
if (attempt >= retry.attempts - 1 || now() - start + nextDelayMs > retry.maxTotalMs) throw new TransientStoreError(collection, id, error);
|
|
139
|
+
await new Promise((resolve) => setTimeout(resolve, nextDelayMs));
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
async function limitedRead(pathname) {
|
|
143
|
+
if (activeReads < readConcurrency) activeReads += 1;
|
|
144
|
+
else await new Promise((resolve) => readWaiters.push(resolve));
|
|
145
|
+
try {
|
|
146
|
+
return await client.read({ pathname });
|
|
147
|
+
} finally {
|
|
148
|
+
const waiter = readWaiters.shift();
|
|
149
|
+
if (waiter === void 0) activeReads -= 1;
|
|
150
|
+
else waiter();
|
|
151
|
+
}
|
|
152
|
+
}
|
|
66
153
|
function pathFor(collection, id) {
|
|
67
154
|
return `${prefix}/${collection}/${id}.json`;
|
|
68
155
|
}
|
|
69
156
|
async function readRecord(collection, id) {
|
|
70
157
|
let object;
|
|
71
158
|
try {
|
|
72
|
-
object = await
|
|
159
|
+
object = await withTransientRetry(collection, id, () => limitedRead(pathFor(collection, id)));
|
|
73
160
|
} catch (error) {
|
|
74
161
|
if (isNotFoundError(error)) return null;
|
|
75
162
|
throw error;
|
|
@@ -83,6 +170,14 @@ function createBlobDriver(options = {}) {
|
|
|
83
170
|
etag: object.etag
|
|
84
171
|
};
|
|
85
172
|
}
|
|
173
|
+
async function wroteThisInsert(collection, id, value) {
|
|
174
|
+
const after = await readRecord(collection, id);
|
|
175
|
+
return after !== null && after.version === 1 && jsonValuesEqual(after.value, value);
|
|
176
|
+
}
|
|
177
|
+
async function wroteThisUpdate(collection, id, value, expectedNewVersion) {
|
|
178
|
+
const after = await readRecord(collection, id);
|
|
179
|
+
return after !== null && after.version === expectedNewVersion && jsonValuesEqual(after.value, value);
|
|
180
|
+
}
|
|
86
181
|
return {
|
|
87
182
|
name: "blob",
|
|
88
183
|
async get(collection, id) {
|
|
@@ -92,56 +187,95 @@ function createBlobDriver(options = {}) {
|
|
|
92
187
|
version: record.version
|
|
93
188
|
};
|
|
94
189
|
},
|
|
95
|
-
async insert(collection, id, value) {
|
|
190
|
+
async insert({ collection, id, value }) {
|
|
191
|
+
let attempts = 0;
|
|
96
192
|
try {
|
|
97
|
-
await
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
193
|
+
await withTransientRetry(collection, id, () => {
|
|
194
|
+
attempts += 1;
|
|
195
|
+
return client.write({
|
|
196
|
+
pathname: pathFor(collection, id),
|
|
197
|
+
body: JSON.stringify({
|
|
198
|
+
value,
|
|
199
|
+
version: 1
|
|
200
|
+
}),
|
|
201
|
+
overwrite: false
|
|
202
|
+
});
|
|
203
|
+
});
|
|
101
204
|
} catch (error) {
|
|
102
|
-
if (isAlreadyExistsError(error))
|
|
205
|
+
if (isAlreadyExistsError(error)) {
|
|
206
|
+
if (attempts > 1 && await wroteThisInsert(collection, id, value)) return;
|
|
207
|
+
throw new RecordExistsError(collection, id);
|
|
208
|
+
}
|
|
103
209
|
throw error;
|
|
104
210
|
}
|
|
105
211
|
},
|
|
106
|
-
async update(collection, id, value, expectedVersion) {
|
|
212
|
+
async update({ collection, id, value, expectedVersion }) {
|
|
107
213
|
const existing = await readRecord(collection, id);
|
|
108
214
|
if (existing === null) throw new RecordNotFoundError(collection, id);
|
|
109
215
|
if (existing.version !== expectedVersion) throw new VersionConflictError(collection, id, expectedVersion);
|
|
216
|
+
let attempts = 0;
|
|
110
217
|
try {
|
|
111
|
-
await
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
218
|
+
await withTransientRetry(collection, id, () => {
|
|
219
|
+
attempts += 1;
|
|
220
|
+
return client.write({
|
|
221
|
+
pathname: pathFor(collection, id),
|
|
222
|
+
body: JSON.stringify({
|
|
223
|
+
value,
|
|
224
|
+
version: expectedVersion + 1
|
|
225
|
+
}),
|
|
226
|
+
overwrite: true,
|
|
227
|
+
ifMatch: existing.etag.replace(/^W\//u, "")
|
|
228
|
+
});
|
|
229
|
+
});
|
|
115
230
|
} catch (error) {
|
|
116
|
-
if (isPreconditionError(error))
|
|
231
|
+
if (isPreconditionError(error)) {
|
|
232
|
+
if (attempts > 1 && await wroteThisUpdate(collection, id, value, expectedVersion + 1)) return;
|
|
233
|
+
throw new VersionConflictError(collection, id, expectedVersion);
|
|
234
|
+
}
|
|
117
235
|
throw error;
|
|
118
236
|
}
|
|
119
237
|
},
|
|
120
238
|
async list(collection) {
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
239
|
+
const active = activeLists.get(collection);
|
|
240
|
+
if (active !== void 0) return active;
|
|
241
|
+
const listing = (async () => {
|
|
242
|
+
const dir = `${prefix}/${collection}/`;
|
|
243
|
+
const paths = await client.listPaths({ prefix: dir });
|
|
244
|
+
const entries = new Array(paths.length);
|
|
245
|
+
let next = 0;
|
|
246
|
+
let failure;
|
|
247
|
+
const workers = Array.from({ length: Math.min(readConcurrency, paths.length) }, async () => {
|
|
248
|
+
while (next < paths.length && failure === void 0) {
|
|
249
|
+
const index = next++;
|
|
250
|
+
const id = paths[index].slice(dir.length).replace(/\.json$/u, "");
|
|
251
|
+
try {
|
|
252
|
+
const record = await readRecord(collection, id);
|
|
253
|
+
entries[index] = record === null ? null : {
|
|
254
|
+
id,
|
|
255
|
+
record: {
|
|
256
|
+
value: record.value,
|
|
257
|
+
version: record.version
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
} catch (error) {
|
|
261
|
+
failure ??= error;
|
|
135
262
|
}
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
await Promise.all(workers);
|
|
266
|
+
if (failure !== void 0) throw failure;
|
|
267
|
+
return entries.filter((entry) => entry !== null);
|
|
268
|
+
})();
|
|
269
|
+
activeLists.set(collection, listing);
|
|
270
|
+
try {
|
|
271
|
+
return await listing;
|
|
272
|
+
} finally {
|
|
273
|
+
if (activeLists.get(collection) === listing) activeLists.delete(collection);
|
|
274
|
+
}
|
|
141
275
|
}
|
|
142
276
|
};
|
|
143
277
|
}
|
|
144
278
|
//#endregion
|
|
145
|
-
export { createBlobDriver, vercelBlobClient };
|
|
279
|
+
export { artifactReference, createArtifactStore, createBlobDriver, parseArtifactRef, vercelBlobClient };
|
|
146
280
|
|
|
147
281
|
//# sourceMappingURL=index.mjs.map
|
package/dist/blob/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/blob/index.ts"],"sourcesContent":["import { get, list, put } from \"@vercel/blob\";\nimport {\n RecordExistsError,\n RecordNotFoundError,\n VersionConflictError,\n type CollectionName,\n type StoredEntry,\n type StoreDriver,\n} from \"../store/driver\";\n\nexport interface BlobObject {\n body: string;\n etag: string;\n}\n\n/** The narrow blob operations the driver needs; injectable for tests. */\nexport interface BlobClient {\n /** Must return the latest content, never a cached copy. */\n read(pathname: string): Promise<BlobObject | null>;\n /** Must reject if overwrite is false and the path exists, or if ifMatch is given and the ETag differs. */\n write(pathname: string, body: string, overwrite: boolean, ifMatch?: string): Promise<void>;\n listPaths(prefix: string): Promise<string[]>;\n}\n\n// @vercel/blob throws BlobNotFoundError with the message\n// \"Vercel Blob: The requested blob does not exist\".\nfunction isNotFoundError(error: unknown): boolean {\n return (\n error instanceof Error &&\n (error.name === \"BlobNotFoundError\" ||\n /does not exist|not.?found|404/iu.test(error.message))\n );\n}\n\nfunction isAlreadyExistsError(error: unknown): boolean {\n return (\n error instanceof Error &&\n /already exists|not allowed to overwrite|overwrite/iu.test(error.message)\n );\n}\n\n// @vercel/blob throws BlobPreconditionFailedError when ifMatch does not hold.\nfunction isPreconditionError(error: unknown): boolean {\n return (\n error instanceof Error &&\n (error.name === \"BlobPreconditionFailedError\" || /precondition|etag|412/iu.test(error.message))\n );\n}\n\nexport const vercelBlobClient: BlobClient = {\n async read(pathname) {\n let result;\n try {\n // Uncached: the ledger is read-modify-write and must see the latest version.\n result = await get(pathname, { access: \"private\", useCache: false });\n } catch (error) {\n if (isNotFoundError(error)) {\n return null;\n }\n throw error;\n }\n if (result === null || result.stream === null) {\n return null;\n }\n return { body: await new Response(result.stream).text(), etag: result.blob.etag };\n },\n\n async write(pathname, body, overwrite, ifMatch) {\n await put(pathname, body, {\n access: \"private\",\n allowOverwrite: overwrite,\n contentType: \"application/json\",\n ...(ifMatch === undefined ? {} : { ifMatch }),\n });\n },\n\n async listPaths(prefix) {\n const paths: string[] = [];\n let cursor: string | undefined;\n do {\n const page = await list({ prefix, cursor, limit: 1000 });\n paths.push(...page.blobs.map((blob) => blob.pathname));\n cursor = page.hasMore ? page.cursor : undefined;\n } while (cursor !== undefined);\n return paths;\n },\n};\n\nexport interface BlobDriverOptions {\n client?: BlobClient;\n /** Path prefix inside the blob store; defaults to \"factory\". */\n prefix?: string;\n /** Maximum Blob downloads in flight while listing a collection. Default 32. */\n readConcurrency?: number;\n}\n\n/**\n * Store driver on Vercel Blob: durable and dependency-free (one Blob store on\n * the project, no database). Inserts are create-if-absent. Updates read the latest content,\n * bypassing Blob's CDN cache, and write with an ETag precondition, so a\n * writer that lost a race gets a VersionConflictError rather than\n * overwriting the winner.\n */\nexport function createBlobDriver(options: BlobDriverOptions = {}): StoreDriver {\n const client = options.client ?? vercelBlobClient;\n const prefix = options.prefix ?? \"factory\";\n const readConcurrency = options.readConcurrency ?? 32;\n if (!Number.isInteger(readConcurrency) || readConcurrency < 1) {\n throw new Error(\"Blob read concurrency must be a positive integer\");\n }\n\n function pathFor(collection: CollectionName, id: string): string {\n return `${prefix}/${collection}/${id}.json`;\n }\n\n async function readRecord(\n collection: CollectionName,\n id: string,\n ): Promise<{ value: unknown; version: number; etag: string } | null> {\n let object: BlobObject | null;\n try {\n object = await client.read(pathFor(collection, id));\n } catch (error) {\n if (isNotFoundError(error)) {\n return null;\n }\n throw error;\n }\n if (object === null) {\n return null;\n }\n const parsed = JSON.parse(object.body) as { value: unknown; version: number };\n if (typeof parsed.version !== \"number\") {\n throw new Error(`malformed stored record at ${pathFor(collection, id)}`);\n }\n return { value: parsed.value, version: parsed.version, etag: object.etag };\n }\n\n return {\n name: \"blob\",\n\n async get(collection, id) {\n const record = await readRecord(collection, id);\n return record === null ? null : { value: record.value, version: record.version };\n },\n\n async insert(collection, id, value) {\n try {\n await client.write(\n pathFor(collection, id),\n JSON.stringify({ value, version: 1 }),\n false,\n );\n } catch (error) {\n if (isAlreadyExistsError(error)) {\n throw new RecordExistsError(collection, id);\n }\n throw error;\n }\n },\n\n async update(collection, id, value, expectedVersion) {\n const existing = await readRecord(collection, id);\n if (existing === null) {\n throw new RecordNotFoundError(collection, id);\n }\n if (existing.version !== expectedVersion) {\n throw new VersionConflictError(collection, id, expectedVersion);\n }\n try {\n await client.write(\n pathFor(collection, id),\n JSON.stringify({ value, version: expectedVersion + 1 }),\n true,\n // The write API's ifMatch rejects the weak form (W/\"…\") that reads return.\n existing.etag.replace(/^W\\//u, \"\"),\n );\n } catch (error) {\n if (isPreconditionError(error)) {\n throw new VersionConflictError(collection, id, expectedVersion);\n }\n throw error;\n }\n },\n\n async list(collection) {\n const dir = `${prefix}/${collection}/`;\n const paths = await client.listPaths(dir);\n const entries: (StoredEntry | null)[] = new Array(paths.length);\n let next = 0;\n const workers = Array.from(\n { length: Math.min(readConcurrency, paths.length) },\n async () => {\n while (next < paths.length) {\n const index = next++;\n const pathname = paths[index]!;\n const id = pathname.slice(dir.length).replace(/\\.json$/u, \"\");\n const record = await readRecord(collection, id);\n entries[index] = record === null\n ? null\n : { id, record: { value: record.value, version: record.version } };\n }\n },\n );\n await Promise.all(workers);\n return entries.filter((entry): entry is StoredEntry => entry !== null);\n },\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/blob/index.ts"],"sourcesContent":["import { get, list, put } from \"@vercel/blob\";\nexport {\n artifactReference,\n createArtifactStore,\n parseArtifactRef,\n type ArtifactRef,\n type ArtifactInput,\n type ArtifactKind,\n type ReadArtifactInput,\n type ArtifactStore,\n} from \"./artifacts\";\nimport {\n RecordExistsError,\n RecordNotFoundError,\n TransientStoreError,\n VersionConflictError,\n type CollectionName,\n type StoredEntry,\n type StoreDriver,\n} from \"../store/driver\";\nimport type { MillisecondClock } from \"../schema/common\";\n\n/** Blob content paired with the ETag required for compare-and-swap persistence. */\nexport interface BlobObject {\n /** Complete UTF-8 body returned by an uncached read. */\n body: string;\n /** Current object ETag used to fence a later conditional write. */\n etag: string;\n}\n\n/** Exact blob pathname requested from storage. */\nexport interface ReadBlobInput {\n /** Exact namespaced object path; the client must not apply a second namespace. */\n pathname: string;\n}\n\n/** Blob body and conditional-write policy used for one storage write. */\nexport interface WriteBlobInput extends ReadBlobInput {\n /** Complete replacement body to persist. */\n body: string;\n /** Whether an existing path may be replaced; `false` means create-if-absent. */\n overwrite: boolean;\n /** Optional ETag that must still match atomically when overwriting. */\n ifMatch?: string;\n}\n\n/** Path prefix used to list matching blobs. */\nexport interface ListBlobPathsInput {\n /** Path prefix whose complete matching object names are requested. */\n prefix: string;\n}\n\n/** The narrow blob operations the driver needs; injectable for tests. */\nexport interface BlobClient {\n /**\n * Returns the latest content rather than a cached copy, or `null` when absent.\n *\n * @param input - Exact object path to read.\n * @returns Current body and ETag, or `null` when the path does not exist.\n */\n read(input: ReadBlobInput): Promise<BlobObject | null>;\n /**\n * Persists one complete object and enforces its create or ETag precondition atomically.\n *\n * @param input - Exact path, body, overwrite policy, and optional ETag fence.\n * @returns After the object write is durable at the provider boundary.\n */\n write(input: WriteBlobInput): Promise<void>;\n /**\n * Lists every object path matching the prefix, following all provider pages.\n *\n * @param input - Namespace prefix to enumerate.\n * @returns Matching exact paths; callers do not depend on their order.\n */\n listPaths(input: ListBlobPathsInput): Promise<readonly string[]>;\n}\n\n// @vercel/blob throws BlobNotFoundError with the message\n// \"Vercel Blob: The requested blob does not exist\".\nfunction isNotFoundError(error: unknown): boolean {\n return (\n error instanceof Error &&\n (error.name === \"BlobNotFoundError\" || /does not exist|not.?found|404/iu.test(error.message))\n );\n}\n\nfunction isAlreadyExistsError(error: unknown): boolean {\n return (\n error instanceof Error &&\n /already exists|not allowed to overwrite|overwrite/iu.test(error.message)\n );\n}\n\n// The SDK also forwards provider conditional-operation conflicts as generic BlobErrors.\nfunction isPreconditionError(error: unknown): boolean {\n return (\n error instanceof Error &&\n (error.name === \"BlobPreconditionFailedError\" ||\n /precondition|etag|412/iu.test(error.message) ||\n error.message ===\n \"Vercel Blob: The conditional request cannot succeed due to a conflicting operation against this resource.\")\n );\n}\n\nconst TRANSIENT_NETWORK_ERROR_CODES = new Set([\n \"ECONNRESET\",\n \"ECONNREFUSED\",\n \"ETIMEDOUT\",\n \"ENOTFOUND\",\n \"EAI_AGAIN\",\n \"EPIPE\",\n \"EHOSTUNREACH\",\n \"ENETUNREACH\",\n \"UND_ERR_CONNECT_TIMEOUT\",\n \"UND_ERR_SOCKET\",\n \"UND_ERR_HEADERS_TIMEOUT\",\n]);\n\n// Blob 5xx/429s and plain network failures are transient: the same read or\n// write is worth retrying with backoff. Direct downloads bypass the SDK's\n// own request-retry policy entirely, and outages can also surface as a\n// generic HTTP error on the write path, so this checks the error's code and\n// message rather than one fixed message shape.\nfunction isTransientBlobError(error: unknown): boolean {\n if (!(error instanceof Error)) return false;\n if (isAlreadyExistsError(error) || isPreconditionError(error) || isNotFoundError(error))\n return false;\n const code = (error as NodeJS.ErrnoException).code;\n if (code !== undefined && TRANSIENT_NETWORK_ERROR_CODES.has(code)) return true;\n if (error.name === \"AbortError\") return true;\n if (\n /fetch failed|network|socket hang up|timed? ?out|ECONNRESET|ECONNREFUSED|ETIMEDOUT|ENOTFOUND|EAI_AGAIN/iu.test(\n error.message,\n )\n ) {\n return true;\n }\n // Matches an HTTP 429 or 5xx status appearing anywhere in the message,\n // whether from the direct-download fetch or a generic API error.\n return /(?:^|[^0-9])(?:429|5\\d{2})(?:[^0-9]|$)/u.test(error.message);\n}\n\n// Structural equality over JSON-serializable values: good enough to tell\n// whether a record we just read back is the exact one we tried to write,\n// without depending on key order surviving a JSON round-trip.\nfunction jsonValuesEqual(a: unknown, b: unknown): boolean {\n if (a === b) return true;\n if (typeof a !== typeof b) return false;\n if (a === null || b === null || typeof a !== \"object\" || typeof b !== \"object\") return false;\n if (Array.isArray(a) || Array.isArray(b)) {\n if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false;\n return a.every((item, index) => jsonValuesEqual(item, b[index]));\n }\n const aKeys = Object.keys(a as Record<string, unknown>);\n const bKeys = Object.keys(b as Record<string, unknown>);\n if (aKeys.length !== bKeys.length) return false;\n return aKeys.every(\n (key) =>\n Object.hasOwn(b as Record<string, unknown>, key) &&\n jsonValuesEqual((a as Record<string, unknown>)[key], (b as Record<string, unknown>)[key]),\n );\n}\n\n/**\n * Default private Vercel Blob client used by the durable Factory store driver.\n * Reads explicitly bypass the CDN cache, and writes resolve only after the SDK accepts them.\n */\nexport const vercelBlobClient: BlobClient = {\n async read({ pathname }) {\n let result;\n try {\n // Uncached: the ledger is read-modify-write and must see the latest version.\n result = await get(pathname, { access: \"private\", useCache: false });\n } catch (error) {\n if (isNotFoundError(error)) {\n return null;\n }\n throw error;\n }\n if (result === null || result.stream === null) {\n return null;\n }\n return { body: await new Response(result.stream).text(), etag: result.blob.etag };\n },\n\n async write({ pathname, body, overwrite, ifMatch }) {\n // The SDK treats an empty string as a missing body.\n await put(pathname, body === \"\" ? Buffer.alloc(0) : body, {\n access: \"private\",\n allowOverwrite: overwrite,\n contentType: \"application/json\",\n ...(ifMatch === undefined ? {} : { ifMatch: ifMatch.replace(/^W\\//u, \"\") }),\n });\n },\n\n async listPaths({ prefix }) {\n const paths: string[] = [];\n let cursor: string | undefined;\n do {\n const page = await list({ prefix, cursor, limit: 1000 });\n paths.push(...page.blobs.map((blob) => blob.pathname));\n cursor = page.hasMore ? page.cursor : undefined;\n } while (cursor !== undefined);\n return paths;\n },\n};\n\n/** Bounds retries and exponential backoff for transient Blob operations. */\nexport interface BlobRetryOptions {\n /** Positive integer attempts for one read or write, including the first. Default 3. */\n attempts?: number;\n /** Finite non-negative backoff base in milliseconds; doubles each retry. Default 250. */\n baseDelayMs?: number;\n /** Finite non-negative elapsed-time ceiling in milliseconds. Default 5000. */\n maxTotalMs?: number;\n}\n\n/** Configures the Blob client, namespace, read concurrency, and transient retries. */\nexport interface BlobDriverOptions {\n /** Blob transport used for reads, conditional writes, and listings; defaults to Vercel Blob. */\n client?: BlobClient;\n /** Path prefix inside the blob store; defaults to \"factory\". */\n prefix?: string;\n /** Maximum Blob downloads in flight while listing a collection. Default 32. */\n readConcurrency?: number;\n /** Bounds retry of transient (5xx/429/network) Blob failures on reads and writes. */\n retry?: BlobRetryOptions;\n /** Millisecond clock used to bound total retry time. */\n now?: MillisecondClock;\n}\n\n/**\n * Creates a durable Vercel Blob driver with atomic compare-and-swap updates.\n *\n * @remarks\n * Inserts are create-if-absent. Updates bypass Blob's CDN cache and use the current ETag, so a\n * writer that loses a race receives `VersionConflictError` instead of overwriting the winner.\n * Reads and writes retry only 5xx, 429, and network failures within the configured attempt and\n * elapsed-time bounds; exhaustion surfaces as `TransientStoreError`. If a transient write may\n * have landed before its response was lost, the retry reads the record back and treats only the\n * exact intended value and version as success. A different value remains a real conflict.\n *\n * Collection listing downloads records under the shared `readConcurrency` bound, coalesces\n * concurrent lists of the same collection, and rejects malformed persisted values. Listing the\n * provider's paths itself is not retried by this driver. The caller owns credentials and any\n * injected client's lifecycle.\n *\n * @param options - Optional Blob transport, namespace, read limit, retry policy, and retry clock.\n * @returns A `blob` driver suitable for `createStores`.\n * @throws When concurrency or retry bounds are invalid.\n */\nexport function createBlobDriver(options: BlobDriverOptions = {}): StoreDriver {\n const client = options.client ?? vercelBlobClient;\n const now = options.now ?? Date.now;\n const prefix = options.prefix ?? \"factory\";\n const readConcurrency = options.readConcurrency ?? 32;\n if (!Number.isInteger(readConcurrency) || readConcurrency < 1) {\n throw new Error(\"Blob read concurrency must be a positive integer\");\n }\n const retry = {\n attempts: options.retry?.attempts ?? 3,\n baseDelayMs: options.retry?.baseDelayMs ?? 250,\n maxTotalMs: options.retry?.maxTotalMs ?? 5000,\n };\n if (!Number.isInteger(retry.attempts) || retry.attempts < 1) {\n throw new Error(\"Blob retry attempts must be a positive integer\");\n }\n if (!Number.isFinite(retry.baseDelayMs) || retry.baseDelayMs < 0) {\n throw new Error(\"Blob retry base delay must be a finite non-negative number\");\n }\n if (!Number.isFinite(retry.maxTotalMs) || retry.maxTotalMs < 0) {\n throw new Error(\"Blob retry elapsed-time limit must be a finite non-negative number\");\n }\n let activeReads = 0;\n const readWaiters: (() => void)[] = [];\n const activeLists = new Map<CollectionName, Promise<StoredEntry[]>>();\n\n // Retries only transient failures, with exponential backoff bounded by\n // both attempt count and total elapsed time. Non-transient errors (not\n // found, already exists, precondition/version conflicts, malformed data)\n // are rethrown immediately and untouched by this loop.\n async function withTransientRetry<T>(\n collection: CollectionName,\n id: string,\n operation: () => Promise<T>,\n ): Promise<T> {\n const start = now();\n for (let attempt = 0; ; attempt += 1) {\n try {\n return await operation();\n } catch (error) {\n if (!isTransientBlobError(error)) throw error;\n const nextDelayMs = retry.baseDelayMs * 2 ** attempt;\n if (attempt >= retry.attempts - 1 || now() - start + nextDelayMs > retry.maxTotalMs) {\n throw new TransientStoreError(collection, id, error);\n }\n await new Promise((resolve) => setTimeout(resolve, nextDelayMs));\n }\n }\n }\n\n async function limitedRead(pathname: string): Promise<BlobObject | null> {\n if (activeReads < readConcurrency) activeReads += 1;\n else await new Promise<void>((resolve) => readWaiters.push(resolve));\n try {\n return await client.read({ pathname });\n } finally {\n const waiter = readWaiters.shift();\n if (waiter === undefined) activeReads -= 1;\n else waiter();\n }\n }\n\n function pathFor(collection: CollectionName, id: string): string {\n return `${prefix}/${collection}/${id}.json`;\n }\n\n async function readRecord(\n collection: CollectionName,\n id: string,\n ): Promise<{ value: unknown; version: number; etag: string } | null> {\n let object: BlobObject | null;\n try {\n object = await withTransientRetry(collection, id, () => limitedRead(pathFor(collection, id)));\n } catch (error) {\n if (isNotFoundError(error)) {\n return null;\n }\n throw error;\n }\n if (object === null) {\n return null;\n }\n const parsed = JSON.parse(object.body) as { value: unknown; version: number };\n if (typeof parsed.version !== \"number\") {\n throw new Error(`malformed stored record at ${pathFor(collection, id)}`);\n }\n return { value: parsed.value, version: parsed.version, etag: object.etag };\n }\n\n // A write that fails with a transient error (network drop, 5xx) may still\n // have landed on the server before the response reached us: the retry\n // withTransientRetry just took then legitimately collides with our own\n // prior success. These two helpers disambiguate that case from a real\n // conflict with another writer by reading the record back and comparing\n // it to what this call tried to write; only called after this call itself\n // retried (a fresh, non-retried collision is always a real conflict).\n async function wroteThisInsert(\n collection: CollectionName,\n id: string,\n value: unknown,\n ): Promise<boolean> {\n const after = await readRecord(collection, id);\n return after !== null && after.version === 1 && jsonValuesEqual(after.value, value);\n }\n\n async function wroteThisUpdate(\n collection: CollectionName,\n id: string,\n value: unknown,\n expectedNewVersion: number,\n ): Promise<boolean> {\n const after = await readRecord(collection, id);\n return (\n after !== null && after.version === expectedNewVersion && jsonValuesEqual(after.value, value)\n );\n }\n\n return {\n name: \"blob\",\n\n async get(collection, id) {\n const record = await readRecord(collection, id);\n return record === null ? null : { value: record.value, version: record.version };\n },\n\n async insert({ collection, id, value }) {\n // Tracks whether this call itself retried: a write that fails with a\n // transient error can still have landed on the server before the\n // response was lost, so a *retried* write's own \"already exists\"\n // collision is ambiguous rather than automatically a real conflict.\n let attempts = 0;\n try {\n await withTransientRetry(collection, id, () => {\n attempts += 1;\n return client.write({\n pathname: pathFor(collection, id),\n body: JSON.stringify({ value, version: 1 }),\n overwrite: false,\n });\n });\n } catch (error) {\n if (isAlreadyExistsError(error)) {\n if (attempts > 1 && (await wroteThisInsert(collection, id, value))) {\n return;\n }\n throw new RecordExistsError(collection, id);\n }\n throw error;\n }\n },\n\n async update({ collection, id, value, expectedVersion }) {\n const existing = await readRecord(collection, id);\n if (existing === null) {\n throw new RecordNotFoundError(collection, id);\n }\n if (existing.version !== expectedVersion) {\n throw new VersionConflictError(collection, id, expectedVersion);\n }\n // Tracks whether this call itself retried: see the matching comment on\n // insert() above. A retried update's own precondition failure is\n // ambiguous in the same way.\n let attempts = 0;\n try {\n await withTransientRetry(collection, id, () => {\n attempts += 1;\n return client.write({\n pathname: pathFor(collection, id),\n body: JSON.stringify({ value, version: expectedVersion + 1 }),\n overwrite: true,\n // The write API's ifMatch rejects the weak form (W/\"…\") that reads return.\n ifMatch: existing.etag.replace(/^W\\//u, \"\"),\n });\n });\n } catch (error) {\n if (isPreconditionError(error)) {\n if (attempts > 1 && (await wroteThisUpdate(collection, id, value, expectedVersion + 1))) {\n return;\n }\n throw new VersionConflictError(collection, id, expectedVersion);\n }\n throw error;\n }\n },\n\n async list(collection) {\n const active = activeLists.get(collection);\n if (active !== undefined) return active;\n const listing = (async () => {\n const dir = `${prefix}/${collection}/`;\n const paths = await client.listPaths({ prefix: dir });\n const entries: (StoredEntry | null)[] = new Array(paths.length);\n let next = 0;\n let failure: unknown;\n const workers = Array.from(\n { length: Math.min(readConcurrency, paths.length) },\n async () => {\n while (next < paths.length && failure === undefined) {\n const index = next++;\n const pathname = paths[index]!;\n const id = pathname.slice(dir.length).replace(/\\.json$/u, \"\");\n try {\n const record = await readRecord(collection, id);\n entries[index] =\n record === null\n ? null\n : { id, record: { value: record.value, version: record.version } };\n } catch (error) {\n failure ??= error;\n }\n }\n },\n );\n await Promise.all(workers);\n if (failure !== undefined) throw failure;\n return entries.filter((entry): entry is StoredEntry => entry !== null);\n })();\n activeLists.set(collection, listing);\n try {\n return await listing;\n } finally {\n if (activeLists.get(collection) === listing) activeLists.delete(collection);\n }\n },\n };\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { FactoryId, TaskId } from "./schema/id.mjs";
|
|
2
|
+
import { Task } from "./schema/task.mjs";
|
|
3
|
+
import { DateClock } from "./schema/common.mjs";
|
|
4
|
+
import { BudgetLimits, BudgetMonth } from "./schema/budget.mjs";
|
|
5
|
+
import { FactoryStores } from "./store/engine.mjs";
|
|
6
|
+
//#region src/budget.d.ts
|
|
7
|
+
/**
|
|
8
|
+
* Stable factory identity for budget rows. Nothing mints factory ids yet, so
|
|
9
|
+
* the app derives one from a deployment-stable seed such as the Vercel
|
|
10
|
+
* project id.
|
|
11
|
+
*/
|
|
12
|
+
declare function deriveFactoryId(seed: string): FactoryId;
|
|
13
|
+
/** The budget month ("YYYY-MM", UTC) a timestamp falls into. */
|
|
14
|
+
declare function budgetMonthOf(isoDateTime: string): BudgetMonth;
|
|
15
|
+
/**
|
|
16
|
+
* Monthly spend is derived from task rows, never stored: each task counts its
|
|
17
|
+
* settled cost once that exists, otherwise its held reservation. A task
|
|
18
|
+
* settles once, so a settled row never also counts its reservation.
|
|
19
|
+
* Attribution uses the reservation time so tasks that wait across a month
|
|
20
|
+
* boundary charge the month they actually ran in.
|
|
21
|
+
*/
|
|
22
|
+
declare function computeMonthSpendUsd(tasks: readonly Task[], month: BudgetMonth): number;
|
|
23
|
+
/** Derived monthly capacity state before another per-Task reservation. */
|
|
24
|
+
type BudgetStatus = "ok" | "warning" | "exhausted";
|
|
25
|
+
/**
|
|
26
|
+
* True when one more per-task slice fits under the monthly total. Dispatch
|
|
27
|
+
* refuses on this exact predicate and budgetStatus reports "exhausted" on its
|
|
28
|
+
* negation, so refusing and reporting can never disagree.
|
|
29
|
+
*/
|
|
30
|
+
declare function canReserve(spendUsd: number, limits: Pick<BudgetLimits, "totalUsd" | "perTaskReservationUsd">): boolean;
|
|
31
|
+
/** Classify monthly spend using the same exact-fit predicate enforced by dispatch. */
|
|
32
|
+
declare function budgetStatus(spendUsd: number, limits: Pick<BudgetLimits, "totalUsd" | "perTaskReservationUsd" | "warningPercent">): BudgetStatus;
|
|
33
|
+
/** Grace period, clock, and optional snapshot for terminal usage settlement. */
|
|
34
|
+
interface SweepSettlementsOptions {
|
|
35
|
+
/** How long a terminal task waits for late usage before the sweep settles it. */
|
|
36
|
+
graceMinutes?: number;
|
|
37
|
+
now?: DateClock;
|
|
38
|
+
/** A dispatcher-wide snapshot avoids another complete task read. */
|
|
39
|
+
tasks?: readonly Task[];
|
|
40
|
+
}
|
|
41
|
+
/** Task-local charges newly settled by one reconciliation pass. */
|
|
42
|
+
interface SweepSettlementsResult {
|
|
43
|
+
readonly settled: readonly {
|
|
44
|
+
readonly taskId: TaskId;
|
|
45
|
+
readonly totalUsd: number;
|
|
46
|
+
}[];
|
|
47
|
+
}
|
|
48
|
+
/** Task capability required to settle terminal reservations. */
|
|
49
|
+
type SettlementSweepStores = Pick<FactoryStores, "tasks">;
|
|
50
|
+
/**
|
|
51
|
+
* Settle reserved terminal tasks the session hook did not: a task that ended
|
|
52
|
+
* after its session (a change verified later) or whose session died. Settles
|
|
53
|
+
* at the recorded usage, or at zero with a warning reason when none was
|
|
54
|
+
* recorded within the grace window.
|
|
55
|
+
*/
|
|
56
|
+
declare function sweepSettlements(stores: SettlementSweepStores, options?: SweepSettlementsOptions): Promise<SweepSettlementsResult>;
|
|
57
|
+
/** Owner-only notification emitted when monthly spend crosses a budget threshold. */
|
|
58
|
+
interface BudgetNotice {
|
|
59
|
+
outcome: "budget_warning" | "budget_exhausted";
|
|
60
|
+
month: BudgetMonth;
|
|
61
|
+
spendUsd: number;
|
|
62
|
+
limits: BudgetLimits;
|
|
63
|
+
}
|
|
64
|
+
/** Monthly limits, delivery hook, and optional snapshot used for budget reconciliation. */
|
|
65
|
+
interface BudgetSweepOptions {
|
|
66
|
+
factoryId: FactoryId;
|
|
67
|
+
limits: BudgetLimits;
|
|
68
|
+
/** Defaults to true. False reports spending without threshold checks or budget notices. */
|
|
69
|
+
enforce?: boolean;
|
|
70
|
+
/** Defaults to the current UTC month. */
|
|
71
|
+
month?: BudgetMonth;
|
|
72
|
+
now?: DateClock;
|
|
73
|
+
/** Owner-only delivery; invoked at most once per month per outcome. */
|
|
74
|
+
notify?: (notice: BudgetNotice) => Promise<void>;
|
|
75
|
+
/** A dispatcher-wide snapshot avoids another complete task read. */
|
|
76
|
+
tasks?: readonly Task[];
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Derived monthly spend and exact notice outcome produced by budget reconciliation.
|
|
80
|
+
* `stamped` means this sweep recorded the threshold, `already_stamped` means no
|
|
81
|
+
* new stamp was needed, and `delivery_failed` remains eligible for retry.
|
|
82
|
+
*/
|
|
83
|
+
type BudgetSweepResult = {
|
|
84
|
+
readonly month: BudgetMonth;
|
|
85
|
+
readonly spendUsd: number;
|
|
86
|
+
readonly status: "disabled";
|
|
87
|
+
readonly noticeState: "not_applicable";
|
|
88
|
+
} | {
|
|
89
|
+
readonly month: BudgetMonth;
|
|
90
|
+
readonly spendUsd: number;
|
|
91
|
+
readonly status: "ok";
|
|
92
|
+
readonly noticeState: "not_applicable";
|
|
93
|
+
} | {
|
|
94
|
+
readonly month: BudgetMonth;
|
|
95
|
+
readonly spendUsd: number;
|
|
96
|
+
readonly status: "warning";
|
|
97
|
+
readonly noticeState: "stamped" | "already_stamped" | "delivery_failed";
|
|
98
|
+
} | {
|
|
99
|
+
readonly month: BudgetMonth;
|
|
100
|
+
readonly spendUsd: number;
|
|
101
|
+
readonly status: "exhausted";
|
|
102
|
+
readonly noticeState: "stamped" | "already_stamped" | "delivery_failed";
|
|
103
|
+
};
|
|
104
|
+
/** Budget and Task capabilities required to reconcile monthly budget thresholds. */
|
|
105
|
+
type BudgetSweepStores = Pick<FactoryStores, "budgets" | "tasks">;
|
|
106
|
+
/**
|
|
107
|
+
* Ensure the month's budget row, derive spend, and record threshold crossings
|
|
108
|
+
* write-once. Delivery comes first and the stamp only lands after it, so a
|
|
109
|
+
* failed notification retries on the next sweep instead of being lost; the
|
|
110
|
+
* write-once stamp still keeps each threshold to one message per month.
|
|
111
|
+
*/
|
|
112
|
+
declare function sweepBudget(stores: BudgetSweepStores, options: BudgetSweepOptions): Promise<BudgetSweepResult>;
|
|
113
|
+
//#endregion
|
|
114
|
+
export { BudgetNotice, BudgetStatus, BudgetSweepOptions, BudgetSweepResult, BudgetSweepStores, SettlementSweepStores, SweepSettlementsOptions, SweepSettlementsResult, budgetMonthOf, budgetStatus, canReserve, computeMonthSpendUsd, deriveFactoryId, sweepBudget, sweepSettlements };
|
|
115
|
+
//# sourceMappingURL=budget.d.mts.map
|
package/dist/budget.mjs
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { factoryIdSchema } from "./schema/id.mjs";
|
|
2
|
+
import { budgetMonthSchema } from "./schema/budget.mjs";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
//#region src/budget.ts
|
|
5
|
+
/**
|
|
6
|
+
* Stable factory identity for budget rows. Nothing mints factory ids yet, so
|
|
7
|
+
* the app derives one from a deployment-stable seed such as the Vercel
|
|
8
|
+
* project id.
|
|
9
|
+
*/
|
|
10
|
+
function deriveFactoryId(seed) {
|
|
11
|
+
return factoryIdSchema.parse(`fac_${createHash("sha256").update(seed).digest("hex").slice(0, 32)}`);
|
|
12
|
+
}
|
|
13
|
+
/** The budget month ("YYYY-MM", UTC) a timestamp falls into. */
|
|
14
|
+
function budgetMonthOf(isoDateTime) {
|
|
15
|
+
return budgetMonthSchema.parse(isoDateTime.slice(0, 7));
|
|
16
|
+
}
|
|
17
|
+
function microsOf(usd) {
|
|
18
|
+
return Math.round(usd * 1e6);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Monthly spend is derived from task rows, never stored: each task counts its
|
|
22
|
+
* settled cost once that exists, otherwise its held reservation. A task
|
|
23
|
+
* settles once, so a settled row never also counts its reservation.
|
|
24
|
+
* Attribution uses the reservation time so tasks that wait across a month
|
|
25
|
+
* boundary charge the month they actually ran in.
|
|
26
|
+
*/
|
|
27
|
+
function computeMonthSpendUsd(tasks, month) {
|
|
28
|
+
let spendMicros = 0;
|
|
29
|
+
for (const task of tasks) {
|
|
30
|
+
if (budgetMonthOf(task.reservedAt ?? task.createdAt) !== month) continue;
|
|
31
|
+
spendMicros += microsOf(task.settledUsd ?? task.reservedUsd ?? 0);
|
|
32
|
+
}
|
|
33
|
+
return spendMicros / 1e6;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* True when one more per-task slice fits under the monthly total. Dispatch
|
|
37
|
+
* refuses on this exact predicate and budgetStatus reports "exhausted" on its
|
|
38
|
+
* negation, so refusing and reporting can never disagree.
|
|
39
|
+
*/
|
|
40
|
+
function canReserve(spendUsd, limits) {
|
|
41
|
+
return microsOf(spendUsd) + microsOf(limits.perTaskReservationUsd) <= microsOf(limits.totalUsd);
|
|
42
|
+
}
|
|
43
|
+
/** Classify monthly spend using the same exact-fit predicate enforced by dispatch. */
|
|
44
|
+
function budgetStatus(spendUsd, limits) {
|
|
45
|
+
if (!canReserve(spendUsd, limits)) return "exhausted";
|
|
46
|
+
if (microsOf(spendUsd) * 100 >= microsOf(limits.totalUsd) * limits.warningPercent) return "warning";
|
|
47
|
+
return "ok";
|
|
48
|
+
}
|
|
49
|
+
const TERMINAL_TASK_STATES = /* @__PURE__ */ new Set([
|
|
50
|
+
"succeeded",
|
|
51
|
+
"failed",
|
|
52
|
+
"cancelled"
|
|
53
|
+
]);
|
|
54
|
+
/**
|
|
55
|
+
* Settle reserved terminal tasks the session hook did not: a task that ended
|
|
56
|
+
* after its session (a change verified later) or whose session died. Settles
|
|
57
|
+
* at the recorded usage, or at zero with a warning reason when none was
|
|
58
|
+
* recorded within the grace window.
|
|
59
|
+
*/
|
|
60
|
+
async function sweepSettlements(stores, options = {}) {
|
|
61
|
+
const graceMs = (options.graceMinutes ?? 10) * 6e4;
|
|
62
|
+
const nowMs = (options.now ?? (() => /* @__PURE__ */ new Date()))().getTime();
|
|
63
|
+
const settled = [];
|
|
64
|
+
for (const task of options.tasks ?? await stores.tasks.list()) {
|
|
65
|
+
if (task.reservedUsd === void 0 || task.settledUsd !== void 0) continue;
|
|
66
|
+
if (!TERMINAL_TASK_STATES.has(task.state)) continue;
|
|
67
|
+
if (nowMs - Date.parse(task.updatedAt) < graceMs) continue;
|
|
68
|
+
const totalUsd = task.usage?.costUsd ?? 0;
|
|
69
|
+
try {
|
|
70
|
+
const result = await stores.tasks.settle(task.id, {
|
|
71
|
+
cost: { totalUsd },
|
|
72
|
+
reason: task.usage === void 0 ? "usage missing after ten minutes; settled at zero" : "measured usage settled by sweep"
|
|
73
|
+
});
|
|
74
|
+
settled.push({
|
|
75
|
+
taskId: result.id,
|
|
76
|
+
totalUsd
|
|
77
|
+
});
|
|
78
|
+
} catch {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return { settled };
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Ensure the month's budget row, derive spend, and record threshold crossings
|
|
86
|
+
* write-once. Delivery comes first and the stamp only lands after it, so a
|
|
87
|
+
* failed notification retries on the next sweep instead of being lost; the
|
|
88
|
+
* write-once stamp still keeps each threshold to one message per month.
|
|
89
|
+
*/
|
|
90
|
+
async function sweepBudget(stores, options) {
|
|
91
|
+
const now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
92
|
+
const month = options.month ?? budgetMonthOf(now().toISOString());
|
|
93
|
+
const budget = await stores.budgets.ensure({
|
|
94
|
+
factoryId: options.factoryId,
|
|
95
|
+
month,
|
|
96
|
+
limits: options.limits
|
|
97
|
+
});
|
|
98
|
+
const spendUsd = computeMonthSpendUsd(options.tasks ?? await stores.tasks.list(), month);
|
|
99
|
+
if (options.enforce === false) return {
|
|
100
|
+
month,
|
|
101
|
+
spendUsd,
|
|
102
|
+
status: "disabled",
|
|
103
|
+
noticeState: "not_applicable"
|
|
104
|
+
};
|
|
105
|
+
const status = budgetStatus(spendUsd, options.limits);
|
|
106
|
+
async function crossThreshold(notice, outcome) {
|
|
107
|
+
const field = notice === "warning" ? "warningAt" : "exhaustedAt";
|
|
108
|
+
if (budget.notices[field] !== void 0) return "already_stamped";
|
|
109
|
+
if (options.notify !== void 0) try {
|
|
110
|
+
await options.notify({
|
|
111
|
+
outcome,
|
|
112
|
+
month,
|
|
113
|
+
spendUsd,
|
|
114
|
+
limits: options.limits
|
|
115
|
+
});
|
|
116
|
+
} catch {
|
|
117
|
+
return "delivery_failed";
|
|
118
|
+
}
|
|
119
|
+
return (await stores.budgets.stampNotice(month, notice)).stamped ? "stamped" : "already_stamped";
|
|
120
|
+
}
|
|
121
|
+
if (status === "exhausted") {
|
|
122
|
+
const noticeState = await crossThreshold("exhausted", "budget_exhausted");
|
|
123
|
+
if (budget.notices.warningAt === void 0) await stores.budgets.stampNotice(month, "warning");
|
|
124
|
+
return {
|
|
125
|
+
month,
|
|
126
|
+
spendUsd,
|
|
127
|
+
status,
|
|
128
|
+
noticeState
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
if (status === "warning") return {
|
|
132
|
+
month,
|
|
133
|
+
spendUsd,
|
|
134
|
+
status,
|
|
135
|
+
noticeState: await crossThreshold("warning", "budget_warning")
|
|
136
|
+
};
|
|
137
|
+
return {
|
|
138
|
+
month,
|
|
139
|
+
spendUsd,
|
|
140
|
+
status,
|
|
141
|
+
noticeState: "not_applicable"
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
//#endregion
|
|
145
|
+
export { budgetMonthOf, budgetStatus, canReserve, computeMonthSpendUsd, deriveFactoryId, sweepBudget, sweepSettlements };
|
|
146
|
+
|
|
147
|
+
//# sourceMappingURL=budget.mjs.map
|